Documentation

Overview

Shark WebAuthn is a .NET library that provides a server-side implementation of the WebAuthn standard, enabling secure passwordless and multi-factor authentication (MFA) for web applications. It supports key WebAuthn operations, such as public key credential registration and authentication, ensuring compliance with the WebAuthn Level 2 specification.

The library is written in .NET 8, making it suitable for modern ASP.NET Core applications and environments. The library itself is focused solely on the WebAuthn protocol layer – handling attestation, assertion, metadata validation, and related cryptography. It doesn't aim to replace a full identity framework such as ASP.NET Identity.

Shark WebAuthn exposes high-level APIs for both registration and authentication flows:

  • BeginRegistration and CompleteRegistration generate PublicKeyCredentialCreationOptions and process the attestation response during public key credential registration.
  • BeginAuthentication and CompleteAuthentication generate PublicKeyCredentialRequestOptions and validate the assertion response during authentication.

These APIs enables integration with the browser's Web Authentication API while ensuring standards-compliant and secure public key credential handling. More information about the Web Authentication API is available on the MDN Web Docs site.

About This Documentation

This documentation provides comprehensive guidance for integrating and using the Shark WebAuthn .NET library to implement passwordless and multi-factor authentication in ASP.NET Core web applications. It covers server-side configuration, API usage, persistent data storage options, and provides sample client-side integration. Please note that session management and client-side code are not part of the library itself and are offered only as sample references to assist with implementation.

License

Shark WebAuthn is fully open source, with every component freely available on GitHub under the permissive BSD 3-Clause License.

Installing Packages

To begin integrating Shark WebAuthn into your ASP.NET Core application, add the following NuGet packages to your project:

dotnet add package Shark.Fido2.Core
dotnet add package Shark.Fido2.Models
dotnet add package Shark.Fido2.InMemory

These packages provide the core WebAuthn functionality, data models, and an in-memory credential repository for development and testing. For production, consider using a persistent credential store instead of the in-memory implementation. See the Persistent Data Stores section for details on integrating with Microsoft SQL Server and Amazon DynamoDB.

Server-side Configuration

Shark WebAuthn .NET library requires specific configuration to operate as a WebAuthn relying party. Configuration is typically provided via the Fido2Configuration section in your application's configuration files (e.g., appsettings.json, appsettings.Production.json). This section details all available configuration options, their default values, and their intended usage.

Configuration Schema

The following is an example of the server-side configuration.

{
  "Fido2Configuration": {
    "RelyingPartyId": "example.com", // Use 'localhost' for local development
    "RelyingPartyIdName": "Example Corporation",
    "Origins": [ "https://example.com" ], // Use '[ "localhost" ]' for local development
    "Timeout": 60000,
    "AlgorithmsSet": "Extended",
    "AllowNoneAttestation": true,
    "AllowSelfAttestation": true,
    "EnableTrustedExecutionEnvironmentOnly": false,
    "EnableMetadataService": true,
    "EnableStrictAuthenticatorVerification": false,
    "MetadataServiceConfiguration": {
      "MetadataBlobLocation": "https://mds3.fidoalliance.org/",
      "RootCertificateLocationUrl": "https://secure.globalsign.com/cacert/root-r3.crt",
      "MaximumTokenSizeInBytes": 8388608
    }
  }
}

A minimal server-side configuration example is shown below.

{
  "Fido2Configuration": {
    "RelyingPartyId": "example.com", // Use 'localhost' for local development
    "RelyingPartyIdName": "Example Corporation",
    "Origins": [ "https://example.com" ] // Use '[ "localhost" ]' for local development
  }
}

Property Reference

Core Configuration

