Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@passwordless-id/webauthn

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@passwordless-id/webauthn - npm Package Compare versions

Comparing version 1.6.2 to 2.0.0

dist/browser/webauthn.min.js

25

dist/esm/authenticatorMetadata.js

@@ -13,2 +13,3 @@ /**

"08987058-cadc-4b81-b6e1-30de50dcbe96": "Windows Hello",
"092277e5-8437-46b5-b911-ea64b294acb7": "Taglio CTAP2.1 CS",
"09591fc6-9811-48f7-8f57-b9f23df6413f": "Pone Biometrics OFFPAD Authenticator",

@@ -24,6 +25,9 @@ "0acf3011-bc60-f375-fb53-6f05f43154e0": "Nymi FIDO2 Authenticator",

"175cd298-83d2-4a26-b637-313c07a6434e": "Chunghwa Telecom FIDO2 Smart Card Authenticator",
"19083c3d-8383-4b18-bc03-8f1c9ab2fd1b": "YubiKey 5 Series",
"1c086528-58d5-f211-823c-356786e36140": "Atos CardOS FIDO2",
"20f0be98-9af9-986a-4b42-8eca4acb28e4": "Excelsecu eSecu FIDO2 Fingerprint Security Key",
"2194b428-9397-4046-8f39-007a1605a482": "IDPrime 931 Fido",
"234cd403-35a2-4cc2-8015-77ea280c77f5": "Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)",
"23786452-f02d-4344-87ed-aaf703726881": "SafeNet eToken Fusion CC",
"2772ce93-eb4b-4090-8b73-330f48477d73": "Security Key NFC by Yubico - Enterprise Edition Preview",
"2c0df832-92de-4be1-8412-88a8f074df4a": "Feitian FIDO Smart Card",

@@ -35,4 +39,6 @@ "2d3bec26-15ee-4f5d-88b2-53622490270b": "HID Crescendo Key V2",

"30b5035e-d297-4ff1-b00b-addc96ba6a98": "OneSpan DIGIPASS FX1 BIO",
"3124e301-f14e-4e38-876d-fbeeb090e7bf": "YubiKey 5 Series with Lightning Preview",
"31c3f7ff-bf15-4327-83ec-9336abcbcd34": "WinMagic FIDO Eazy - Software",
"341e4da9-3c2e-8103-5a9f-aad887135200": "Ledger Nano S FIDO2 Authenticator",
"34f5766d-1536-4a24-9033-0e294e510fb0": "YubiKey 5 Series CTAP2.1 Preview Expired ",
"34f5766d-1536-4a24-9033-0e294e510fb0": "YubiKey 5 Series with NFC Preview",
"361a3082-0278-4583-a16f-72a527f973e4": "eWBM eFA500 FIDO2 Authenticator",

@@ -47,2 +53,4 @@ "3789da91-f943-46bc-95c3-50ea2012f03a": "NEOWAVE Winkeo FIDO2",

"454e5346-4944-4ffd-6c93-8e9267193e9a": "Ensurity ThinC",
"454e5346-4944-4ffd-6c93-8e9267193e9b": "Ensurity AUTH BioPro",
"47ab2fb4-66ac-4184-9ae1-86be814012d5": "Security Key NFC by Yubico - Enterprise Edition",
"4b3f8944-d4f2-4d21-bb19-764a986ec160": "KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator",

@@ -54,2 +62,3 @@ "4c0cf95d-2f40-43b5-ba42-4c83a11c04ba": "Feitian BioPass FIDO2 Pro Authenticator",

"504d7149-4e4c-3841-4555-55445a677357": "WiSECURE AuthTron USB FIDO2 Authenticator",
"50726f74-6f6e-5061-7373-50726f746f6e": "Proton Pass",
"50a45b0c-80e7-f944-bf29-f552bfa2e048": "ACS FIDO Authenticator",

@@ -63,2 +72,3 @@ "516d3969-5a57-5651-5958-4e7a49434167": "SmartDisplayer BobeePass FIDO2 Authenticator",

"58b44d0b-0a7c-f33a-fd48-f7153c871352": "Ledger Nano S Plus FIDO2 Authenticator",
"5b0e46ba-db02-44ac-b979-ca9b84f5e335": "YubiKey 5 FIPS Series with Lightning Preview",
"5ca1ab1e-1337-fa57-f1d0-a117e71ca702": "Allthenticator App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers",

@@ -70,2 +80,3 @@ "5d629218-d3a5-11ed-afa1-0242ac120002": "Swissbit iShield Key Pro",

"61250591-b2bc-4456-b719-0b17be90bb30": "eWBM eFPA FIDO2 Authenticator",
"62e54e98-c209-4df3-b692-de71bb6a8528": "YubiKey 5 FIPS Series with NFC Preview",
"664d9f67-84a2-412a-9ff7-b4f7d8ee6d05": "OpenSK authenticator",

@@ -76,7 +87,11 @@ "66a0ccb3-bd6a-191f-ee06-e375c50b9846": "Thales Bio iOS SDK",

"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73": "Security Key by Yubico with NFC",
"6dae43be-af9c-417b-8b9f-1b611168ec60": "Dapple Authenticator from Dapple Security Inc.",
"73402251-f2a8-4f03-873e-3cb6db604b03": "uTrust FIDO2 Security Key",
"73bb0cd4-e502-49b8-9c6f-b59445bf720b": "YubiKey 5 FIPS Series",
"74820b05-a6c9-40f9-8fb0-9f86aca93998": "SafeNet eToken Fusion",
"760eda36-00aa-4d29-855b-4012a182cdeb": "Security Key NFC by Yubico Preview",
"77010bd7-212a-4fc9-b236-d2ca5e9d4084": "Feitian BioPass FIDO2 Authenticator",
"771b48fd-d3d4-4f74-9232-fc157ab0507a": "Edge on Mac",
"7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3": "YubiKey Bio Series - Multi-protocol Edition",
"7d2afadd-bf6b-44a2-a66b-e831fceb8eff": "Taglio CTAP2.1 EP",
"7e3f3d30-3557-4442-bdae-139312178b39": "RSA DS100",

@@ -91,2 +106,3 @@ "820d89ed-d65a-409e-85cb-f73f0578f82a": "IDmelon iOS Authenticator",

"88bbd2f0-342a-42e7-9729-dd158be5407a": "Precision InnaIT Key FIDO 2 Level 2 certified",
"891494da-2c90-4d31-a9cd-4eab0aed1309": "Sésame",
"8976631b-d4a0-427f-5773-0ec71c9e0279": "Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator",

@@ -100,2 +116,3 @@ "89b19028-256b-4025-8872-255358d950e4": "Sentry Enterprises CTAP2 Authenticator",

"95e4d58c-056e-4a65-866d-f5a69659e880": "TruU Windows Authenticator",
"970c8d9c-19d2-46af-aa32-3f448db49e35": "WinMagic FIDO Eazy - TPM",
"973446ca-e21c-9a9b-99f5-9b985a67af0f": "ACS FIDO Authenticator Card",

@@ -110,3 +127,5 @@ "9876631b-d4a0-427f-5773-0ec71c9e0279": "Somu Secp256R1 FIDO2 CTAP2 Authenticator",

"9f77e279-a6e2-4d58-b700-31e5943c6a98": "Hyper FIDO Pro",
"a02167b9-ae71-4ac7-9a07-06432ebb6f1c": "YubiKey 5 Series with Lightning",
"a1f52be5-dfab-4364-b51c-2bd496b14a56": "OCTATCO EzFinger2 FIDO2 AUTHENTICATOR",
"a25342c0-3cdc-4414-8e46-f4807fca511c": "YubiKey 5 Series with NFC",
"a3975549-b191-fd67-b8fb-017e2917fdb3": "Excelsecu eSecu FIDO2 NFC Security Key",

@@ -138,2 +157,3 @@ "a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa": "Security Key NFC by Yubico",

"cb69481e-8ff7-4039-93ec-0a2729a154a8": "YubiKey 5 Series",
"cc45f64e-52a2-451b-831a-4edd8022a202": "ToothPic Passkey Provider",
"cd69adb5-3c7a-deb9-3177-6800ea6cb72a": "Thales PIN Android SDK",

@@ -155,2 +175,3 @@ "cdbdaea2-c415-5073-50f7-c04e968640b6": "Excelsecu eSecu FIDO2 Security Key",

"e416201b-afeb-41ca-a03d-2281c28322aa": "ATKey.Pro CTAP2.1",
"e77e3c64-05e3-428b-8824-0cbeb04b829d": "Security Key NFC by Yubico",
"e86addcd-7711-47e5-b42a-c18257b0bf61": "IDCore 3121 Fido",

@@ -166,2 +187,3 @@ "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": "Google Password Manager",

"f4c63eff-d26c-4248-801c-3736c7eaa93a": "FIDO KeyPass S3",
"f56f58b3-d711-4afc-ba7d-6ac05f88cb19": "WinMagic FIDO Eazy - Phone",
"f7c558a0-f465-11e8-b568-0800200c9a66": "KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator",

@@ -173,3 +195,4 @@ "f8a011f3-8c0a-4d15-8006-17111f9edc7d": "Security Key by Yubico",

"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe": "Ledger Nano X FIDO2 Authenticator",
"fdb141b2-5d84-443e-8a35-4698c205a502": "KeePassXC",
"fec067a1-f1d0-4c5e-b4c0-cc3237475461": "KX701 SmartToken FIDO",
};

4

dist/esm/authenticators.d.ts

@@ -1,3 +0,1 @@

export declare function parseAuthBuffer(authData: ArrayBuffer): any;
export declare function extractAaguid(authData: ArrayBuffer): string;
/**

@@ -8,3 +6,3 @@ * Kept for compatibility purposes.

export declare function resolveAuthenticatorName(aaguid: string): string;
/**
/**O
* Updates the built-in metadata according to raw data available at https://mds.fidoalliance.org/

@@ -11,0 +9,0 @@ * This service delivers a list of AAGUIDs encoded as a JWT.

@@ -1,46 +0,2 @@

import { authenticatorMetadata } from './authenticatorMetadata.js';
import * as utils from './utils.js';
export function parseAuthBuffer(authData) {
//console.debug(authData)
let flags = new DataView(authData.slice(32, 33)).getUint8(0);
//console.debug(flags)
// https://w3c.github.io/webauthn/#sctn-authenticator-data
let parsed = {
rpIdHash: utils.toBase64url(authData.slice(0, 32)),
flags: {
userPresent: !!(flags & 1),
//reserved1: !!(flags & 2),
userVerified: !!(flags & 4),
backupEligibility: !!(flags & 8),
backupState: !!(flags & 16),
//reserved2: !!(flags & 32),
attestedData: !!(flags & 64),
extensionsIncluded: !!(flags & 128)
},
counter: new DataView(authData.slice(33, 37)).getUint32(0, false), // Big-Endian!
};
// this is more descriptive than "backupState"
parsed.synced = parsed.flags.backupState;
if (authData.byteLength > 37) {
// registration contains additional data
const aaguid = extractAaguid(authData); // bytes 37->53
// https://w3c.github.io/webauthn/#attested-credential-data
parsed = {
...parsed,
aaguid,
name: authenticatorMetadata[aaguid] ?? 'Unknown',
icon_light: 'https://webauthn.passwordless.id/authenticators/' + aaguid + '-light.png',
icon_dark: 'https://webauthn.passwordless.id/authenticators/' + aaguid + '-dark.png',
};
}
return parsed;
}
export function extractAaguid(authData) {
return formatAaguid(authData.slice(37, 53)); // 16 bytes
}
function formatAaguid(buffer) {
let aaguid = utils.bufferToHex(buffer);
aaguid = aaguid.substring(0, 8) + '-' + aaguid.substring(8, 12) + '-' + aaguid.substring(12, 16) + '-' + aaguid.substring(16, 20) + '-' + aaguid.substring(20, 32);
return aaguid; // example: "d41f5a69-b817-4144-a13c-9ebd6d9254d6"
}
import { authenticatorMetadata } from './authenticatorMetadata';
/**

@@ -55,3 +11,3 @@ * Kept for compatibility purposes.

let updatedAuthenticatorMetadata = null;
/**
/**O
* Updates the built-in metadata according to raw data available at https://mds.fidoalliance.org/

@@ -58,0 +14,0 @@ * This service delivers a list of AAGUIDs encoded as a JWT.

@@ -1,2 +0,2 @@

import { AuthenticateOptions, AuthenticationEncoded, RegisterOptions, RegistrationEncoded } from './types.js';
import { AuthenticateOptions, AuthenticationJSON, RegisterOptions, RegistrationJSON } from './types.js';
/**

@@ -13,19 +13,13 @@ * Returns whether passwordless authentication is available on this browser/platform or not.

*
* @param {string} username
* @param {string} challenge A server-side randomly generated string.
* @param {Object} [options] Optional parameters.
* @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {'auto'|'local'|'roaming'|'both'} [options.authenticatorType='auto'] Which device to use as authenticator.
* 'auto': if the local device can be used as authenticator it will be preferred. Otherwise it will prompt for a roaming device.
* 'local': use the local device (using TouchID, FaceID, Windows Hello or PIN)
* 'roaming': use a roaming device (security key or connected phone)
* 'both': prompt the user to choose between local or roaming device. The UI and user interaction in this case is platform specific.
* @param {boolean} [options.attestation=false] If enabled, the device attestation and clientData will be provided as Base64url encoded binary data.
* Note that this is not available on some platforms.
* @param {'discouraged'|'preferred'|'required'} [options.discoverable] A "discoverable" credential can be selected using `authenticate(...)` without providing credential IDs.
* @param {string|Object} [user] Username or user object (id, name, displayName)
* @param {string} [challenge] A server-side randomly generated string.
* @param {number} [timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {PublicKeyCredentialHints[]} [hints]: Can contain a list of "client-device", "hybrid" or "security-key"
* @param {boolean} [attestation=false] If enabled, the device attestation and clientData will be provided as Base64url encoded binary data. Note that this is not available on some platforms.
* @param {'discouraged'|'preferred'|'required'} [discoverable] A "discoverable" credential can be selected using `authenticate(...)` without providing credential IDs.
* Instead, a native pop-up will appear for user selection.
* This may have an impact on the "passkeys" user experience and syncing behavior of the key.
*/
export declare function register(username: string, challenge: string, options?: RegisterOptions): Promise<RegistrationEncoded>;
export declare function register(options: RegisterOptions): Promise<RegistrationJSON>;
/**

@@ -36,7 +30,6 @@ * Signs a challenge using one of the provided credentials IDs in order to authenticate the user.

* @param {string} challenge A server-side randomly generated string, the base64 encoded version will be signed.
* @param {Object} [options] Optional parameters.
* @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {'optional'|'conditional'|'required'|'silent'} [options.mediation='optional'] https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#mediation
* @param {number} [timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {boolean} [conditional] Does not return directly, but only when the user has selected a credential in the input field with `autocomplete="username webauthn"`
*/
export declare function authenticate(credentialIds: string[], challenge: string, options?: AuthenticateOptions): Promise<AuthenticationEncoded>;
export declare function authenticate(options: AuthenticateOptions): Promise<AuthenticationJSON>;

