Tuesday, 23 September 2025

Passkeys in ASP.NET Core

Leave a Comment

I've created this tutorial to help you implement passkeys in ASP.NET Core. It offers a complete, executable code example and goes over the fundamental ideas. The most recent ASP.NET Core updates, on which the entire system is based, provide native passkey support, which facilitates much easier integration.


This is a comprehensive tutorial on how to use passkeys in ASP.NET Core.

Implementing Passkeys in ASP.NET Core

1. Introduction: The Dawn of a Passwordless Future

In an increasingly security-conscious digital landscape, passwords have become a liability. They are susceptible to phishing, credential stuffing, and data breaches. Passkeys, built on the Web Authentication (WebAuthn) standard, offer a revolutionary alternative. They replace traditional passwords with a secure, phishing-resistant public-private key pair. In this guide, we will explore how to integrate this powerful authentication method into your ASP.NET Core application, leveraging the framework's built-in Identity features and the WebAuthn API.

2. Understanding the Core Concepts

Passkeys rely on a simple yet robust cryptographic model. This model involves three key components:

  • Relying Party (RP): This is your ASP.NET Core server. It is the entity that you, the developer, control, and it's the server the user is trying to authenticate with.

  • Authenticator: This is the user's device—a smartphone, laptop, or a physical security key (like a YubiKey). It securely holds the private key and performs the cryptographic signing.

  • WebAuthn API: A JavaScript API in the browser that acts as the bridge between the Relying Party and the Authenticator, orchestrating the registration and authentication ceremonies.

When a user registers a passkey, their authenticator generates a unique public-private key pair for your website (the Relying Party). The private key remains on the device, never leaving it. The public key is securely sent to your server, where it is stored and linked to the user's account. For subsequent logins, the server sends a unique, cryptographic challenge to the user's browser. The browser then prompts the authenticator to use the private key to sign the challenge. The signed challenge is sent back to the server, which verifies it using the stored public key. If the signature is valid, the user is authenticated.

This process is inherently phishing-resistant because the user never types a secret that can be stolen, and the cryptographic signature is bound to your specific website.

3. Server-Side Implementation with ASP.NET Core Identity

The introduction of native passkey support in ASP.NET Core Identity simplifies the server-side implementation significantly, eliminating the need for complex external libraries for most use cases.

Step 1. Project Setup

Begin by creating a new ASP.NET Core Web App with the "Individual Accounts" authentication type. This template automatically includes ASP.NET Core Identity and a database context.

Step 2. Configure Passkeys

In your Program.cs file, you need to enable passkey support within the Identity configuration. This is a simple, one-line addition.

// Add services to the container.
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddPasskeys() // New in .NET 10 or later
    .AddDefaultTokenProviders();

The AddPasskeys() extension method handles all the necessary service registrations and middleware for WebAuthn.

Step 3. Registration Endpoint

The registration process requires two parts: generating the creation options and verifying the attestation response.

A. Generate Creation Options: This API endpoint is responsible for creating a PublicKeyCredentialCreationOptions object, which is a JSON payload that the browser needs to create the passkey.

[HttpPost("login/finish")]
public async Task<IActionResult> FinishAuthentication([FromBody] JsonElement credential)
{
    var result = await _signInManager.SignInWithPasskeyAsync(credential.GetRawText());
    if (result.Succeeded)
    {
        return Ok("Login successful.");
    }
    return Unauthorized(result.ToString());
}

B. Verify Attestation Response: After the browser receives the options and the user creates the passkey, it sends the result (the attestation) back to your server. This endpoint verifies the attestation and saves the new credential.

Step 4. Authentication Endpoint

The login process is similar to registration, but it uses the navigator.credentials.get() method.

4. Client-Side Implementation (JavaScript)

The front-end code is responsible for calling the server endpoints and interacting with the WebAuthn API.

const bufferToBase64Url = (buffer) => {
    const bytes = new Uint8Array(buffer);
    let str = '';
    for (const char of bytes) {
        str += String.fromCharCode(char);
    }
    return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};

const base64UrlToBuffer = (base64Url) => {
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const binary = atob(base64);
    const len = binary.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
        bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
};