Option Default Description
RelyingPartyIdValid domain string identifying the Relying Party on whose behalf a given registration or authentication ceremony is being performed. This is a critical parameter in the WebAuthn protocol. It defines the security scope within which credentials are valid. Therefore, careful selection is essential, as an incorrect or overly broad value can lead to unintended credential reuse or security vulnerabilities.
RelyingPartyIdNameHuman-palatable identifier for the Relying Party, intended only for display.
OriginsList of the fully qualified origins of the Relying Party making the request, passed to the authenticator by the browser.
Timeout60000Time, in milliseconds, that the Relying Party is willing to wait for the call to complete.
AlgorithmsSetExtendedSet of the supported cryptographic algorithms. Possible values are Required, Recommended or Extended. More information about the cryptographic algorithms is available on the fidoalliance.org site.
AllowNoneAttestationtrueValue indicating whether None attestation type is acceptable under Relying Party policy. None attestation is used when the authenticator doesn't have any attestation information available.
AllowSelfAttestationtrueValue indicating whether Self attestation type is acceptable under Relying Party policy. Self attestation is used when the authenticator doesn't have a dedicated attestation key pair or a vendor-issued certificate.
EnableTrustedExecutionEnvironmentOnlytrueValue indicating whether the Relying Party trusts only keys that are securely generated and stored in a Trusted Execution Environment (Android Key Attestation).
EnableMetadataServicetrueValue indicating whether the Relying Party uses the FIDO Metadata Service to verify the attestation object. Metadata from the FIDO Metadata Service is stored in an in-memory cache and remains valid until the nextUpdate timestamp, which is received from the metadata BLOB and indicates the latest time a new metadata BLOB may be provided.
EnableStrictAuthenticatorVerificationfalseValue indicating whether the Relying Party requires strict verification of authenticators. If enabled, missing metadata for the authenticator would cause attestation to fail. This parameter is ignored if the FIDO Metadata Service is disabled.

FIDO Metadata Service Configuration

Option Default Description
MetadataBlobLocationhttps://mds3.fidoalliance.org/Location of the centralized and trusted source of information about FIDO authenticators (Metadata Service BLOB).
RootCertificateLocationUrlhttps://secure.globalsign.com/cacert/root-r3.crtLocation of GlobalSign Root R3 for Metadata Service BLOB.
MaximumTokenSizeInBytes8388608Maximum token size in bytes that will be processed. This configuration is related to the Metadata Service BLOB size.

Configuration Usage

Add the Fido2Configuration section to your appsettings.json or environment-specific configuration file.

Best Practices

  • Setting EnableMetadataService to true is recommended in production environments to ensure authenticators are validated against the FIDO Metadata Service.

Troubleshooting

  • If authentication fails with an "origin" error, verify that the Origins array matches the actual origin of your frontend application.
  • If using the FIDO Metadata Service, ensure your application can reach the URLs specified in MetadataServiceConfiguration.

Registering Dependencies

To register the Shark WebAuthn services in your ASP.NET Core application, add the following registrations to your Program.cs file:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Other service registrations

builder.Services.AddFido2(builder.Configuration);
builder.Services.AddFido2InMemoryStore();

var app = builder.Build();

// Configure the HTTP request pipelines

// HTTP request pipelines

app.UseStaticFiles();

// Other HTTP request pipelines

app.MapControllers();

await app.RunAsync();

AddFido2(builder.Configuration) registers the core Shark WebAuthn services using your application's configuration. AddFido2InMemoryStore() registers an in-memory credential repository, suitable for development and testing. For production, consider using a persistent credential store instead of the in-memory implementation.

Server-side API

To enable WebAuthn functionality, your application requires REST API controllers to handle core WebAuthn operations: credential registration (attestation) and authentication (assertion). These controllers expose endpoints for frontend interaction with the WebAuthn flows.

Attestation (Registration)

The Attestation controller handles the registration ceremony.

1. Create the AttestationController and inject an instance of the IAttestation interface into its constructor

[Route("[controller]")]
[ApiController]
public class AttestationController : ControllerBase
{
    private readonly IAttestation _attestation;

    public AttestationController(IAttestation attestation)
    {
        _attestation = attestation;
    }

    // Endpoints for attestation operations
}

2. Add endpoint to get create credential options

[HttpPost("options")]
public async Task<IActionResult> Options(ServerPublicKeyCredentialCreationOptionsRequest request, CancellationToken cancellationToken)
{
    var createOptions = await _attestation.BeginRegistration(request.Map(), cancellationToken);
    var response = createOptions.Map();
    HttpContext.Session.SetString("CreateOptions", JsonSerializer.Serialize(createOptions));
    return Ok(response);
}

