Skip to main content

WebAuthn

WebAuthn brings the browser's Web Authentication API to Wisej.NET. It lets users register and sign in with passkeys, the device's built-in authenticator (Windows Hello, Touch ID, Face ID, Android biometrics) or a hardware security key such as a YubiKey, instead of a password or alongside one.

The extension covers the whole round trip:

  • WebAuthn.CreateAsync asks the browser to create a new credential and returns its public key, which you store with the user account (registration);
  • WebAuthn.GetAsync asks the browser to sign a challenge with that credential (authentication);
  • WebAuthn.Validate checks the signature on the server against the stored public key. ES256 and RS256 keys are supported.

The private key never leaves the user's authenticator, and the server stores only public keys. A leaked credential database therefore gives an attacker nothing to sign in with.

Installation​

Add the Wisej-4-WebAuthn package to your Wisej.NET project:

dotnet add package Wisej-4-WebAuthn

The package targets .NET Framework 4.8 and .NET 8 or later. WebAuthn is a static class that you call directly.

HTTPS and the relying party ID
  • WebAuthn works only in a secure context: HTTPS, or http://localhost during development.
  • The relying party ID you pass to RelyingParty must be the page's domain or a parent domain of it. For example, example.com is valid for app.example.com. For localhost, use "localhost". Otherwise the browser rejects the request.
  • Credentials are bound to the relying party ID. If you change it later, every user must register again.

Checking for support​

Before offering passkey registration, check whether the device has a user-verifying platform authenticator, such as Windows Hello, Touch ID or Face ID:

private async void Page1_Load(object sender, EventArgs e)
{
this.buttonRegisterPasskey.Visible =
await WebAuthn.IsUserVerifyingPlatformAuthenticatorAvailableAsync();
}

Hardware security keys (AuthenticatorAttachment.CrossPlatform) can work even when this returns false.

Registration​

Registration creates a key pair on the user's authenticator. Generate a random challenge, describe your application (the relying party) and the user, then call CreateAsync:

using System.Security.Cryptography;
using Wisej.Core;
using Wisej.Ext.WebAuthn;

private async void buttonRegisterPasskey_Click(object sender, EventArgs e)
{
// A fresh random challenge for every request.
var challengeBytes = new byte[32];
using (var rng = RandomNumberGenerator.Create())
rng.GetBytes(challengeBytes);
string challenge = Convert.ToBase64String(challengeBytes);

var rp = new RelyingParty("example.com", "Example Inc.");
var user = new PublicKeyCredentialUserEntity(
id: currentUser.Id, // a stable, opaque id (no personal data)
name: currentUser.Email,
displayName: currentUser.FullName);

var algorithms = new[]
{
new PublicKeyCredentialParameters(COSEAlgorithmIdentifier.ES256),
new PublicKeyCredentialParameters(COSEAlgorithmIdentifier.RS256)
};
var selection = new AuthenticatorSelectionCriteria(AuthenticatorAttachment.Platform);

try
{
CredentialsResponse response = await WebAuthn.CreateAsync(
challenge, rp, user, algorithms, selection, 60000, AttestationConveyancePreference.None);

// Check that the browser signed *our* challenge.
if (!response.ClientData.Challenge.SequenceEqual(challengeBytes))
throw new InvalidOperationException("Challenge mismatch.");

// Store the credential with the user account.
PublicKey publicKey = response.AuthenticatorData.PublicKey;
SaveCredential(currentUser.Id, publicKey.CredentialID, JSON.Stringify(publicKey));

AlertBox.Show("Passkey registered.");
}
catch (Exception ex)
{
// The user cancelled, the request timed out, or the browser refused it.
AlertBox.Show("Registration failed: " + ex.Message, MessageBoxIcon.Error);
}
}

SaveCredential stands for your own persistence code. Store the CredentialID and the serialized PublicKey against the user. A user can have several credentials, one per device or security key.

ArgumentNotes
challengeA base64 string of at least 16 random bytes, new for every request.
rpRelyingParty(id, name): your domain and the name the browser shows.
userPublicKeyCredentialUserEntity(id, name, displayName). Use an opaque id, never an email address.
publicKeyCredentialParametersThe algorithms you accept, in order of preference. Offer ES256 and RS256, the two that Validate supports.
authenticatorSelectionAuthenticatorSelectionCriteria: Platform for the device's built-in authenticator, CrossPlatform for security keys.
timeoutMilliseconds the user has to respond.
attestationAttestationConveyancePreference. None is right for most applications.

Authentication​

To sign in, send a new challenge together with the credentials registered for the user. The browser asks the user to confirm (fingerprint, face, PIN or key touch) and returns a signature, which you check with Validate:

private async void buttonSignIn_Click(object sender, EventArgs e)
{
var challengeBytes = new byte[32];
using (var rng = RandomNumberGenerator.Create())
rng.GetBytes(challengeBytes);
string challenge = Convert.ToBase64String(challengeBytes);

// The credentials registered for this user.
var stored = LoadCredentials(this.textBoxUser.Text);
var allowCredentials = stored
.Select(c => new PublicKeyCredentialDescriptor(c.CredentialID))
.ToArray();

try
{
CredentialsResponse response = await WebAuthn.GetAsync(challenge, allowCredentials, 60000);

// The response doesn't say which credential signed: find the key that validates.
var credential = stored.FirstOrDefault(c =>
WebAuthn.Validate(new PublicKey(c.PublicKeyJson),
response.AuthenticatorData.Base64,
response.ClientData.Base64,
response.Signature));

bool valid =
credential != null
&& response.ClientData.Challenge.SequenceEqual(challengeBytes)
&& response.ClientData.Type == "webauthn.get"
&& response.ClientData.Origin == "https://app.example.com"
&& response.AuthenticatorData.UserPresent;

if (valid)
SignIn(credential.UserId);
else
AlertBox.Show("Sign-in failed.", MessageBoxIcon.Error);
}
catch (Exception ex)
{
AlertBox.Show("Sign-in cancelled: " + ex.Message);
}
}

Pass null as allowCredentials to let the user choose any discoverable credential (passkey) they have for your site, with no user name typed first. Then use CredentialsResponse.UserHandle, the user id given at registration, to find the account.

Validate checks only the signature

WebAuthn.Validate verifies that the signature matches the stored public key. It does not check the rest of the response, and skipping those checks opens the door to replayed or forwarded assertions. Your code must also:

Supported algorithms

Validate supports ES256 and RS256 keys. It returns false for other algorithms, and throws NotImplementedException for EdDSA. Offer only ES256 and RS256 in CreateAsync, so that no authenticator creates a key you can't verify.

Errors​

CreateAsync and GetAsync throw an Exception carrying the browser's message when the request fails. That happens when the user cancels or the timeout expires, when the relying party ID doesn't match the page, when the page isn't in a secure context, or when no matching credential is found. Always wrap the calls in try/catch.

API reference​