Fingerprint Verification in ASP.NET Core with SourceAFIS (1:1 Matching)
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 dee
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.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.