// Function to handle passkey registration
const registerPasskey = async (username) => {
    try {
        // Step 1: Get registration options from the server
        const response = await fetch('/Passkeys/register/start', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(username),
        });

        if (!response.ok) {
            throw new Error(`Server error: ${response.statusText}`);
        }

        const options = await response.json();

        // Convert base64Url-encoded fields to ArrayBuffers
        options.publicKey.challenge = base64UrlToBuffer(options.publicKey.challenge);
        options.publicKey.user.id = base64UrlToBuffer(options.publicKey.user.id);

        if (options.publicKey.excludeCredentials) {
            options.publicKey.excludeCredentials.forEach(cred => {
                cred.id = base64UrlToBuffer(cred.id);
            });
        }

        // Step 2: Call the WebAuthn API to create the credential
        const credential = await navigator.credentials.create(options);

        // Step 3: Serialize and send the credential to the server
        const attestationResponse = {
            id: credential.id,
            rawId: bufferToBase64Url(credential.rawId),
            type: credential.type,
            response: {
                attestationObject: bufferToBase64Url(credential.response.attestationObject),
                clientDataJSON: bufferToBase64Url(credential.response.clientDataJSON),
            },
        };

        const verificationResponse = await fetch('/Passkeys/register/finish', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(attestationResponse),
        });

        if (verificationResponse.ok) {
            console.log('Passkey registered successfully!');
        } else {
            const error = await verificationResponse.text();
            console.error(`Passkey registration failed: ${error}`);
        }

    } catch (error) {
        console.error('Registration failed:', error);
    }
};

// Function to handle passkey authentication
const authenticatePasskey = async (username) => {
    try {
        // Step 1: Get authentication options from the server
        const response = await fetch('/Passkeys/login/start', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(username),
        });

        if (!response.ok) {
            throw new Error(`Server error: ${response.statusText}`);
        }

        const options = await response.json();

        // Convert base64Url-encoded fields to ArrayBuffers
        options.publicKey.challenge = base64UrlToBuffer(options.publicKey.challenge);
        options.publicKey.allowCredentials.forEach(cred => {
            cred.id = base64UrlToBuffer(cred.id);
        });

        // Step 2: Call the WebAuthn API to get the assertion
        const credential = await navigator.credentials.get(options);

        // Step 3: Serialize and send the assertion to the server
        const assertionResponse = {
            id: credential.id,
            rawId: bufferToBase64Url(credential.rawId),
            type: credential.type,
            response: {
                authenticatorData: bufferToBase64Url(credential.response.authenticatorData),
                clientDataJSON: bufferToBase64Url(credential.response.clientDataJSON),
                signature: bufferToBase64Url(credential.response.signature),
                userHandle: bufferToBase64Url(credential.response.userHandle),
            },
        };

        const verificationResponse = await fetch('/Passkeys/login/finish', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(assertionResponse),
        });

        if (verificationResponse.ok) {
            console.log('Passkey login successful!');
        } else {
            const error = await verificationResponse.text();
            console.error(`Passkey login failed: ${error}`);
        }

    } catch (error) {
        console.error('Login failed:', error);
    }
};

// Example usage
// await registerPasskey('testuser');
// await authenticatePasskey('testuser');
```
Conclusion: A Simpler, More Secure Future

Implementing passkeys in ASP.NET Core has never been easier, especially with the framework's native support. By adopting this technology, you are not only providing a more secure login experience that is resistant to the most common attack vectors, but you are also dramatically improving user convenience. This passwordless flow removes the cognitive burden of remembering complex passwords and the friction of forgotten password resets. It's a powerful step toward a more secure, user-friendly web.

This guide provides a solid foundation for your project. You can now build upon this knowledge by adding features like multi-factor authentication, account management pages to view and revoke passkeys, and user interface elements that provide clear feedback during the registration and authentication processes. Let me know if you would like to dive deeper into the fido2-net-lib library for more advanced configurations or explore how to integrate passkeys with existing identity providers.

Best ASP.NET Core 10.0 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 8.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFEASP.NET, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFEASP.NET guarantees 99.9% uptime for ASP.NET Core. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.

0 comments:

Post a Comment