@@ -1,2 +0,2 @@

import * as utils from './utils.js';
import * as utils from './utils';
/**

@@ -14,55 +14,46 @@ * Returns whether passwordless authentication is available on this browser/platform or not.

}
async function getAuthAttachment(authType) {
if (authType === "local")
return "platform";
if (authType === "roaming" || authType === "extern")
return "cross-platform";
if (authType === "both")
/**
* Before "hints" were a thing, the "authenticatorAttachment" was the way to go.
*/
function getAuthAttachment(hints) {
if (!hints || hints.length === 0)
return undefined; // The webauthn protocol considers `null` as invalid but `undefined` as "both"!
// the default case: "auto", depending on device capabilities
try {
if (await isLocalAuthenticator())
if (hints.includes('client-device')) {
if (hints.includes('security-key') || hints.includes('hybrid'))
return undefined; // both
else
return "platform";
else
return "cross-platform";
}
catch (e) {
// might happen due to some security policies
// see https://w3c.github.io/webauthn/#sctn-isUserVerifyingPlatformAuthenticatorAvailable
return undefined; // The webauthn protocol considers `null` as invalid but `undefined` as "both"!
}
return "cross-platform";
}
function getAlgoName(num) {
switch (num) {
case -7: return "ES256";
// case -8 ignored to to its rarity
case -257: return "RS256";
default: throw new Error(`Unknown algorithm code: ${num}`);
}
}
/**
* For conditional UI, the ongoing "authentication" must be aborted when triggering a registration.
* It should also be aborted when triggering authentication another time.
*/
let ongoingAuth = null;
/**
* Creates a cryptographic key pair, in order to register the public key for later passwordless authentication.
*
* @param {string} username
* @param {string} challenge A server-side randomly generated string.
* @param {Object} [options] Optional parameters.
* @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {'auto'|'local'|'roaming'|'both'} [options.authenticatorType='auto'] Which device to use as authenticator.
* 'auto': if the local device can be used as authenticator it will be preferred. Otherwise it will prompt for a roaming device.
* 'local': use the local device (using TouchID, FaceID, Windows Hello or PIN)
* 'roaming': use a roaming device (security key or connected phone)
* 'both': prompt the user to choose between local or roaming device. The UI and user interaction in this case is platform specific.
* @param {boolean} [options.attestation=false] If enabled, the device attestation and clientData will be provided as Base64url encoded binary data.
* Note that this is not available on some platforms.
* @param {'discouraged'|'preferred'|'required'} [options.discoverable] A "discoverable" credential can be selected using `authenticate(...)` without providing credential IDs.
* @param {string|Object} [user] Username or user object (id, name, displayName)
* @param {string} [challenge] A server-side randomly generated string.
* @param {number} [timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {PublicKeyCredentialHints[]} [hints]: Can contain a list of "client-device", "hybrid" or "security-key"
* @param {boolean} [attestation=false] If enabled, the device attestation and clientData will be provided as Base64url encoded binary data. Note that this is not available on some platforms.
* @param {'discouraged'|'preferred'|'required'} [discoverable] A "discoverable" credential can be selected using `authenticate(...)` without providing credential IDs.
* Instead, a native pop-up will appear for user selection.
* This may have an impact on the "passkeys" user experience and syncing behavior of the key.
*/
export async function register(username, challenge, options) {
options = options ?? {};
if (!utils.isBase64url(challenge))
export async function register(options) {
if (!options.challenge)
throw new Error('"challenge" required');
if (!options.user)
throw new Error('"user" required');
if (!utils.isBase64url(options.challenge))
throw new Error('Provided challenge is not properly encoded in Base64url');
const user = typeof (options.user) === 'string' ? { name: options.user } : options.user;
if (!user.id)
user.id = crypto.randomUUID();
const creationOptions = {
challenge: utils.parseBase64url(challenge),
challenge: utils.parseBase64url(options.challenge),
rp: {

@@ -73,15 +64,16 @@ id: options.domain ?? window.location.hostname,

user: {
id: options.userHandle ? utils.toBuffer(options.userHandle) : await utils.sha256(new TextEncoder().encode('passwordless.id-user:' + username)),
name: username,
displayName: username,
id: utils.toBuffer(user.id),
name: user.name,
displayName: user.displayName ?? user.name,
},
hints: options.hints,
pubKeyCredParams: [
{ alg: -7, type: "public-key" },
{ alg: -257, type: "public-key" }, // RS256 (for Windows Hello and others)
{ alg: -7, type: "public-key" }, // ES256 (Webauthn's default algorithm)
{ alg: -257, type: "public-key" }, // RS256 (for older Windows Hello and others)
],
timeout: options.timeout ?? 60000,
timeout: options.timeout,
authenticatorSelection: {
userVerification: options.userVerification ?? "required",
authenticatorAttachment: await getAuthAttachment(options.authenticatorType ?? "auto"),
residentKey: options.discoverable ?? 'preferred',
userVerification: options.userVerification,
authenticatorAttachment: getAuthAttachment(options.hints),
residentKey: options.discoverable,
requireResidentKey: (options.discoverable === 'required') // mainly for backwards compatibility, see https://www.w3.org/TR/webauthn/#dictionary-authenticatorSelection

@@ -91,45 +83,37 @@ },

};
if (options.debug)
console.debug(creationOptions);
const credential = await navigator.credentials.create({ publicKey: creationOptions, signal: options.signal }); //PublicKeyCredential
if (options.debug)
console.debug(credential);
const response = credential.response; // AuthenticatorAttestationResponse
let registration = {
username: username,
credential: {
id: credential.id,
publicKey: utils.toBase64url(response.getPublicKey()),
algorithm: getAlgoName(credential.response.getPublicKeyAlgorithm())
console.debug(creationOptions);
if (ongoingAuth != null)
ongoingAuth.abort('Cancel ongoing authentication');
ongoingAuth = new AbortController();
const raw = await navigator.credentials.create({
publicKey: creationOptions,
signal: ongoingAuth?.signal
});
const response = raw.response;
ongoingAuth = null;
console.debug(raw);
if (raw.type != "public-key")
throw "Unexpected credential type!";
const publicKey = response.getPublicKey();
if (!publicKey)
throw "Non-compliant browser or authenticator!";
// This should provide the same as `response.toJson()` which is sadly only available on FireFox
const json = {
type: raw.type,
id: raw.id,
rawId: utils.toBase64url(raw.rawId), // Same as ID, but useful in tests
authenticatorAttachment: raw.authenticatorAttachment,
clientExtensionResults: raw.getClientExtensionResults(),
response: {
attestationObject: utils.toBase64url(response.attestationObject),
authenticatorData: utils.toBase64url(response.getAuthenticatorData()),
clientDataJSON: utils.toBase64url(response.clientDataJSON),
publicKey: utils.toBase64url(publicKey),
publicKeyAlgorithm: response.getPublicKeyAlgorithm(),
transports: response.getTransports(),
},
authenticatorData: utils.toBase64url(response.getAuthenticatorData()),
clientData: utils.toBase64url(response.clientDataJSON),
user, // That's our own addition
};
if (options.attestation) {
registration.attestationData = utils.toBase64url(response.attestationObject);
}
return registration;
return json;
}
async function getTransports(authType) {
const local = ['internal'];
// 'hybrid' was added mid-2022 in the specs and currently not yet available in the official dom types
// @ts-ignore
const roaming = ['hybrid', 'usb', 'ble', 'nfc'];
if (authType === "local")
return local;
if (authType == "roaming" || authType === "extern")
return roaming;
if (authType === "both")
return [...local, ...roaming];
// the default case: "auto", depending on device capabilities
try {
if (await isLocalAuthenticator())
return local;
else
return roaming;
}
catch (e) {
return [...local, ...roaming];
}
}
/**

@@ -140,43 +124,52 @@ * Signs a challenge using one of the provided credentials IDs in order to authenticate the user.

* @param {string} challenge A server-side randomly generated string, the base64 encoded version will be signed.
* @param {Object} [options] Optional parameters.
* @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {'optional'|'conditional'|'required'|'silent'} [options.mediation='optional'] https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#mediation
* @param {number} [timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {boolean} [conditional] Does not return directly, but only when the user has selected a credential in the input field with `autocomplete="username webauthn"`
*/
export async function authenticate(credentialIds, challenge, options) {
options = options ?? {};
if (!utils.isBase64url(challenge))
export async function authenticate(options) {
if (!utils.isBase64url(options.challenge))
throw new Error('Provided challenge is not properly encoded in Base64url');
const transports = await getTransports(options.authenticatorType ?? "auto");
let authOptions = {
challenge: utils.parseBase64url(challenge),
challenge: utils.parseBase64url(options.challenge),
rpId: options.domain ?? window.location.hostname,
allowCredentials: credentialIds.map(id => {
allowCredentials: options.allowCredentials?.map(cred => {
return {
id: utils.parseBase64url(id),
id: utils.parseBase64url(cred.id),
type: 'public-key',
transports: transports,
transports: cred.transports,
};
}),
userVerification: options.userVerification ?? "required",
timeout: options.timeout ?? 60000,
hints: options.hints,
userVerification: options.userVerification,
timeout: options.timeout,
};
if (options.debug)
console.debug(authOptions);
let auth = await navigator.credentials.get({
console.debug(authOptions);
if (ongoingAuth != null)
ongoingAuth.abort('Cancel ongoing authentication');
ongoingAuth = new AbortController();
const raw = await navigator.credentials.get({
publicKey: authOptions,
mediation: options.mediation,
signal: options.signal,
mediation: options.conditional ? 'conditional' : undefined,
signal: ongoingAuth?.signal
});
if (options.debug)
console.debug(auth);
const response = auth.response;
const authentication = {
credentialId: auth.id,
authenticatorData: utils.toBase64url(response.authenticatorData),
clientData: utils.toBase64url(response.clientDataJSON),
signature: utils.toBase64url(response.signature),
userHandle: response.userHandle ? utils.toBase64url(response.userHandle) : undefined // may not be returned by every authenticator
if (raw.type != "public-key")
throw "Unexpected credential type!";
ongoingAuth = null;
console.debug(raw);
const response = raw.response;
// This should provide the same as `response.toJson()` which is sadly only available on FireFox
const json = {
clientExtensionResults: raw.getClientExtensionResults(),
id: raw.id,
rawId: utils.toBase64url(raw.rawId),
type: raw.type,
authenticatorAttachment: raw.authenticatorAttachment,
response: {
authenticatorData: utils.toBase64url(response.authenticatorData),
clientDataJSON: utils.toBase64url(response.clientDataJSON),
signature: utils.toBase64url(response.signature),
userHandle: response.userHandle ? utils.toBase64url(response.userHandle) : undefined
}
};
return authentication;
return json;
}

@@ -1,8 +0,8 @@

import * as client from './client.js';
import * as server from './server.js';
import * as parsers from './parsers.js';
import * as utils from './utils.js';
import { authenticatorMetadata } from './authenticatorMetadata.js';
import * as client from './client';
import * as server from './server';
import * as parsers from './parsers';
import * as utils from './utils';
import { authenticatorMetadata } from './authenticatorMetadata';
export { client, server, parsers, utils, authenticatorMetadata };
declare const _default: {
declare const webauthn: {
client: typeof client;

@@ -14,2 +14,2 @@ server: typeof server;

};
export default _default;
export default webauthn;

@@ -1,13 +0,8 @@

/*
export * from './types'
export * from './webauthn'
export * from './parsers'
export * from './validation'
*/
import * as client from './client.js';
import * as server from './server.js';
import * as parsers from './parsers.js';
import * as utils from './utils.js';
import { authenticatorMetadata } from './authenticatorMetadata.js';
import * as client from './client';
import * as server from './server';
import * as parsers from './parsers';
import * as utils from './utils';
import { authenticatorMetadata } from './authenticatorMetadata';
export { client, server, parsers, utils, authenticatorMetadata };
export default { client, server, parsers, utils, authenticatorMetadata };
const webauthn = { client, server, parsers, utils, authenticatorMetadata };
export default webauthn;

@@ -1,6 +0,8 @@

import { AuthenticatorInfo, ClientInfo, RegistrationEncoded, RegistrationParsed, AuthenticationEncoded, AuthenticationParsed } from './types';
export declare function parseClient(data: string | ArrayBuffer): ClientInfo;
export declare function parseAuthenticator(data: string | ArrayBuffer): AuthenticatorInfo;
export declare function parseAttestation(data: string | ArrayBuffer): unknown;
export declare function parseRegistration(registration: RegistrationEncoded): RegistrationParsed;
export declare function parseAuthentication(authentication: AuthenticationEncoded): AuthenticationParsed;
import { Base64URLString, CollectedClientData, NamedAlgo, AuthenticatorParsed, RegistrationJSON, RegistrationInfo, AuthenticationInfo, AuthenticationJSON } from './types';
export declare function parseClient(data: Base64URLString | ArrayBuffer): CollectedClientData;
export declare function parseAuthenticator(authData: Base64URLString | ArrayBuffer): AuthenticatorParsed;
export declare function getAlgoName(num: COSEAlgorithmIdentifier): NamedAlgo;
export declare function parseRegistration(registrationJson: RegistrationJSON): RegistrationInfo;
export declare function toRegistrationInfo(registrationJson: RegistrationJSON, authenticator: AuthenticatorParsed): RegistrationInfo;
export declare function toAuthenticationInfo(authenticationJson: AuthenticationJSON, authenticator: AuthenticatorParsed): AuthenticationInfo;
export declare function parseAuthentication(authenticationJson: AuthenticationJSON): AuthenticationInfo;

@@ -1,3 +0,3 @@

import * as authenticators from './authenticators.js';
import * as utils from './utils.js';
import * as utils from './utils';
import { authenticatorMetadata } from './authenticatorMetadata';
const utf8Decoder = new TextDecoder('utf-8');

@@ -9,30 +9,83 @@ export function parseClient(data) {

}
export function parseAuthenticator(data) {
if (typeof data == 'string')
data = utils.parseBase64url(data);
return authenticators.parseAuthBuffer(data);
export function parseAuthenticator(authData) {
if (typeof authData == 'string')
authData = utils.parseBase64url(authData);
//console.debug(authData)
let flags = new DataView(authData.slice(32, 33)).getUint8(0);
//console.debug(flags)
// https://w3c.github.io/webauthn/#sctn-authenticator-data
return {
rpIdHash: extractRpIdHash(authData),
flags: {
userPresent: !!(flags & 1),
//reserved1: !!(flags & 2),
userVerified: !!(flags & 4),
backupEligibility: !!(flags & 8),
backupState: !!(flags & 16),
//reserved2: !!(flags & 32),
attestedData: !!(flags & 64),
extensionsIncluded: !!(flags & 128)
},
signCount: new DataView(authData.slice(33, 37)).getUint32(0, false), // Big-Endian!
aaguid: extractAaguid(authData),
//credentialId: extractCredentialId()
};
}
export function parseAttestation(data) {
//if(typeof data == 'string')
// data = utils.parseBase64url(data)
// Useless comment, let's at least provide the raw value
// return "The device attestation proves the authenticity of the device model / aaguid. It's not guaranteed to be included and really complex to parse / verify. Good luck with that one!"
return data;
function extractRpIdHash(authData) {
return utils.toBase64url(authData.slice(0, 32));
}
export function parseRegistration(registration) {
/**
* Returns the AAGUID in the format "00000000-0000-0000-0000-000000000000"
*/
function extractAaguid(authData) {
if (authData.byteLength < 53)
return "00000000-0000-0000-0000-000000000000";
const buffer = authData.slice(37, 53); // 16 byte
const hex = utils.bufferToHex(buffer);
const aaguid = `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}`;
return aaguid; // example: "d41f5a69-b817-4144-a13c-9ebd6d9254d6"
}
export function getAlgoName(num) {
switch (num) {
case -7: return "ES256";
case -8: return "EdDSA";
case -257: return "RS256";
default: throw new Error(`Unknown algorithm code: ${num}`);
}
}
export function parseRegistration(registrationJson) {
const authenticator = parseAuthenticator(registrationJson.response.authenticatorData);
return toRegistrationInfo(registrationJson, authenticator);
}
export function toRegistrationInfo(registrationJson, authenticator) {
const aaguid = authenticator.aaguid;
return {
username: registration.username,
credential: registration.credential,
client: parseClient(registration.clientData),
authenticator: parseAuthenticator(registration.authenticatorData),
attestation: registration.attestationData ? parseAttestation(registration.attestationData) : null
authenticator: {
aaguid,
counter: authenticator.signCount,
icon_light: 'https://webauthn.passwordless.id/authenticators/' + aaguid + '-light.png',
icon_dark: 'https://webauthn.passwordless.id/authenticators/' + aaguid + '-dark.png',
name: authenticatorMetadata[aaguid] ?? 'Unknown',
},
credential: {
id: registrationJson.id,
publicKey: registrationJson.response.publicKey,
algorithm: getAlgoName(registrationJson.response.publicKeyAlgorithm),
},
synced: authenticator.flags.backupEligibility,
user: registrationJson.user, // That's specific to this library
userVerified: authenticator.flags.userVerified,
};
}
export function parseAuthentication(authentication) {
export function toAuthenticationInfo(authenticationJson, authenticator) {
return {
credentialId: authentication.credentialId,
client: parseClient(authentication.clientData),
authenticator: parseAuthenticator(authentication.authenticatorData),
signature: authentication.signature
credentialId: authenticationJson.id,
userId: authenticationJson.response.userHandle,
coutner: authenticator.signCount,
userVerified: authenticator.flags.userVerified
};
}
export function parseAuthentication(authenticationJson) {
const authenticator = parseAuthenticator(authenticationJson.response.authenticatorData);
return toAuthenticationInfo(authenticationJson, authenticator);
}

@@ -1,2 +0,2 @@

import { AuthenticationEncoded, AuthenticationParsed, CredentialKey, NamedAlgo, RegistrationEncoded, RegistrationParsed } from "./types.js";
import { AuthenticationJSON, NamedAlgo, RegistrationJSON, RegistrationInfo, AuthenticationInfo, Base64URLString, CredentialInfo } from "./types";
interface RegistrationChecks {

@@ -6,3 +6,3 @@ challenge: string | Function;

}
export declare function verifyRegistration(registrationRaw: RegistrationEncoded, expected: RegistrationChecks): Promise<RegistrationParsed>;
export declare function verifyRegistration(registrationJson: RegistrationJSON, expected: RegistrationChecks): Promise<RegistrationInfo>;
interface AuthenticationChecks {

@@ -16,9 +16,9 @@ challenge: string | Function;

}
export declare function verifyAuthentication(authenticationRaw: AuthenticationEncoded, credential: CredentialKey, expected: AuthenticationChecks): Promise<AuthenticationParsed>;
export declare function verifyAuthentication(authenticationJson: AuthenticationJSON, credential: CredentialInfo, expected: AuthenticationChecks): Promise<AuthenticationInfo>;
type VerifyParams = {
algorithm: NamedAlgo;
publicKey: string;
authenticatorData: string;
clientData: string;
signature: string;
publicKey: Base64URLString;
authenticatorData: Base64URLString;
clientData: Base64URLString;
signature: Base64URLString;
verbose?: boolean;

@@ -25,0 +25,0 @@ };

@@ -1,3 +0,4 @@

import { parseAuthentication, parseRegistration } from "./parsers.js";
import * as utils from './utils.js';
import { parsers } from "./index";
import { parseAuthenticator, parseClient, toAuthenticationInfo } from "./parsers";
import * as utils from './utils';
async function isValid(validator, value) {

@@ -17,46 +18,53 @@ if (typeof validator === 'function') {

}
export async function verifyRegistration(registrationRaw, expected) {
const registration = parseRegistration(registrationRaw);
if (registration.client.type !== "webauthn.create")
throw new Error(`Unexpected ClientData type: ${registration.client.type}`);
if (await isNotValid(expected.origin, registration.client.origin))
throw new Error(`Unexpected ClientData origin: ${registration.client.origin}`);
if (await isNotValid(expected.challenge, registration.client.challenge))
throw new Error(`Unexpected ClientData challenge: ${registration.client.challenge}`);
return registration;
export async function verifyRegistration(registrationJson, expected) {
const client = parseClient(registrationJson.response.clientDataJSON);
const authenticator = parseAuthenticator(registrationJson.response.authenticatorData);
const aaguid = authenticator.aaguid;
if (!aaguid) // should never happen, worst case should be a fallback to "zeroed" aaguid
throw new Error("Unexpected errror, no AAGUID.");
if (client.type !== "webauthn.create")
throw new Error(`Unexpected ClientData type: ${client.type}`);
if (await isNotValid(expected.origin, client.origin))
throw new Error(`Unexpected ClientData origin: ${client.origin}`);
if (await isNotValid(expected.challenge, client.challenge))
throw new Error(`Unexpected ClientData challenge: ${client.challenge}`);
return parsers.toRegistrationInfo(registrationJson, authenticator);
}
export async function verifyAuthentication(authenticationRaw, credential, expected) {
if (authenticationRaw.credentialId !== credential.id)
throw new Error(`Credential ID mismatch: ${authenticationRaw.credentialId} vs ${credential.id}`);
export async function verifyAuthentication(authenticationJson, credential, expected) {
if (authenticationJson.id !== credential.id)
throw new Error(`Credential ID mismatch: ${authenticationJson.id} vs ${credential.id}`);
const isValidSignature = await verifySignature({
algorithm: credential.algorithm,
publicKey: credential.publicKey,
authenticatorData: authenticationRaw.authenticatorData,
clientData: authenticationRaw.clientData,
signature: authenticationRaw.signature,
authenticatorData: authenticationJson.response.authenticatorData,
clientData: authenticationJson.response.clientDataJSON,
signature: authenticationJson.response.signature,
verbose: expected.verbose
});
if (!isValidSignature)
throw new Error(`Invalid signature: ${authenticationRaw.signature}`);
const authentication = parseAuthentication(authenticationRaw);
if (expected.verbose)
console.debug(authentication);
if (authentication.client.type !== "webauthn.get")
throw new Error(`Unexpected clientData type: ${authentication.client.type}`);
if (await isNotValid(expected.origin, authentication.client.origin))
throw new Error(`Unexpected ClientData origin: ${authentication.client.origin}`);
if (await isNotValid(expected.challenge, authentication.client.challenge))
throw new Error(`Unexpected ClientData challenge: ${authentication.client.challenge}`);
throw new Error(`Invalid signature: ${authenticationJson.response.signature}`);
const client = parseClient(authenticationJson.response.clientDataJSON);
const authenticator = parseAuthenticator(authenticationJson.response.authenticatorData);
if (expected.verbose) {
console.debug(client);
console.debug(authenticator);
}
if (client.type !== "webauthn.get")
throw new Error(`Unexpected clientData type: ${client.type}`);
if (await isNotValid(expected.origin, client.origin))
throw new Error(`Unexpected ClientData origin: ${client.origin}`);
if (await isNotValid(expected.challenge, client.challenge))
throw new Error(`Unexpected ClientData challenge: ${client.challenge}`);
// this only works because we consider `rp.origin` and `rp.id` to be the same during authentication/registration
const rpId = expected.domain ?? new URL(authentication.client.origin).hostname;
const rpId = expected.domain ?? new URL(client.origin).hostname;
const expectedRpIdHash = utils.toBase64url(await utils.sha256(utils.toBuffer(rpId)));
if (authentication.authenticator.rpIdHash !== expectedRpIdHash)
throw new Error(`Unexpected RpIdHash: ${authentication.authenticator.rpIdHash} vs ${expectedRpIdHash}`);
if (!authentication.authenticator.flags.userPresent)
if (authenticator.rpIdHash !== expectedRpIdHash)
throw new Error(`Unexpected RpIdHash: ${authenticator.rpIdHash} vs ${expectedRpIdHash}`);
if (!authenticator.flags.userPresent)
throw new Error(`Unexpected authenticator flags: missing userPresent`);
if (!authentication.authenticator.flags.userVerified && expected.userVerified)
if (!authenticator.flags.userVerified && expected.userVerified)
throw new Error(`Unexpected authenticator flags: missing userVerified`);
if (expected.counter && authentication.authenticator.counter <= expected.counter)
throw new Error(`Unexpected authenticator counter: ${authentication.authenticator.counter} (should be > ${expected.counter})`);
return authentication;
if (expected.counter && authenticator.signCount <= expected.counter)
throw new Error(`Unexpected authenticator counter: ${authenticator.signCount} (should be > ${expected.counter})`);
return toAuthenticationInfo(authenticationJson, authenticator);
}

@@ -131,9 +139,9 @@ // https://w3c.github.io/webauthn/#sctn-public-key-easy

// Convert signature from ASN.1 sequence to "raw" format
const usignature = new Uint8Array(signatureBuffer);
const rStart = usignature[4] === 0 ? 5 : 4;
const signature = new Uint8Array(signatureBuffer);
const rStart = signature[4] === 0 ? 5 : 4;
const rEnd = rStart + 32;
const sStart = usignature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;
const r = usignature.slice(rStart, rEnd);
const s = usignature.slice(sStart);
const sStart = signature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;
const r = signature.slice(rStart, rEnd);
const s = signature.slice(sStart);
return new Uint8Array([...r, ...s]);
}

@@ -1,69 +0,124 @@

export type AuthType = 'auto' | 'local' | 'extern' | 'roaming' | 'both';
export type NumAlgo = -7 | -257;
export type NamedAlgo = 'RS256' | 'ES256';
export type NamedAlgo = 'RS256' | 'EdDSA' | 'ES256';
export type Base64URLString = string;
/**
* The available "hints" for WebAuthn, not yet available in the official DOM types
*/
export type PublicKeyCredentialHints = "client-device" | "hybrid" | "security-key";
/**
* Extends the native DOM type since the "hints" are not yet included in the official version.
*/
export interface WebAuthnCreateOptions extends PublicKeyCredentialCreationOptions {
hints?: PublicKeyCredentialHints[];
}
/**
* Extends the native DOM type since the "hints" are not yet included in the official version.
*/
export interface WebAuthnGetOptions extends PublicKeyCredentialRequestOptions {
hints?: PublicKeyCredentialHints[];
}
/*********************** OPTIONS *************************/
export interface CommonOptions {
challenge: string;
domain?: string;
hints?: PublicKeyCredentialHints[];
timeout?: number;
userVerification?: UserVerificationRequirement;
authenticatorType?: AuthType;
timeout?: number;
debug?: boolean;
signal?: AbortSignal;
}
export interface RegisterOptions extends CommonOptions {
attestation?: boolean;
discoverable?: ResidentKeyRequirement;
user: string | User;
}
export interface User {
id?: string;
name: string;
displayName?: string;
}
/**
* @see PublicKeyCredentialDescriptor
*/
export interface CredentialDescriptor {
id: Base64URLString;
transports?: AuthenticatorTransport[];
}
export interface AuthenticateOptions extends CommonOptions {
mediation?: CredentialMediationRequirement;
allowCredentials?: CredentialDescriptor[];
conditional?: boolean;
}
export interface AuthenticationEncoded {
credentialId: string;
authenticatorData: string;
clientData: string;
signature: string;
userHandle?: string;
/********************************** JSON PAYLOADS **********************/
export interface RegistrationJSON extends RegistrationResponseJSON {
user: User;
}
export interface AuthenticationParsed {
credentialId: string;
authenticator: AuthenticatorInfo;
client: ClientInfo;
signature: string;
export type AuthenticationJSON = AuthenticationResponseJSON;
/**
* https://w3c.github.io/webauthn/#dictdef-registrationresponsejson
*/
export interface RegistrationResponseJSON {
/** The credential ID */
id: Base64URLString;
/** The credential ID */
rawId: Base64URLString;
response: AuthenticatorAttestationResponseJSON;
authenticatorAttachment?: AuthenticatorAttachment;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
type: PublicKeyCredentialType;
}
export interface RegisterOptions extends CommonOptions {
userHandle?: string;
attestation?: boolean;
discoverable?: ResidentKeyRequirement;
/**
* A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* https://w3c.github.io/webauthn/#dictdef-authenticatorattestationresponsejson
*/
export interface AuthenticatorAttestationResponseJSON {
attestationObject: Base64URLString;
authenticatorData: Base64URLString;
clientDataJSON: Base64URLString;
transports: AuthenticatorTransport[];
publicKey: Base64URLString;
publicKeyAlgorithm: COSEAlgorithmIdentifier;
}
export interface CredentialKey {
id: string;
publicKey: string;
algorithm: 'RS256' | 'ES256';
/**
* A slightly-modified AuthenticationCredential to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson
*/
export interface AuthenticationResponseJSON {
id: Base64URLString;
rawId: Base64URLString;
response: AuthenticatorAssertionResponseJSON;
authenticatorAttachment?: AuthenticatorAttachment;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
type: PublicKeyCredentialType;
}
export interface RegistrationEncoded {
username: string;
credential: CredentialKey;
authenticatorData: string;
clientData: string;
attestationData?: string;
/**
* A slightly-modified AuthenticatorAssertionResponse to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* https://w3c.github.io/webauthn/#dictdef-authenticatorassertionresponsejson
*/
export interface AuthenticatorAssertionResponseJSON {
clientDataJSON: Base64URLString;
authenticatorData: Base64URLString;
signature: Base64URLString;
userHandle?: Base64URLString;
}
export interface RegistrationParsed {
username: string;
credential: {
id: string;
publicKey: string;
algorithm: 'RS256' | 'ES256';
};
authenticator: AuthenticatorInfo;
client: ClientInfo;
attestation?: any;
}
export interface ClientInfo {
type: "webauthn.create" | "webauthn.get";
challenge: string;
/**
* WebAuthn added transports that are not yet defined in the DOM definitions.
* However, it's partly obsoleted by the `hints` in the registration/authentication request.
*/
export type AuthenticatorTransport = 'ble' | 'cable' | 'hybrid' | 'internal' | 'nfc' | 'smart-card' | 'usb';
/************************** PARSED **************************/
/**
* https://w3c.github.io/webauthn/#dictionary-client-data
*/
export interface CollectedClientData {
type: string;
challenge: Base64URLString;
origin: string;
crossOrigin: boolean;
tokenBindingId?: {
id: string;
status: string;
};
extensions?: any;
topOrigin?: string;
crossOrigin?: boolean;
}
export interface AuthenticatorInfo {
rpIdHash: string;
export interface AuthenticatorParsed {
rpIdHash: Base64URLString;
flags: {

@@ -77,7 +132,39 @@ userPresent: boolean;

};
counter: number;
signCount: number;
aaguid: string;
attestation?: Base64URLString;
}
/**
* https://w3c.github.io/webauthn/#sctn-authenticator-data
*/
/************************** RESULTS *************************/
export interface RegistrationInfo {
user: UserInfo;
credential: CredentialInfo;
authenticator: AuthenticatorInfo;
synced: boolean;
userVerified: boolean;
}
export interface AuthenticationInfo {
credentialId: Base64URLString;
userId?: Base64URLString;
userVerified: boolean;
coutner: number;
}
export interface UserInfo {
id: string;
name: string;
displayName: string;
}
export interface CredentialInfo {
id: string;
publicKey: string;
algorithm: NamedAlgo;
}
export interface AuthenticatorInfo {
aaguid: string;
name: string;
icon_light: string;
icon_dark: string;
counter: number;
}
/********************************
Encoding/Decoding Utils
********************************/
export declare function randomChallenge(): string;
import { Base64URLString } from "./types";
export declare function randomChallenge(): `${string}-${string}-${string}-${string}-${string}`;
export declare function toBuffer(txt: string): ArrayBuffer;
export declare function parseBuffer(buffer: ArrayBuffer): string;
export declare function isBase64url(txt: string): boolean;
export declare function toBase64url(buffer: ArrayBuffer): string;
export declare function parseBase64url(txt: string): ArrayBuffer;
export declare function toBase64url(buffer: ArrayBuffer): Base64URLString;
export declare function parseBase64url(txt: Base64URLString): ArrayBuffer;
export declare function sha256(buffer: ArrayBuffer): Promise<ArrayBuffer>;
export declare function bufferToHex(buffer: ArrayBuffer): string;
export declare function concatenateBuffers(buffer1: ArrayBuffer, buffer2: ArrayBuffer): Uint8Array;
{
"name": "@passwordless-id/webauthn",
"version": "1.6.2",
"version": "2.0.0",
"description": "A small wrapper around the webauthn protocol to make one's life easier.",
"type": "module",
"main": "dist/esm/index.js",
"files": ["dist/"],
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"browser": "dist/webauthn.min.js",
"unpkg": "dist/webauthn.min.js",
"main": "dist/cjs/index.js",
"browser": "dist/browser/webauthn.min.js",
"scripts": {
"build": "tsc && esbuild src/index.ts --platform=neutral --bundle --sourcemap --minify --target=es2022 --outfile=dist/webauthn.min.js",
"build": "npm run build-module && npm run build-nodejs && npm run build-browser && npm run build-demos",
"build-module": "tsc",
"build-nodejs": "esbuild src/index.ts --platform=node --bundle --outdir=dist/cjs",
"build-browser": "esbuild src/index.ts --platform=browser --bundle --format=esm --minify --sourcemap --outfile=dist/browser/webauthn.min.js",
"build-demos": "cp dist/browser/*.js docs/demos/js; cp dist/browser/*.js.map docs/demos/js",
"test": "jest",
"dev": "http-server"
"dev": "mkdocs serve"
},

@@ -31,14 +39,11 @@ "repository": {

"homepage": "https://webauthn.passwordless.id",
"funding": "https://github.com/sponsors/passwordless-id",
"devDependencies": {
"@babel/preset-typescript": "^7.21.4",
"@types/jest": "^29.2.3",
"esbuild": "^0.15.8",
"@types/jest": "^29.5.12",
"esbuild": "^0.21.3",
"http-server": "^14.1.1",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"jest": "^29.7.0",
"jest-ts-webcompat-resolver": "^1.0.0",
"ts-jest": "^29.0.3",
"typescript": "^4.8.3"
"ts-jest": "^29.1.3",
"typescript": "^5.4.5"
}
}

@@ -1,41 +0,36 @@

Passwordless.ID / webauthn
==========================
@passwordless-id/webauthn
=========================
A greatly simplified and opinionated wrapper to invoke the [webauthn protocol](https://w3c.github.io/webauthn/) more conveniently.
It is an [open source](https://github.com/passwordless-id/webauthn), dependency-free and minimalistic library (17kb only, from which 11kb is the list of authenticator aaguids/names).
![NPM Version](https://img.shields.io/npm/v/%40passwordless-id%2Fwebauthn)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@passwordless-id/webauthn)](https://bundlephobia.com/package/@passwordless-id/webauthn)
![NPM Downloads](https://img.shields.io/npm/dm/%40passwordless-id%2Fwebauthn)
![GitHub Repo stars](https://img.shields.io/github/stars/passwordless-id/webauthn)
![GitHub Sponsors](https://img.shields.io/github/sponsors/passwordless-id?color=pink)
<img src="demos/img/banner-biometric-auth.svg" />
![banner](docs/img/banner-biometric-auth.svg)
> This library is used in [Passwordless.ID](https://passwordless.id), a free public identity provider based on WebAuthn.
Try out the playground to see how this library works:
> This library greatly simplifies the usage of **passkeys** by invoking the [WebAuthn protocol](https://w3c.github.io/webauthn/) more conveniently. It is [open source](https://github.com/passwordless-id/webauthn), opinionated, dependency-free and minimalistic (20kb only, from which 13kb is the list of authenticator AAGUIDs/names).
>
> This library is provided by [Passwordless.ID](https://passwordless.id), a free public identity provider.
### *[Testing Playground](https://webauthn.passwordless.id/demos/playground.html)*
Other demos with minimal examples:
👀 Demos
---------
- [Basic Demo](https://webauthn.passwordless.id/demos/basic.html)
- [Minimal Example (CDN)](https://webauthn.passwordless.id/demos/example-cdn.html)
- [Minimal Example (repository)](https://webauthn.passwordless.id/demos/example-raw.html)
- [Conditional UI](https://webauthn.passwordless.id/demos/conditional-ui.html)
- [Testing Playground](https://webauthn.passwordless.id/demos/playground.html)
- [Authenticators list](https://webauthn.passwordless.id/demos/authenticators.html)
- [Docs](https://webauthn.passwordless.id)
These demos are plain HTML/JS, not minimized. Just open the sources in your browser if you are curious.
The source of all demos is on [GitHub](https://github.com/passwordless-id/webauthn)
How does the protocol work?
---------------------------
📦 Installation
----------------
This diagram shows how the webauthn protocol works, slightly simplified.
### Modules (recommended)
![diagram](https://passwordless.id/protocols/webauthn/overview.svg)
Further documentation about the protocol can be found in the webauthn guide at [Passwordless.ID](https://passwordless.id).
Installation / Usage
--------------------
### NPM
```bash

@@ -45,547 +40,112 @@ npm install @passwordless-id/webauthn

The base package contains both client and server side modules. You can import the `client` submodule or the `server` depending on your needs.
```js
import * as webauthn from '@passwordless-id/webauthn'
import {client} from '@passwordless-id/webauthn'
import {server} from '@passwordless-id/webauthn'
```
### Browser
*Note: the brackets in the import are important!*
### Alternatives
For **browsers**, it can be imported using a CDN link in the page, or even inside the script itself.
```html
<script type="module">
import { client } from 'https://cdn.jsdelivr.net/npm/@passwordless-id/webauthn@1'
import {client} from src="https://cdn.jsdelivr.net/npm/@passwordless-id/webauthn@2.0.0/dist/webauthn.min.js"
</script>
```
### Import
The `webauthn` module is basically a "bundle" composed of the following modules:
Lastly, a **CommonJS** variant is also available for old Node stacks, to be imported using `require('@passwordless-id/webauthn')`. It's usage is discouraged though, in favor of the default ES modules.
- `client`: used for invoking webauthn in the browser
- `server`: used for verifying responses in the server
- `parsers`: used to parse part or all of the encoded data without verifications
- `utils`: various encoding, decoding, challenge generator and other utils
Note that at least NodeJS **19+** is necessary. (The reason is that previous Node versions had no `WebCrypto` being globally available, making it impossible to have a "universal build")
It was designed that way so that you can import only the module(s) you need. That way, the size of your final js bundle is reduced even further. Importing all is dependency free and < 10kb anyway.
So you might for example `import { client } from '@passwordless-id/webauthn'` for browser side stuff and `import { server } from '@passwordless-id/webauthn'` for server side stuff.
🚀 Getting started
-------------------
### Runs in...
There are multiple ways to use and invoke the WebAuthn protocol.
What follows is just an example of the most straightforward use case.
- In Chrome, Edge, Firefox, Safari
- NodeJS **19+** (*)
- Cloudflare Workers
- Probably in most recent browsers/servers
### Registration
(*) For older Node versions, take a look at [Arch0125's fork](https://github.com/Arch0125/webauthn/tree/nodev14-v16-support). (The reason of the Node 19+ compatibility is basically `WebCrypto` being globally available, making it possible to use the same build for all targets: browser, node, clouflare workers...)
Utilities
---------
```js
import { client } from '@passwordless-id/webauthn'
client.isAvailable()
```
Returns `true` or `false` depending on whether the Webauthn protocol is available on this platform/browser.
Particularly linux and "exotic" web browsers might not have supported yet.
---
```js
await client.isLocalAuthenticator()
```
This promise returns `true` or `false` depending on whether the device itself can act as authenticator. Otherwise, a "roaming" authenticator like a smartphone or usb security key can be used. This information is mainly used for information messages and user guidance.
Registration
------------
### Overview
The registration process occurs in four steps:
1. The browser requests a challenge from the server
2. The browser triggers `client.register(...)` and sends the result to the server
3. The server parses and verifies the registration payload
4. The server stores the credential key of this device for the user account
Note that unlike traditional authentication, the credential key is attached to the device. Therefore, it might make sense for a single user account to have multiple credential keys.
### 1. Requesting challenge
The challenge is basically a [nonce](https://en.wikipedia.org/wiki/nonce) to avoid replay attacks.
```
const challenge = /* request it from server */
```
Remember it on the server side during a certain amount of time and "consume" it once used.
### 2. Trigger registration in browser
Example call:
```js
import { client } from '@passwordless-id/webauthn'
const challenge = "a7c61ef9-dc23-4806-b486-2428938a547e"
const registration = await client.register("Arnaud", challenge, {
authenticatorType: "auto",
userVerification: "required",
timeout: 60000,
attestation: true,
userHandle: "Optional server-side user id. Must not reveal personal information.",
debug: false
import {client} from '@passwordless-id/webauthn'
await client.register({
challenge: 'a random string generated by the server',
user: 'John Doe'
})
```
Parameters:
By default, this registers a passkey on any authenticator (local or roaming) with `preferred` user verification. For further options, see [&rarr; Registration docs](/registration/)
- `username`: The desired username.
- `challenge`: A server-side randomly generated string.
- `options`: See [below](#common-options).
The `registration` object looks like this:
### Authentication
```json
{
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="
}
```
Then simply send this object as JSON to the server.
### 3. Verify it server side
```js
import { server } from '@passwordless-id/webauthn'
const expected = {
challenge: "a7c61ef9-dc23-4806-b486-2428938a547e", // whatever was randomly generated by the server
origin: "http://localhost:8080",
}
const registrationParsed = await server.verifyRegistration(registration, expected)
```
Either this operation fails and throws an Error, or the verification is successful and returns the parsed registration.
Example result:
```json
{
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticator": {
...
"name": "Windows Hello",
"icon_dark": "https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-dark.png",
"icon_light": "https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-light.png",
"synced": true
},
...
}
```
> **NOTE:** Currently, the *attestation* which proves the exact model type of the authenticator is *not verified*. [Do I need attestation?](https://medium.com/webauthnworks/webauthn-fido2-demystifying-attestation-and-mds-efc3b3cb3651)
### 4. Store the credential key
The credential key is the most important part and should be stored in a database for later since it will be used to verify the authentication signature.
```json
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
```
*Please note that unlike traditional systems, you might allow a user to have multiple credential keys.
For example, if you allow the user to use multiple device-bound keys and/or registering keys for multiple platforms.*
Authentication
--------------
### Overview
There are two kinds of authentications possible:
- by providing a list of allowed credential IDs
- by letting the platform offer a default UI to select the user and its credential
Both have their pros & cons (TODO: article).
The authentication procedure is similar to the procedure and divided in four steps.
1. The browser requests a challenge from the server
2. The browser triggers `client.authenticate(...)` and sends the result to the server
3. The server loads the credential key used for authentication
4. The server parses and verifies the authentication payload
### 1. Requesting challenge
The challenge is basically a [nonce](https://en.wikipedia.org/wiki/nonce) to avoid replay attacks.
```
const challenge = /* request it from server */
```
Remember it on the server side during a certain amount of time and "consume" it once used.
### 2. Trigger authentication in browser
Example call:
```js
import { client } from '@passwordless-id/webauthn'
const challenge = "56535b13-5d93-4194-a282-f234c1c24500"
const authentication = await client.authenticate(["3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU"], challenge, {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000
import {client} from '@passwordless-id/webauthn'
await client.authenticate({
challenge: 'a random string generated by the server'
})
```
Example response:
By default, this triggers the native passkey selection dialog, for any authenticator (local or roaming) and with `preferred` user verification. For further options, see [&rarr; Authentication docs](https://webauthn.passwordless.id/authentication/)
```json
{
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ==",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNTY1MzViMTMtNWQ5My00MTk0LWEyODItZjIzNGMxYzI0NTAwIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=",
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
}
```
Parameters:
### Verification
- `credentialIds`: The list of credential IDs that can be used for signing.
- `challenge`: A server-side randomly generated string, the base64url encoded version will be signed.
- `options`: See [below](#common-options).
### 3. In the server, load the credential key
```js
import { server } from '@passwordless-id/webauthn'
const credentialKey = { // obtained from database by looking up `authentication.credentialId`
id: "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
algorithm: "ES256"
} as const
const expected = {
challenge: "56535b13-5d93-4194-a282-f234c1c24500", // whatever was randomly generated by the server.
origin: "http://localhost:8080",
userVerified: true, // should be set if `userVerification` was set to `required` in the authentication options (default)
counter: 123 // Optional. For device-bound credentials, you should verify the authenticator "usage" counter increased since last time.
}
```
Regarding the `counter`, it might or might not be implemented by the authenticator.
Typically, it's implemented by hardware-bound keys to detect and avoid the risk of cloning the authenticator and starts with 1 during registration.
On the opposite, for password managers syncing keys in the cloud, the counter is typically always 0 since in that case cloning is a "feature".
For example, device-bound keys on Android and Windows do have an increasing `counter`, USB security keys also, while MacOS/iOS do not.
Lastly, please note that the specs do not mandate "+1" increases, it could theoretically increase by any amount.
Often, it might also be more practical to use functions to verify challenge or origin. This is possible too:
```js
const expected = {
challenge: async (challenge) => { /* async call to DB for example */ return true },
origin: (origin) => listOfAllowedOrigins.includes(origin),
userVerified: true, // no function allowed here
counter: 123, // optional, no function allowed here
verbose: true, // optional, enables debug logs containing sensitive information
}
import {server} from '@passwordless-id/webauthn'
await server.verifyRegistration(registration, expected)
await server.verifyAuthentication(registration, expected)
```
### 4. Verify the authentication
[&rarr; Verification docs](https://webauthn.passwordless.id/verification/)
```js
const authenticationParsed = await server.verifyAuthentication(authentication, credentialKey, expected)
```
<!--
Either this operation fails and throws an Error, or the verification is successful and returns the parsed authentication payload.
🛠️ A tool vs a solution
------------------------
Please note that this parsed result `authenticationParsed` has no real use. It is solely returned for the sake of completeness. The `verifyAuthentication` already verifies the payload, including the signature.
This library is a tool to implement passkeys for your website. Whether it is the main mechanism or to improve an existing authentication system, it is flexible enough to do both. However, you may also need to...
- Register multiple authenticators per account
- Verify e-mail address upon registration
- Have account recovery mechanisms
- Detect suspicious activity
- Upload a user portrait
- Manage the user profile
- ...and so on
Remarks
-------
Basically, this library is just a tool to realize something bigger. If you just want to "register" and "authenticate" users without dealing with the intricacies, a "solution" like [Passwordless.ID](https://passwordless.id) would be more suited. It's free and (soon) open source too, so there is no need for you to re-invent the wheel.
### The `challenge` is critical
-->
The challenge must be a random value.
Otherwise, your implementation might become vulnerable to replay attacks.
📃 Changelog
-------------
> The "Version 2" is a complete overhaul of the first version.
> While it still strives for simplicity and ease of use, it also differs from the previous mainly regarding its default behavior.
>
> Previously, this lib defaulted to using the platform as authenticator if possible.
> The user experience was improved that way, going straight to user verification instead of intermediate popup(s) to select the authenticator.
>
> Now, letting the user select the authenticator is the default.
> Why this change of mind? Because many platform authenticators now sync credentials in the cloud, with the built-in password manager.
> While this is certainly convenient, the security and privacy guarantees using synced credentials are not as strong as when using security keys with hardware-bound credentials.
> That is why security keys now deserve some love.
>
> Same goes for user verification, it is now `preferred`, like the native WebAuthn protocol.
> While this reduces security, it supports a wider range of security keys.
>
> Lastly, the response format has been changed completely to be compatible with the output as the `PublicKeyCredential.toJson()` method. An official part of the spec that only FireFox implements. Using the same intermediate format increases compatibility cross-libraries in the long term.
### There can be multiple credentials per user ID
- Use platform authenticator by default => authenticator selection pops up by default
- `authenticatorType` was removed => use `hints` instead
- User verification default: `required` => `preferred`
- Timeout: 1 minute => no timeout
- Response format changed
- Transports as part of `allowCredentials`
Unlike traditional authentication, you might have multiple credential keys per user.
### Authentication does *not* provide `username` out of the box
Only `credentialId` is provided during the authentication.
So either you maintain a mapping `credentialId -> username` in your database, or you add the `username` in your frontend to backend communication.
### Passkeys a.k.a "discoverable" credentials
If the credential is [discoverable](https://w3c.github.io/webauthn/#client-side-discoverable-public-key-credential-source), the credential id and user information is kept on the system. Although you can "discourage" it, there is no option in the spec to "forbid" it. For example, iOS always use "discoverable" keys, even if the option is set to `"discouraged"`
Afterwards, you can use `client.authenticate([], ...)` without specifying credential IDs. In that case, the platform will pop-up a default dialog to let you pick a user and perform authentication. Of course, the look and feel is platform specific.
### Disable synced credentials
That is sadly impossible, the spec authors refuse to add an option to disable syncing.
See:
- https://github.com/w3c/webauthn/issues/1714
- https://github.com/w3c/webauthn/issues/1739
Note that in practice, and although not precised by the specs, the "discoverable" property has an impact on syncing in the cloud. As of end 2023, for Microsoft and Android, when marked as `discoverable: "discouraged"`, they are not synced in the cloud ...but still discoverable. Since this is not covered by the specs, this might change in the future.
### This library simplifies a few things by using sensible defaults
Unlike the [WebAuthn protocol](https://w3c.github.io/webauthn/), some defaults are different:
- The `timeout` is one minute by default.
- If the device can act as authenticator itself, it is preferred instead of asking which authenticator type to use.
- The `userVerification` is required by default.
- The protocol "Relying Party ID" is always set to be the origin domain
- The `username` is used for both the protocol level user "name" and "displayName"
Common options
--------------
The following options are available for both `register` and `authenticate`.
- `timeout`: Number of milliseconds the user has to respond to the biometric/PIN check. *(Default: 60000)*
- `userVerification`: Whether to prompt for biometric/PIN check or not. *(Default: "required")*
- `authenticatorType`: Which device to use as authenticator. Possible values:
- `'auto'`: if the local device can be used as authenticator it will be preferred. Otherwise it will prompt for a roaming device. *(Default)*
- `'local'`: use the local device (using TouchID, FaceID, Windows Hello or PIN)
- `'roaming'`: use a roaming device (security key or connected phone)
- `'both'`: prompt the user to choose between local or roaming device. The UI and user interaction in this case is platform specific.
- `domain`: by default, the current domain name is used. Also known as "relying party id". You may want to customize it for ...
- a parent domain to let the credential work on all subdomains
- browser extensions requiring specific IDs instead of domains ?
- specific iframes use cases?
- `debug`: If enabled, parses the "data" objects and provide it in a "debug" properties.
Registration options
--------------------
- `discoverable`: (`'discouraged'`, `'preferred'` or `'required'`) If the credential is "discoverable", it can be selected using `authenticate` without providing credential IDs. In that case, a native pop-up will appear for user selection. This may have an impact on the "passkeys" user experience and syncing behavior of the key. *(Default: 'preferred')*
- `attestation`: If enabled, the device attestation and clientData will be provided as base64 encoded binary data. Note that this may impact the authenticator information available or the UX depending on the platform. *(Default: false)*
- `userHandle`: The user "handle" (also known as user "id") can be used to re-register credentials for an existing user, thus overriding the current credential key pair and username for that `userHandle`. *The default here is based on a hash of the `username`, and thus has some security implications as described in [issue](https://github.com/passwordless-id/webauthn/issues/29).*
Authentication options
----------------------
- `mediation`: See https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#mediation
Verification options
--------------------
- `userVerified`: to ensure that the user has been verified by the authenticator
- `counter`: this should be an incrementing value on each authentication, but it was made optional according to https://github.com/passwordless-id/webauthn/issues/38
- `domain`: in case you used a specific domain (relying party id) during registration/authentication, you need this too during verification
- `verbose`: prints more details to the console if enabled
Parsing data
------------
If you want to parse the encoded registration, authentication or parts of it without verifying it, it is possible using the `parsers` module. This might be helpful when debugging.
### Registration
```js
import { parsers } from '@passwordless-id/webauthn'
parsers.parseRegistration({
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="
})
```
```json
{
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"client": {
"type": "webauthn.create",
"challenge": "a7c61ef9-dc23-4806-b486-2428938a547e",
"origin": "http://localhost:8080",
"crossOrigin": false
},
"authenticator": {
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": true,
"extensionsIncluded": false
},
"counter": 0,
"aaguid": "08987058-cadc-4b81-b6e1-30de50dcbe96",
"name": "Windows Hello",
"icon_dark": "https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-dark.png",
"icon_light": "https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-light.png",
"synced": true
},
"attestation": null
}
```
### Authentication
```js
import { parsers } from '@passwordless-id/webauthn'
parsers.parseAuthentication({
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ==",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNTY1MzViMTMtNWQ5My00MTk0LWEyODItZjIzNGMxYzI0NTAwIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=",
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
})
```
```json
{
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"client": {
"type": "webauthn.get",
"challenge": "56535b13-5d93-4194-a282-f234c1c24500",
"origin": "http://localhost:8080",
"crossOrigin": false,
"other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex"
},
"authenticator": {
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": false,
"extensionsIncluded": false
},
"counter": 1
},
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
}
```
### `clientData`
```js
import { parsers } from '@passwordless-id/webauthn'
parsers.parseClient("eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==")
```
```json
{
"type": "webauthn.create",
"challenge": "a7c61ef9-dc23-4806-b486-2428938a547e",
"origin": "http://localhost:8080",
"crossOrigin": false
}
```
### `authenticatorData`
```js
import { parsers } from '@passwordless-id/webauthn'
parsers.parseAuthenticator("SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=")
```
```json
{
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": true,
"extensionsIncluded": false
},
"counter": 0,
"aaguid": "08987058-cadc-4b81-b6e1-30de50dcbe96",
"name": "Windows Hello",
"icon_dark": "https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-dark.png",
"icon_light": "https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-light.png",
"synced": true
}
```
Please note that `aaguid` and `name` are only available during registration.
What is the difference between this and Passwordless.ID?
--------------------------------------------------------
This library is a wrapper around the WebAuthn protocol.
It is the technical foundation for strong authentication.
No more, no less.
[Passwordless.ID](https://passwordless.id) is a service. It provides all the other things required for a complete authentication system:
- multiple registered devices per account
- user profile
- e-mail verification (phone should come too at some point)
- account recovery mechanisms
- OAuth2/OpenID integration
- ...
This WebAuthn library enables you to build a custom solution from scratch. In contrast, [Passwordless.ID](https://passwordless.id) enables you to use a "Sign in with Passwordless.ID" button, a bit like "Sign in with Google/Microsoft/Apple" but platform neutral, secure and without configuration.
The docs for the legacy version 1.x are found [here](https://webauthn.passwordless.id/version-1)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc