DEV Community

Cover image for Fingerprint Verification in ASP.NET Core with SourceAFIS (1:1 Matching)
Lepres KIKOUNGA
Lepres KIKOUNGA

Posted on • Originally published at lepresk.com

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 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To rebuild a template later, you just pass the bytes back:

var template = new FingerprintTemplate(serialized);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 for loop over Match(). 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)