Listen
Copied RSS Feed

ASP.NET Core

How to Implement Passkey in ASP.NET Core with Fido2-net-lib?

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.

What is Passkey 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.

Getting started with fido2-net-lib

To implement Passkey authentication on our website, we should set up the fido2-net-lib library. Follow these steps to get started:

Step 1: Install fido2-net-lib

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

Step 2: Set up your project

Create a new ASP.NET Core app or use an existing one. Ensure you have a proper structure for handling API requests and responses.

Step 3: Configure the FIDO2 service

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"
    };
}

Step 4: Registration of Passkey

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.

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

Generate attestation options

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:

  • User information: The first parameter is a Fido2User object that contains information about the user, such as their display name, username, and a unique identifier.
  • Exclude list: The second parameter is a list of PublicKeyCredentialDescriptor objects. This list can be used to exclude certain credentials from being used. For example, you can prevent users from registering the same device multiple times.
  • Authenticator selection: The third parameter is an AuthenticatorSelection object that defines the criteria for selecting authenticators. For instance, you might specify that only cross-platform authenticators are allowed.
  • Attestation conveyance preference: The final parameter is an AttestationConveyancePreference enum, indicating how attestation data should be conveyed to the relying party. Options include None, Indirect, and Direct.

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.

Register credentials

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.

Step 5: Passkey authentication

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.

Generate assertion options

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.

Verify the response

Step 1: Convert assertion options to the appropriate format

Convert the received assertion options from Base64URL format to Uint8Array format. This conversion is necessary because the WebAuthn API uses Uint8Array for cryptographic operations.

Step 2: Generate an assertion

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.

Step 3: Prepare the assertion response

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.

Step 4: Send the assertion response to the server for verification

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:

  1. Deserialization: First, MakeAssertionAsync deserializes the received assertion response. This means it translates the data from its transmitted format back into a usable form.
  2. Credential retrieval: The method retrieves the user’s stored credential information from the database. This information typically includes the public key associated with the user’s passkey.
  3. Assertion request construction: The method constructs an assertion request object. This object contains essential elements such as the challenge, the user’s credentials, and other parameters needed for the verification process.
  4. Verification: Using the FIDO2 library, the MakeAssertionAsync method verifies the assertion. It checks that the signature matches and confirms that the user is the owner of the credential.

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.

References

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.

Conclusion

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.

Related blogs

Meet the Author

Darshankumar Pandiane

Full-stack developer at Syncfusion with 1 year of experience in web development. My expertise lies in ASP.NET Core, MVC, JavaScript, HTML, and CSS. Passionate about learning new technologies, I enjoy sharing insights and knowledge from my development journey.