Sooner or later a backend has to answer a hard question: is this fingerprint the same person we enrolled earlier? An attendance system, a KYC check, an agent verifying a customer, a high-security login. It feels like deep, vendor-only territory, the kind of thing you buy an expensive SDK for. For the basic case, 1:1 verification, it is not. You can get a working thread going in an afternoon with an open-source library.
This post walks through that thread in ASP.NET Core using SourceAFIS: how a fingerprint becomes a template, how two templates are compared, how to pick a threshold, and how to wrap it in a minimal API. It is intentionally naive. At the end I will be honest about everything a real, scalable system needs on top of it.
1:1 verification vs 1:N identification
First, scope it, because these are two very different problems.
- 1:1 verification answers: is this fingerprint the same finger that was enrolled for this specific user? One probe, one stored template. This is what a fingerprint login does.
- 1:N identification answers: who does this fingerprint belong to, out of everyone in the database? One probe against millions of templates. Different, much harder problem.
This article is only about 1:1. We come back to 1:N at the end.
The mental model
The whole thing is four steps:
fingerprint image -> template -> match score -> threshold -> match / no match
- A template is the compact set of features (minutiae) extracted from a fingerprint image. You store the template, never the raw image.
- Matching two templates produces a similarity score (a number).
- You compare that score to a threshold to decide match or no match.
That is it. SourceAFIS does the hard math for the first three; you own the threshold and the plumbing.
Setup
Create an API project and add SourceAFIS:
dotnet new web -n FingerprintDemo
cd FingerprintDemo
dotnet add package SourceAFIS
SourceAFIS v3 does not decode PNG or JPEG for you. It expects raw 8-bit grayscale pixels. So we also add an image library to decode uploads:
dotnet add package SixLabors.ImageSharp
Note: the exact SourceAFIS constructor and options shape have changed across major versions. If a signature below does not match your version, check the SourceAFIS docs, the concepts are identical.
From image to template
Decode the uploaded file to grayscale, then build a template. Tell SourceAFIS the scan resolution (500 DPI is the common default for fingerprint sensors), because it matters for feature extraction.
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SourceAFIS;
// Decode an uploaded PNG/JPEG into raw 8-bit grayscale pixels.
static (int width, int height, byte[] pixels) DecodeGrayscale(byte[] fileBytes)
{
using var image = Image.Load<L8>(fileBytes); // L8 = 8 bits luminance
var pixels = new byte[image.Width * image.Height];
image.CopyPixelDataTo(pixels);
return (image.Width, image.Height, pixels);
}
// Turn grayscale pixels into a SourceAFIS template.
static FingerprintTemplate Extract(int width, int height, byte[] grayscale)
{
var options = new FingerprintImageOptions { Dpi = 500 };
var image = new FingerprintImage(width, height, grayscale, options);
return new FingerprintTemplate(image);
}
Enrollment: extract once, store the template
On enrollment, extract the template and serialize it to bytes. Store those bytes, tied to the user. Do not store the fingerprint image.
FingerprintTemplate template = Extract(width, height, grayscale);
byte[] serialized = template.ToByteArray(); // store this in your DB
To rebuild a template later, you just pass the bytes back:
var template = new FingerprintTemplate(serialized);
Verification: match probe against the enrolled template
At login, extract a template from the freshly scanned finger (the probe), build a matcher from the enrolled template, and get a score.
static double Match(byte[] enrolledSerialized, FingerprintTemplate probe)
{
var enrolled = new FingerprintTemplate(enrolledSerialized);
var matcher = new FingerprintMatcher(enrolled);
return matcher.Match(probe); // higher score = more similar
}
Choosing a threshold
The score is meaningless until you compare it to a threshold, and the threshold is a business decision, not a technical constant.
- Raise the threshold: fewer false accepts (someone else's finger matching), but more false rejects (the real user gets denied). Lower FMR, higher FRR.
- Lower the threshold: the opposite.
SourceAFIS calibrates its scores so that a threshold around 40 corresponds to a false match rate of roughly 0.01%. That is a sensible starting point:
const double Threshold = 40;
bool isMatch = score >= Threshold;
But starting point is the key phrase. You tune this on your own data, your own sensors, and your own risk appetite. A payments app and a gym check-in do not want the same threshold.
The minimal API
Putting the thread together with two endpoints. This uses an in-memory dictionary as a stand-in for a database, so it is obviously not production code.
using SourceAFIS;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// userId -> serialized enrolled template. A real app uses a database.
var store = new Dictionary<string, byte[]>();
static async Task<byte[]> ReadBody(HttpRequest req)
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
return ms.ToArray();
}
app.MapPost("/enroll/{userId}", async (string userId, HttpRequest req) =>
{
var (w, h, gray) = DecodeGrayscale(await ReadBody(req));
var template = Extract(w, h, gray);
store[userId] = template.ToByteArray();
return Results.Ok(new { enrolled = true });
});
app.MapPost("/verify/{userId}", async (string userId, HttpRequest req) =>
{
if (!store.TryGetValue(userId, out var enrolled))
return Results.NotFound(new { error = "user not enrolled" });
var (w, h, gray) = DecodeGrayscale(await ReadBody(req));
var probe = new FingerprintTemplate(
new FingerprintImage(w, h, gray, new FingerprintImageOptions { Dpi = 500 }));
double score = new FingerprintMatcher(new FingerprintTemplate(enrolled)).Match(probe);
return Results.Ok(new { score, match = score >= 40 });
});
app.Run();
Post a fingerprint image to /enroll/alice, then post another scan to /verify/alice, and you get back a score and a match decision. That is a working 1:1 fingerprint verification service.
What this naive version deliberately ignores
The code above is a thread to pull on, not a system. Everything below is what stands between it and something you would put in front of real users:
- Image quality and failed captures: partial, smudged, wet or rotated prints. Production systems score capture quality and ask for a re-scan before they even try to match.
- Liveness and anti-spoofing: the code above would happily match a photocopy or a gelatin fake. Detecting a live finger is a whole field of its own.
- Secure storage: a template is biometric data, some of the most sensitive PII there is. It must be encrypted at rest, access-controlled, and handled under laws like GDPR Article 9 or BIPA. You cannot treat it like a password hash.
- Standards and interoperability: real deployments use standardized templates (ISO/IEC 19794-2) and image formats (WSQ) so that enrollments survive across different sensors and vendors.
- Multiple fingers and modalities: enrolling several fingers, and often fusing fingerprint with face for reliability.
-
1:N at scale: matching one probe against millions of templates is not a
forloop overMatch(). It needs candidate filtering, indexing, and careful throughput engineering, or a single identification takes minutes.
Where this leaves you
For a feature, a fingerprint login for a known user, the thread in this post is genuinely enough. Extract, store, match, threshold. You now understand every step.
Turning that into a real biometric identification system, one that scales to large populations, resists spoofing, stays compliant, and stays fast at 1:N, means putting all of those missing pieces together correctly. That is a different level of work, and its own discipline. But it starts exactly here, with understanding what a template is and what a match score means.
If building that kind of system is your problem, this is the foundation to build it on.
Top comments (0)