How to Implement Passkey Authentication in ASP.NET Core
By FoxLearn 2/11/2025 3:34:17 AM 105
Leveraging the FIDO2 standards, Passkey authentication provides password-less, phishing-resistant security using public-key cryptography.
In this guide, we'll walk you through how to implement Passkey authentication in your ASP.NET Core application using the fido2-net-lib library, which simplifies the integration of FIDO2 authentication.
What is Passkey Authentication?
Passkey authentication is a modern, secure way to authenticate users without relying on traditional passwords. Instead, it utilizes public-key cryptography. This method involves a key pair where the public key is stored on the server, and the private key remains securely on the user’s device. When a user attempts to authenticate, the server sends a challenge that the user’s device signs with their private key, proving their identity without transmitting the private key itself.
Getting Started with fido2-net-lib
To integrate Passkey authentication, you’ll need to set up the fido2-net-lib library in your .NET project. Here’s how you can get started:
Step 1: Install fido2-net-lib
First, add the fido2-net-lib package to your project using either NuGet or the .NET CLI:
dotnet add package fido2-net-lib
Step 2: Set Up Your Project
Create a new ASP.NET Core app or use an existing one. Ensure your project structure is ready for handling API requests and responses.
Step 3: Configure the FIDO2 Service
The next step is to configure the FIDO2 service in your application by setting up your relying party information, such as your website’s name, domain, and origin.
using Fido2NetLib; public void ConfigureServices(IServiceCollection services) { services.AddFido2(options => { options.ServerDomain = "example.com"; options.ServerName = "Example"; options.Origins = "https://example.com"; }); }
Step 4: User Registration
For Passkey authentication, users need to register their devices. This involves generating attestation options, sending them to the user’s device, and verifying the response before storing the credentials.
What Are Attestation Options?
Attestation options are a set of parameters sent from the server to the user’s device during registration. These options include details about the relying party, the user, and the desired security settings. The user's device uses these options to generate a new key pair and returns an attestation object that the server can verify.
Generate Attestation Options
The following code shows how to generate the attestation options:
[Route("/AttestationOptions")] public JsonResult Register([FromBody] string username) { var user = DemoStorage.GetUser(username, () => new Fido2User { DisplayName = displayName, Name = username, Id = Encoding.UTF8.GetBytes(username) }); var existingKeys = Storage.GetCredentialsByUser(username).ToList(); var options = fido.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), AuthenticatorSelection.Default, AttestationConveyancePreference.None); HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); return Json(options); }
In this example:
- User information: The
Fido2User
object contains essential details like the username and a unique user identifier. - Exclude list: The list of
PublicKeyCredentialDescriptor
objects ensures no duplicate devices are registered for the same user. - Authenticator selection: Defines the criteria for selecting authenticators, such as whether to allow cross-platform devices.
- Attestation conveyance preference: Specifies how attestation data should be conveyed.
Register Credentials
To complete the registration, implement client-side logic that uses the attestation options to create a new credential.
const response = await fetch('/api/AttestationOptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, username }) }); const options = await response.json(); const credential = await navigator.credentials.create({ publicKey: options }); const attestationResponse = { id: credential.id, rawId: Array.from(new Uint8Array(credential.rawId)), type: credential.type, response: { attestationObject: Array.from(new Uint8Array(credential.response.attestationObject)), clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)) } }; await fetch('/api/fido2/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(attestationResponse) });
After the client sends the response, the server verifies and stores the credentials:
[Route("/register")] public async Task<JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) { var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); var options = CredentialCreateOptions.FromJson(jsonOptions); IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => { var users = await Storage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); return users.Count == 0; }; var credential = await fido.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken); Storage.AddCredential(options.User, new Credential { Id = credential.Id, PublicKey = credential.PublicKey, SignCount = credential.SignCount, RegDate = DateTimeOffset.UtcNow, Transports = credential.Transports, IsBackedUp = credential.IsBackedUp, userId = userId, DeviceName = "User Device 1" }); return Json(credential.Status); }
Step 5: Passkey Authentication
Once users have registered their devices, you’ll need to implement the authentication flow to verify their identity.
Generate Assertion Options
Generate the assertion options by fetching the user’s registered credentials:
[Route("/AssertionOptions")] public JsonResult AssertionOptionsPost(string email) { var user = DemoStorage.GetUser(username); var existingCredentials = Storage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); var options = fido.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred); HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); return Json(options); }
Verify the Response
Once the client receives the assertion options, the user’s device generates an assertion.
const response = await fetch('/api/AssertionOptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const options = await response.json(); options.challenge = base64urlToUint8Array(options.challenge); options.allowCredentials = options.allowCredentials.map(cred => ({ ...cred, id: base64urlToUint8Array(cred.id) })); let assertion; try { assertion = await navigator.credentials.get({ publicKey: options }); } catch (err) { alert('Failed to get assertion: ' + err); } const assertionResponse = { id: assertion.id, rawId: arrayBufferToBase64Url(new Uint8Array(assertion.rawId)), type: assertion.type, response: { authenticatorData: arrayBufferToBase64Url(new Uint8Array(assertion.response.authenticatorData)), clientDataJSON: arrayBufferToBase64Url(new Uint8Array(assertion.response.clientDataJSON)), signature: arrayBufferToBase64Url(new Uint8Array(assertion.response.signature)), userHandle: assertion.response.userHandle ? arrayBufferToBase64Url(new Uint8Array(assertion.response.userHandle)) : null } }; await fetch('/api/VerifyPasskey', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(assertionResponse) });
Step 6: Verify Passkey
Finally, the server verifies the assertion and authenticates the user:
[Route("/VerifyPasskey")] public async Task<JsonResult> MakeAssertion(VerifyPasskeyResponse clientResponse, CancellationToken cancellationToken) { var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); HttpContext.Session.Remove("fido2.assertionOptions"); var options = AssertionOptions.FromJson(jsonOptions); var creds = Storage.GetCredentialById(clientResponse.Id); var storedCounter = creds.SignatureCounter; IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => { var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle); return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); }; var res = await fido.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback); DemoStorage.UpdateCounter(res.CredentialId, res.Counter); return Json(res); }
The MakeAssertionAsync
method ensures that the user’s identity is authenticated securely by verifying the assertion and updating the signature counter. If the verification is successful, the user is granted access.
Implementing Passkey authentication in your ASP.NET Core app enhances security by providing a password-less, phishing-resistant authentication method. By using the fido2-net-lib library, you can integrate FIDO2 authentication easily, making your application more secure while improving the user experience.
- Basic Authentication in ASP.NET Core
- How to Implement Stripe Payment Gateway in ASP.NET Core
- Comparing ASP.NET SOAP Services and Core APIs
- How to fix System.InvalidOperationException: Scheme already exists: Identity.Application
- Two-Factor Authentication with Google Authenticator in ASP.NET Core
- Implementing Passkeys to ASP.NET Core Application
- Implementing Google Authentication in the Blazor WebAssembly App
- How to Add a custom InputFormatter in ASP.NET Core