TL;DR: Discover how to implement Passkey authentication in your ASP.NET Core app using the Fido2-net-lib library. Enhance security with a passwordless, phishing-resistant method that leverages public-key cryptography.
In today’s digital landscape, ensuring secure and user-friendly authentication methods is crucial for safeguarding user data and enhancing user experience. One such method gaining traction is Passkey authentication, which leverages the FIDO2 standards to provide password-less, phishing-resistant security.
In this blog, we’ll explore how to implement Passkey authentication in your ASP.NET Core app using the fido2-net-lib, a .NET library that simplifies the integration of FIDO2 authentication.
Passkey authentication is a modern approach to securing user accounts without relying on traditional passwords. It uses public-key cryptography to authenticate users, providing a more secure and user-friendly experience.
This method involves a user’s device generating a key pair, where the public key is stored on the server, and the private key remains securely on the user’s device. When the user attempts to authenticate, the server sends a challenge that the user’s device signs with the private key, proving their identity without transmitting it.
To implement Passkey authentication on our website, we should set up the fido2-net-lib library. Follow these steps to get started:
First, install the fido2-net-lib package in your .NET project. You can also install this using the NuGet Package Manager or the .NET CLI:
dotnet add package fido2-net-lib
Create a new ASP.NET Core app or use an existing one. Ensure you have a proper structure for handling API requests and responses.
Add the FIDO2 service to your app. This involves setting up the necessary configurations, such as relying party information, which includes 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" }; }
We need to implement the registration flow to allow users to register a passkey for their account. This involves generating attestation options, sending them to the user’s device, and verifying the response before storing the credentials.
Attestation options are a set of parameters sent from the server to the user’s device during registration. These options include information about your website (the relying party), the user, and desired security parameters. The user’s device uses these options to create a new key pair and returns an attestation object that the server can verify and store.
Refer to the following code example to generate attestation options.
[Route("/AttestationOptions")] public JsonResult Register([FromBody] string username) { // 1. Get user var user = DemoStorage.GetUser(username, () => new Fido2User { DisplayName = displayName, Name = username, Id = Encoding.UTF8.GetBytes(username) //byte representation of userID is required }); // 2. Get user passkey credentials stored var existingKeys = Storage.GetCredentialsByUser(username).ToList(); // 3. Attestation options var options = fido.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), AuthenticatorSelection.Default, AttestationConveyancePreference.None); // 4. Temporarily store options, session/in-memory cache/redis/db HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); // 5. return options to the client return Json(options); }
The RequestNewCredential method is a key part of the FIDO2 registration process. Here’s a breakdown of its parameters and functionality:
The generated attestation options are sent to the user’s device as a JSON response. The user’s device will use these options to create a new key pair and return an attestation object.
To complete the registration process, implement client-side logic that uses the attestation options to create a new credential. This involves calling the navigator.credentials.create() API with the attestation options.
// Send a POST request to the server to get attestation options const response = await fetch('/api/AttestationOptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, username }) // Send the user's display name and username in the request body }); // Parse the JSON response to get the attestation options const options = await response.json(); // Use the attestation options to create a new credential on the user's device const credential = await navigator.credentials.create({ publicKey: options }); // Construct the attestation response to send back to the server const attestationResponse = { id: credential.id, // The unique ID for the credential rawId: Array.from(new Uint8Array(credential.rawId)), // Convert rawId to an array of bytes type: credential.type, // The type of credential (usually "public-key") response: { attestationObject: Array.from(new Uint8Array(credential.response.attestationObject)), // Convert attestation object to an array of bytes clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)) // Convert client data JSON to an array of bytes } }; // Send the attestation response to the server for verification and storage await fetch('/api/fido2/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(attestationResponse) // Send the attestation response as JSON });
When the client returns a response, we verify and store the credentials.
[Route("/register")] public async Task<JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) { // 1. Get the options that we sent to the client var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); var options = CredentialCreateOptions.FromJson(jsonOptions); // 2. Create a callback so that lib can verify that the credential id is unique to this user IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => { var users = await Storage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); if (users.Count > 0) return false; return true; }; // 2. Verify and make the credentials var credential = await fido.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken); // 3. Store the credentials in the database 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" }); // 4. Return "ok" to the client return Json(credential.Status); }
The MakeNewCredentialAsync method verifies the client’s attestation response and creates a new credential if the verification is successful. This method ensures the security and integrity of the registration process by validating the information provided by the user’s device.
After users have registered their devices, we need to implement the authentication flow to verify their identity. This process involves generating assertion options, sending them to the user’s device, and then verifying the response to log the user in.
Refer to the following code example to generate the assertion options.
[Route("/AssertionOptions")] public JsonResult AssertionOptionsPost(string email) { // 1. Get user from DB var user = DemoStorage.GetUser(username); // 2. Get registered credentials from the database List<PublicKeyCredentialDescriptor> existingCredentials = Storage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); // 3. Assertion options var options = fido.GetAssertionOptions( existingCredentials, UserVerificationRequirement.Preferred ); // 4. Temporarily store options, session/in-memory cache/redis/db HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); // 5. Return options to the client return Json(options); }
The GetAssertionOptions method generates and returns the necessary parameters (assertion options) that the user’s device will use to authenticate. These options typically include information about the relying party (your website), the user, and the challenge that must be signed by the user’s device.
Convert the received assertion options from Base64URL format to Uint8Array format. This conversion is necessary because the WebAuthn API uses Uint8Array for cryptographic operations.
Use the navigator.credentials.get() method, passing in the assertion options. This method will prompt the user’s device to generate an assertion, which includes the necessary cryptographic proof of the user’s identity.
Once the user’s device generates the assertion, prepare the assertion response. This involves converting the various components of the response to Base64URL format, which is suitable for transmission over the network.
Finally, send the prepared assertion response back to the server for verification.
//Get assertion options from the server const response = await fetch('/api/AssertionOptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const options = await response.json(); // Convert base64url to Uint8Array options.challenge = base64urlToUint8Array(options.challenge); options.allowCredentials = options.allowCredentials.map(cred => ({ ...cred, id: base64urlToUint8Array(cred.id) })); // Get an assertion from the user's device let assertion; try { assertion = await navigator.credentials.get({ publicKey: options }); } catch (err) { alert('Failed to get assertion: ' + err); } // Prepare the assertion response to be sent to the server 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 } }; // Send the assertion response to the server for verification const verificationResponse = await fetch('/api/VerifyPasskey', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(assertionResponse) });
When the client returns a response, verify the credentials to log in with credentials.
[Route("/VerifyPasskey")] public async Task<JsonResult> MakeAssertion(VerifyPasskeyResponse clientResponse, CancellationToken cancellationToken) { // 1. Get the assertion options we sent the client and remove them from storage var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); HttpContext.Session.Remove("fido2.assertionOptions"); var options = AssertionOptions.FromJson(jsonOptions); // 2. Get registered credentials from the database StoredCredential creds = Storage.GetCredentialById(clientResponse.Id); // 3. Get credential counter from the database var storedCounter = creds.SignatureCounter; // 4. Create a callback to check if userhandle owns the credentialId IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => { List<StoredCredential> storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle); return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); }; // 5. Make the assertion var res = await fido.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback); // 6. Store the updated counter DemoStorage.UpdateCounter(res.CredentialId, res.Counter); // 7. return OK to client return Json(res); }
The MakeAssertionAsync is a crucial server-side method in the Web Authentication (WebAuthn) process, particularly during the authentication phase. This method plays a key role in verifying the assertion created by a user’s device.
When a user tries to log in using a passkey, their device generates an assertion. This assertion is then sent to the server, where the MakeAssertionAsync method takes over. Here’s how the method works:
If the verification is successful, the user is authenticated and granted access to the system.
MakeAssertionAsync method is integral to implementing secure, passwordless authentication. It facilitates a smooth and reliable authentication process, enhancing security and improving the user experience.
To view the Fido2-net-lib demo, go through this GitHub link, or you can also check out the following links for more details about implementing the Passkey on your web page.
Thank you for reading this blog! Implementing Passkey authentication using fido2-net-lib enhances your website’s security by providing a passwordless, phishing-resistant authentication method. By following the steps outlined in this blog, you can integrate this modern authentication mechanism into your ASP.NET Core app, ensuring a safer and more convenient experience for your users.
With cybersecurity’s increasing importance, adopting advanced authentication methods like Passkeys is a proactive step toward protecting user data and building trust in digital services.