3. Add endpoint to create credential

[HttpPost("result")]
public async Task<IActionResult> Result(ServerPublicKeyCredentialAttestation request, CancellationToken cancellationToken)
{
    var createOptionsString = HttpContext.Session.GetString("CreateOptions");
    var createOptions = JsonSerializer.Deserialize<PublicKeyCredentialCreationOptions>(createOptionsString!);
    await _attestation.CompleteRegistration(request.Map(), createOptions!, cancellationToken);
    return Ok(ServerResponse.Create());
}

Assertion (Authentication)

The Assertion controller handles the authentication ceremony.

1. Create the AssertionController and inject an instance of the IAssertion interface into its constructor

[Route("[controller]")]
[ApiController]
public class AssertionController : ControllerBase
{
    private readonly IAssertion _assertion;

    public AssertionController(IAssertion assertion)
    {
        _assertion = assertion;
    }

    // Endpoints for assertion operations
}

2. Add endpoint to get request credential options

[HttpPost("options")]
public async Task<IActionResult> Options(ServerPublicKeyCredentialGetOptionsRequest request, CancellationToken cancellationToken)
{
    var requestOptions = await _assertion.BeginAuthentication(request.Map(), cancellationToken);
    var response = requestOptions.Map();
    HttpContext.Session.SetString("RequestOptions", JsonSerializer.Serialize(requestOptions));
    return Ok(response);
}

3. Add endpoint to validate credential

[HttpPost("result")]
public async Task<IActionResult> Result(ServerPublicKeyCredentialAssertion request, CancellationToken cancellationToken)
{
    var requestOptionsString = HttpContext.Session.GetString("RequestOptions");
    var requestOptions = JsonSerializer.Deserialize<PublicKeyCredentialRequestOptions>(requestOptionsString!);
    await _assertion.CompleteAuthentication(request.Map(), requestOptions!, cancellationToken);
    return Ok(ServerResponse.Create());
}

Important: These controller examples use session state to store options between requests. To enable session support in your application, ensure that Program.cs includes calls to AddSession and UseSession:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSession();

// Other service registrations

var app = builder.Build();

app.UseSession();

// Other HTTP request pipelines

While the default in-memory session state is sufficient for development and testing, it is recommended to use a distributed cache (such as Redis) in production environments.

Client-side Integration

To finalize the implementation, you must incorporate JavaScript code that interacts with the browser's Web Authentication API. This API manages the client-side authentication process. Please note that JavaScript files must be placed in the wwwroot/js folder so the server can serve them as static assets.

1. Add the attestation.js file containing the navigator.credentials.create() invocation

The following is a minimal sample of JavaScript code for creating discoverable credentials using the Web Authentication API in the browser. This code demonstrates the basic flow for communicating with the server-side REST API endpoints described above. Important: This example does not include production-level safeguards for simplicity. For real-world applications, you should add proper error handling and input validation.

Click to expand
async function registration(username, displayName) {
    const optionsRequest = {
        username: username,
        displayName: displayName,
        attestation: 'direct',
        authenticatorSelection: {
            residentKey: 'required',
            userVerification: 'required',
            requireResidentKey: true
        }
    };
    const options = await fetchAttestationOptions(optionsRequest);
    await createCredential(options);
}

