Skip to content

Sample demonstrating how to use FIDO2 keys with ASP.NET Core using WebAuthn

License

Notifications You must be signed in to change notification settings

wmeints/webauthn-sample

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebAuthn sample

This sample demonstrates how to integrate WebAuthn with FIDO2 keys into ASP.NET Core. Please read the rest of this README to get a full picture of what's in the sample and how the various components work together.

System requirements

  • .NET SDK 6.0
  • Latest LTS release Node

Getting started

You can run this sample as a docker container using the following steps:

  • git clone https://github.com/wmeints/webauthn-sample/

After cloning the repository, create a file .env with the following content:

DB_PASSWORD=<your-password>

You can choose any password you like. Once you have the password set, perform the following steps from the root of the project directory:

  • docker-compose up -d
  • cd src/WebAuthnSample
  • dotnet user-secrets set "ConnectionStrings:DefaultDatabase" "data source=127.0.0.1;initial catalog=webauthn;user id=sa;password=<your-password>"

Make sure the password in the last step matches the password that was stored in the .env file. Now execute the following command from src/WebAuthnSample directory:

  • dotnet run

Open the browser to https://localhost:7140 follow the on-screen instructions.

Documentation

Project structure

The project is made out of two parts:

  • src/WebAuthnSample - Contains the server-side application.
  • src/WebAuthnSample/Client - Contains the client-side scripts.

The server-side components are written in ASP.NET Core 6. For the client-side components I've used webpack with React. I've found that typescript is great for this kind of more complicated, but not too complicated stuff. And webpack is great, since it allows me to generate compact packages of client-side scripts.

The client-side scripts

The client-side scripts use React. I've made a setup where each page gets its own dedicated script.

  • src/WebAuthnSample/Pages/Login.cshtml - Uses src/WebAuthnSample/wwwroot/js/authentication.js.
  • src/WebAuthnSample/Pages/Register.cshtml - Uses src/WebAuthnSample/wwwroot/js/registration.js.

The registration script is compiled from src/WebAuthnSample/Client/registration/index.tsx and related files. The authentication script is compiled from src/WebAuthnSample/Client/authentication/index.tsx and related files.

For performance reasons, I've split the shared scripts into src/WebAuthnSample/wwwroot/shared.js. This script is contains react, react-dom, and the sources from src/WebAuthnSample/Client/shared/.

The server-side components

The server-side part follows this layout:

  • src/WebAuthnSample/Controllers - Contains two API controllers for registration and authentication.
  • src/WebAuthnSample/Services - Contains business logic to implement the registration and authentication.
  • src/WebAuthnSample/Models - Contains models used in the application.
  • src/WebAuthnSample/Data - Contains persistence logic for the application.
  • src/WebAuthnSample/Forms - Contains the forms used in the controllers.
  • src/WebAuthnSample/Pages - Contains the pages for the application.

How does WebAuthn work?

WebAuthn is a standard for authenticating users using public key cryptography. You can choose to use a platform key or a cross-platform key. The former is usually a trusted platform module in your laptop or desktop. The latter is a USB key or a mobile phone.

Using WebAuthn, we can move away from passwords completely. But you can use it also as a second factor next to a traditional password. In this example, we're using WebAuthn to replace passwords with a public key credential.

Registering a user using WebAuthn

To set up authentication with a public key credential, we need to register a new account first. The following flow demonstrates how this is done:

sequenceDiagram
    actor User
    participant Browser
    participant Server
    User->>Browser: navigate (/Identity/Account/Register)
    activate Browser
    Browser->>Server: POST /api/registration/options
    Server-->>Browser: PublicKeyCredentialOptions
    Browser->>Browser: Create new key pair + challenge response
    Browser->>Server: POST /api/registration/complete
    Server->>Server: verify challenge response
    Server->>Server: Create user + store credentials
    Server-->>Browser: HTTP 202
    Browser->>Browser: Redirect to login page
Loading
  1. When the user navigates to the registration page, we'll ask the user to fill out the registration form. After filling out the form, they click on Register to start the registration process.
  2. Next, we need to get a challenge from the server. This challenge includes a set of options under which conditions the server accepts a new public key for registration.
  3. After that, the browser generates a new key pair and answers the challenge. This ensures that the server knows who's registering and we're not some rogue client to break into the website. Each challenge can be used once to register a new key pair.
  4. The browser posts the response to the challenge to the server to complete the registration. The server needs to verify the challenge response against the challenge it generated earlier. We stored the challenge options in the HTTP session earlier. You can choose to store this elsewhere on the server as necessary.
  5. When the challenge is verified and found to be correct, the server sends a HTTP 202 response back to the client.
  6. The browser will redirect the user to the login page.

The ceremony to verify a public key pair during the registration process is quite extensive. If you're interested in the full spec, I can recommend to read the WebAuthn spec.

After we've registered the user, we can work on the login phase.

Authenticating a user using WebAuthn

The following flow demonstrates how to authenticate a user using Webauthn:

sequenceDiagram
    actor User
    participant Browser
    participant Server
    User->>Browser: navigate (/Identity/Account/Login)
    Browser->>Server: POST /api/authentication/options
    Server-->>Browser: PublicKeyCredentialRequestOptions
    Browser->>Browser: Get public key pair using opions
    Browser->>Server: POST /api/authentication/login
    Server->>Server: Verify challenge
    Server->>Server: Sign in user
    Server->>Browser: HTTP 202
    Browser->>Browser: Redirect to homepage
Loading
  1. When the user navigates to the login page, fills out the login form, and clicks Login the login process is started.
  2. The browser will request an authentication challenge from the server that includes all registered public keys for its account.
  3. The server returns an authentication challenge that includes all the registered public keys for the user's account.
  4. The browser asks the user to insert the USB key they like to login with. If the key matches one of the registered public keys, the browser asks the user to touch their key to confirm the login.
  5. The browser signs the authentication challenge and sends this response to the server.
  6. The server verifies the challenge, and signs in the user.
  7. The browser receives a HTTP 202, and redirects the user to the home page.

If you're interested in the ceremony to verify a login challenge, you can find a description in the WebAuthn spec.

Compiling the solution

In case you want to change something in the solution, please follow the instructions in this section.

Changing the client-side components

As we discussed earlier in the README, this project contains some components written in React. To compile these components, you'll need to follow these steps:

  • cd src/WebAuthnSample/Client
  • npm install
  • npm run build

This produces a production-ready build of the scripts. You can't easily debug these. So if you're debugging an issue, you should use the command npm run build:debug instead.

Changing the server-side components

The server-side components are implemented in ASP.NET Core. You can change these just like any other .NET project. For database migrations, we recommend that you use dotnet-ef to generate them. I'm assuming you know how to write applications in ASP.NET Core for the purpose of this sample.

Contributing

If you find an bug in the code, please submit an issue in this repo. Pull requests are welcome too!