Fingerprint Verification in ASP.NET Core with SourceAFIS (1:1 Matching)
DEV Community

Fingerprint Verification in ASP.NET Core with SourceAFIS (1:1 Matching)

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 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.

Comments

No comments yet. Start the discussion.