async function createCredential(options) {
    const credentialCreationOptions = {
        publicKey: {
            rp: {
                id: options.rp.id,
                name: options.rp.name,
            },
            user: {
                id: toUint8Array(options.user.id),
                name: options.user.name,
                displayName: options.user.displayName,
            },
            pubKeyCredParams: options.pubKeyCredParams.map(param => ({
                type: param.type,
                alg: param.alg,
            })),
            authenticatorSelection: options.authenticatorSelection,
            challenge: toUint8Array(options.challenge),
            excludeCredentials: options.excludeCredentials.map(credential => ({
                id: toUint8Array(credential.id),
                transports: credential.transports,
                type: credential.type,
            })),
            timeout: options.timeout,
            attestation: options.attestation
        },
    };

    let attestation = await navigator.credentials.create(credentialCreationOptions);
    const credentials = {
        id: attestation.id,
        rawId: toBase64Url(attestation.rawId),
        response: {
            attestationObject: toBase64Url(attestation.response.attestationObject),
            clientDataJson: toBase64Url(attestation.response.clientDataJSON),
            transports: attestation.response.getTransports(),
        },
        type: attestation.type,
    };

    await fetchAttestationResult(credentials);

    window.alert('User was registered');
}

async function fetchAttestationOptions(optionsRequest) {
    const response = await fetch('/attestation/options/', {
        method: 'POST',
        headers: {
            'content-type': 'application/json'
        },
        body: JSON.stringify(optionsRequest)
    });
    if (response.ok) {
        return await response.json();
    }
}

async function fetchAttestationResult(credentials) {
    const response = await fetch('/attestation/result/', {
        method: 'POST',
        headers: {
            'content-type': 'application/json'
        },
        body: JSON.stringify(credentials)
    });
}

window.registration = registration;

2. Add the assertion.js file containing the navigator.credentials.get() invocation

The following is a minimal sample of JavaScript code for performing authentication with discoverable credentials using the Web Authentication API in the browser. Important: This example does not include production-level safeguards for simplicity. For real-world applications, you should add proper error handling and input validation.

Click to expand
async function authentication() {
    const optionsRequest = {};
    const options = await fetchAssertionOptions(optionsRequest);
    await requestCredential(options);
}

async function requestCredential(options) {
    const credentialRequestOptions = {
        publicKey: {
            rpId: options.rpId,
            challenge: toUint8Array(options.challenge),
            allowCredentials: [],
            timeout: options.timeout
        },
    };

    let assertion = await navigator.credentials.get(credentialRequestOptions);
    const credentials = {
        id: assertion.id,
        rawId: toBase64Url(assertion.rawId),
        response: {
            authenticatorData: toBase64Url(assertion.response.authenticatorData),
            clientDataJson: toBase64Url(assertion.response.clientDataJSON),
            signature: toBase64Url(assertion.response.signature),
            userHandle: toBase64Url(assertion.response.userHandle),
        },
        type: assertion.type,
    };

    await fetchAssertionResult(credentials);

    window.alert('User was authenticated');
}

async function fetchAssertionOptions(optionsRequest) {
    const response = await fetch('/assertion/options/', {
        method: 'POST',
        headers: {
            'content-type': 'application/json'
        },
        body: JSON.stringify(optionsRequest)
    });
    if (response.ok) {
        return await response.json();
    }
}

async function fetchAssertionResult(credentials) {
    const response = await fetch('/assertion/result/', {
        method: 'POST',
        headers: {
            'content-type': 'application/json'
        },
        body: JSON.stringify(credentials)
    });
}

window.authentication = authentication;

This JavaScript code bridges the browser's Web Authentication API with the server-side REST API endpoints provided by the ASP.NET Core controllers described above.

3. Add utilities functions

Add utility functions to convert between Base64URL strings and Uint8Array byte arrays, ensuring proper encoding and decoding for binary data used in web authentication operations.

4. Add HTML markup

Add the HTML markup to support credential registration and authentication. Refer to the sample Index.cshtml file for an example.

5. Add JavaScript logic for the site

Add JavaScript logic to connect the HTML markup with credential registration and authentication functionality. Refer to the sample site.js file for an example.

6. Reference JavaScript files

Reference JavaScript files in _Layout.cshtml.

<script src="~/js/attestation.js" asp-append-version="true"></script>
<script src="~/js/assertion.js" asp-append-version="true"></script>
<script src="~/js/utilities.js" asp-append-version="true"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

7. Run the ASP.NET Core application over HTTPS

Browsers Support

Up-to-date details regarding the Web Authentication API support across modern browsers can be found on Can I use page.

Persistent Data Stores

