WebAuthn Level 3 — Passkeys / FIDO2 registration and authentication via base64url.
Module webauthn | Source packages/front/fw/src/dom/utils/webauthn.js | Deps random, b64 | Worker-safe no
Ergonomic wrapper around navigator.credentials.{create,get} (W3C WebAuthn Level 3 / CTAP2).
All binary inputs and outputs are expressed in base64url without padding (RFC 4648 §5);
apps never manipulate raw ArrayBuffers.
Important: signature verification is not performed client-side. The RP server (or
sde_auth_webauthn) validates the attestation and assertion from the fields returned by this module.
Resolve
const webauthn = runtime.resolve('webauthn');
// Returns: { register, authenticate, support }
API
| Method | Signature | Returns |
|---|---|---|
register |
(opts: RegisterOpts) => Promise<RegistrationResult> |
Registration credential (base64url) |
authenticate |
(opts?: AuthOpts) => Promise<AuthenticationResult> |
Authentication assertion (base64url) |
support |
() => Promise<SupportInfo> |
Browser WebAuthn capabilities |
webauthn.register(opts)
Calls navigator.credentials.create({ publicKey }) with the provided options.
Options register
| Option | Type | Default | Description |
|---|---|---|---|
rp |
{id: string, name: string} |
required | Relying Party — id = effective domain of the origin |
user |
{id: string|Uint8Array, name, displayName} |
required | User identity ; id ≤ 64 bytes |
challenge |
Uint8Array | string (base64url) |
generated | Challenge 32+ bytes; random.bytes(32) if absent |
pubKeyCredParams |
Array |
ES256 + RS256 | Accepted algorithms (alg: -7, -257) |
authenticatorSelection |
Object |
— | userVerification, residentKey, authenticatorAttachment |
attestation |
string |
'none' |
'none' | 'indirect' | 'direct' | 'enterprise' |
timeout |
number |
60_000 |
Timeout in milliseconds |
excludeCredentials |
Array<string|Uint8Array> |
— | Credential IDs to exclude (base64url) |
Return RegistrationResult
{
id: string, // base64url (= rawId)
rawId: string, // base64url
type: 'public-key',
response: {
clientDataJSON: string, // base64url
attestationObject: string, // base64url (CBOR — decode server-side)
transports?: string[], // ['internal','usb','nfc',…] if available
},
authenticatorAttachment?: string,
}
webauthn.authenticate(opts?)
Calls navigator.credentials.get({ publicKey }).
Options authenticate
| Option | Type | Default | Description |
|---|---|---|---|
challenge |
Uint8Array | string |
generated | random.bytes(32) if absent |
rpId |
string |
— | RP ID; absent = current origin |
allowCredentials |
Array<string|Uint8Array> |
— | Allowed credential IDs (base64url) |
userVerification |
string |
'preferred' |
'required' | 'preferred' | 'discouraged' |
timeout |
number |
60_000 |
ms |
Return AuthenticationResult
{
id: string, // base64url
rawId: string, // base64url
type: 'public-key',
response: {
clientDataJSON: string, // base64url
authenticatorData: string, // base64url
signature: string, // base64url — verify server-side
userHandle?: string, // base64url, present if resident key
},
}
webauthn.support()
Detects WebAuthn capabilities without user interaction.
{
available: boolean, // navigator.credentials.create exists
userVerifyingPlatformAuthenticator: boolean, // Touch ID, Windows Hello, etc.
conditionalMediation: boolean, // autofill passkeys (CM UI)
}
Examples
Registering a passkey
const webauthn = runtime.resolve('webauthn');
// 1. Check support
const caps = await webauthn.support();
if (!caps.available) throw new Error('WebAuthn not available');
// 2. Register (challenge normally comes from the server)
const cred = await webauthn.register({
rp: { id: 'example.com', name: 'Example App' },
user: { id: 'user-uuid-42', name: 'alice@example.com', displayName: 'Alice' },
authenticatorSelection: {
userVerification: 'preferred',
residentKey: 'preferred',
},
});
// 3. Send to RP server for verification
await fetch('/api/auth/register', {
method: 'POST',
body: JSON.stringify(cred), // everything is base64url, JSON-friendly
});
Authenticating with an existing passkey
const webauthn = runtime.resolve('webauthn');
// challenge provided by the server
const serverChallenge = '<base64url from /api/auth/challenge>';
const assertion = await webauthn.authenticate({
rpId: 'example.com',
challenge: serverChallenge,
userVerification: 'preferred',
allowCredentials: ['<credentialId-base64url>'],
});
// Send to RP server
await fetch('/api/auth/verify', {
method: 'POST',
body: JSON.stringify(assertion),
});
Checking capabilities
const webauthn = runtime.resolve('webauthn');
const { available, userVerifyingPlatformAuthenticator, conditionalMediation } = await webauthn.support();
if (userVerifyingPlatformAuthenticator) {
// Offer Touch ID / Face ID / Windows Hello
}
if (conditionalMediation) {
// Enable passkey autofill on the email field
}
Notes
- Signature verification is out of scope client-side —
attestationObjectandsignaturemust be transmitted to the RP server (orsde_auth_webauthn) which validates them against the registered public key. user.idaccepts astring(UTF-8 encoded) or aUint8Array; the result is always ≤ 64 bytes per WebAuthn L3 spec §5.4.3.- The challenge is generated via
random.bytes(32)if absent; in production, it should always come from the server to prevent replay attacks. support()is async only becauseisUserVerifyingPlatformAuthenticatorAvailablereturns a Promise; the other two fields are synchronous.- Transports (
usb,nfc,ble,internal,hybrid) are included in theregisterresult if the authenticator exposes them viagetTransports().
See also
- random — challenge generation client-side
- b64 — standard base64 encoding (basis for base64url)
- leaderElection — cross-tab coordination useful for multi-step auth flows