Passkeys in ASP.NET Core Identity

Passkeys in ASP.NET Core Identity

- Matt

Ein Kunde wollte für sein internes Portal keine Passwortrichtlinie mehr pflegen - keine Ablaufregeln, keine Diskussion über Sonderzeichen, sondern Anmeldung per Fingerabdruck oder Windows Hello. Genau dafür bringt ASP.NET Core Identity seit .NET 10 eingebaute Unterstützung für Passkeys mit, ohne dass man selbst eine WebAuthn-Bibliothek einbinden muss.

Einschalten

Die Registrierung läuft über eine einzige Erweiterungsmethode:

builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddPasskeys();

Damit stehen auf SignInManager<TUser> und UserManager<TUser> die Methoden für Attestation (Registrierung) und Assertion (Anmeldung) bereit. Im Blazor Web App-Template ist das schon verdrahtet, in eigenen Razor- oder Minimal-API-Projekten schreibt man die beiden Endpunkte für Creation- und Request-Options selbst:

app.MapPost("/Account/PasskeyCreationOptions", async (
    HttpContext context, UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager) =>
{
    var user = await userManager.GetUserAsync(context.User);
    var userId = await userManager.GetUserIdAsync(user);
    var userName = await userManager.GetUserNameAsync(user) ?? "User";

    var optionsJson = await signInManager.MakePasskeyCreationOptionsAsync(new()
    {
        Id = userId,
        Name = userName,
        DisplayName = userName
    });

    return TypedResults.Content(optionsJson, contentType: "application/json");
});

Die Seite reicht das Ergebnis unverändert an navigator.credentials.create() weiter. PerformPasskeyAttestationAsync prüft anschließend Challenge, Origin und Signatur, und AddOrUpdatePasskeyAsync legt den öffentlichen Schlüssel in der neuen Tabelle AspNetUserPasskeys ab. Für die Anmeldung läuft es spiegelbildlich über MakePasskeyRequestOptionsAsync und PasskeySignInAsync.

Wo die Grenze verläuft

Microsoft nennt das explizit keine vollwertige WebAuthn-Bibliothek, sondern eine auf Identity zugeschnittene Umsetzung der gängigsten Szenarien. Zwei Einschränkungen sind für Anwendungen mit höheren Sicherheitsanforderungen relevant. Erstens validiert die Standardimplementierung keine Attestation Statements - wer belegen will, mit welchem Gerätetyp registriert wurde, muss VerifyAttestationStatement in IdentityPasskeyOptions selbst implementieren, inklusive eigenem Zertifikatsspeicher. Zweitens sind Passkeys als primärer Faktor gedacht, nicht als zweiter - eine bestehende Zwei-Faktor-Pipeline lässt sich nicht einfach um Passkeys erweitern.

Auch die Domain-Bindung braucht Aufmerksamkeit. Ohne explizit gesetztes ServerDomain leitet Identity die Relying-Party-ID aus dem Host-Header ab, und ein auf app.contoso.com registrierter Passkey funktioniert dann auf jeder Subdomain davon. Wer das nicht will, setzt ServerDomain fest und sorgt dafür, dass der Reverse Proxy den Host-Header nicht ungeprüft durchreicht.

Der Haken beim ersten Test

Beim Ausprobieren in Firefox mit aktiviertem eingebauten Passwort-Manager brach die Registrierung mit TypeError: Illegal invocation ab. Der Grund: Manche Passwort-Manager implementieren PublicKeyCredential.toJSON nicht korrekt, worauf JSON.stringify beim Serialisieren des Credentials scheitert. Die Dokumentation liefert dafür einen Workaround, der das Credential von Hand in Base64url umwandelt, statt sich auf toJSON zu verlassen - bis die Passwort-Manager nachziehen, bleibt das Handarbeit im JavaScript der Blazor-Komponente.

Account-Recovery bleibt ebenfalls Aufgabe der Anwendung: Das Blazor-Template verlangt deshalb weiterhin ein Passwort oder einen externen Anbieter als Rückfallebene. Reine Passkey-Konten sind vorgesehen, aber nicht der Standardfall.

Automatischer CSRF-Schutz in ASP.NET Core betrifft übrigens auch die beiden eigenen Endpunkte hier - sie nehmen POST-Anfragen von angemeldeten Nutzern entgegen und sollten entsprechend geschützt sein.