For production environments, you should use a persistent credential store instead of the in-memory implementation. Shark WebAuthn provides support for various database providers.

Microsoft SQL Server

To use Microsoft SQL Server as your credential store, follow these steps:

1. Database Setup

Create the necessary database table by executing the SQL table creation script available at SQL Server Table Creation Script.

2. Installing Package

Add the following NuGet packages to your project:

dotnet add package Shark.Fido2.SqlServer

3. Registering Dependencies

Replace the in-memory store registration with Microsoft SQL Server in your Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Other service registrations

builder.Services.AddFido2(builder.Configuration);
builder.Services.AddFido2SqlServer();

4. Configuration

Add the connection string to your appsettings.json or environment-specific configuration file:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=SQL_SERVER_INSTANCE;Database=DATABASE_NAME;OTHER_PARAMETERS;"
  }
}

Amazon DynamoDB

To use Amazon DynamoDB as your credential store, follow these steps:

1. Table Setup

Create the Amazon DynamoDB table with the following parameters:

  • Table name: Credential
  • Partition key: cid (Binary)
  • Sort key: N/A
  • Global secondary index name: UserNameIndex
    • Partition key: un (String)
    • Projected properties: INCLUDE: cid, tsp

Ensure your AWS credentials have the following permissions to access the Credential table.

Actions

  • dynamodb:GetItem
  • dynamodb:Query
  • dynamodb:PutItem
  • dynamodb:UpdateItem

Resources

  • arn:aws:dynamodb:AWS_REGION:AWS_ACCOUNT_ID:table/Credential
  • arn:aws:dynamodb:AWS_REGION:AWS_ACCOUNT_ID:table/Credential/index/UserNameIndex

2. Installing Package

Add the following NuGet packages to your project:

dotnet add package Shark.Fido2.DynamoDB

3. Registering Dependencies

Replace the in-memory store registration with Amazon DynamoDB in your Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Other service registrations

builder.Services.AddFido2(builder.Configuration);
builder.Services.AddFido2DynamoDB();

4. Configuration

Add the following Amazon DynamoDB configuration to your appsettings.json or environment-specific configuration file:

{
  "AmazonDynamoDbConfiguration": {
    "AwsRegion": "eu-central-1",
    "ConnectTimeoutInSeconds": 10,
    "MaxErrorRetry": 3,
    "AccessKey": "",
    "SecretKey": ""
  }
}

Custom Implementation

If your application uses a database provider that is not yet supported out of the box, you can create a custom persistent credential store by implementing the ICredentialRepository interface.

1. Implementing the Interface

Create a new class that implements the ICredentialRepository interface from the Shark.Fido2.Core.Abstractions.Repositories namespace. This interface defines the contract required for storing and retrieving credentials.

2. Registering Your Implementation

Register your custom repository with the dependency injection container in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Other service registrations

builder.Services.AddFido2(builder.Configuration);
builder.Services.AddScoped<ICredentialRepository, CustomCredentialRepository>();

3. Configuration

Add your custom store settings to appsettings.json or environment-specific configuration file, and bind it as needed in your class using IConfiguration.

Extension Points

Shark WebAuthn .NET library allows you to replace certain functionalities with custom implementations. This extensibility enables you to tailor the WebAuthn behavior to your specific requirements.

Cryptographic Challenge Generator

The most notable example is the IChallengeGenerator interface, which represents the logic to generate cryptographic challenges used in WebAuthn operations.

To provide a custom logic for cryptographic challenge generation, implement the interface:

public sealed class CustomChallengeGenerator : IChallengeGenerator
{
    public byte[] Get()
    {
        Span<byte> challengeSpan = stackalloc byte[37];
        RandomNumberGenerator.Fill(challengeSpan);
        return challengeSpan.ToArray();
    }
}

Then replace the default implementation in the dependency injection container:

builder.Services.AddFido2(builder.Configuration);
builder.Services.AddFido2InMemoryStore();
builder.Services.AddTransient<IChallengeGenerator, CustomChallengeGenerator>();

Another example of an extension point is the IUserIdGenerator interface, which is responsible for generating user identifiers.