UNPKG

@simplewebauthn/server

Version:
175 lines (174 loc) 8.84 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyAuthenticationResponse = verifyAuthenticationResponse; const decodeClientDataJSON_js_1 = require("../helpers/decodeClientDataJSON.js"); const toHash_js_1 = require("../helpers/toHash.js"); const verifySignature_js_1 = require("../helpers/verifySignature.js"); const parseAuthenticatorData_js_1 = require("../helpers/parseAuthenticatorData.js"); const parseBackupFlags_js_1 = require("../helpers/parseBackupFlags.js"); const matchExpectedRPID_js_1 = require("../helpers/matchExpectedRPID.js"); const index_js_1 = require("../helpers/iso/index.js"); /** * Verify that the user has legitimately completed the authentication process * * **Options:** * * @param response - Response returned by **@simplewebauthn/browser**'s `startAssertion()` * @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()` * @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on * @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options * @param credential - An internal {@link WebAuthnCredential} corresponding to `id` in the authentication response * @param expectedType **(Optional)** - The response type expected ('webauthn.get') * @param requireUserVerification **(Optional)** - Enforce user verification by the authenticator (via PIN, fingerprint, etc...) Defaults to `true` * @param advancedFIDOConfig **(Optional)** - Options for satisfying more stringent FIDO RP feature requirements * @param advancedFIDOConfig.userVerification **(Optional)** - Enable alternative rules for evaluating the User Presence and User Verified flags in authenticator data: UV (and UP) flags are optional unless this value is `"required"` */ async function verifyAuthenticationResponse(options) { const { response, expectedChallenge, expectedOrigin, expectedRPID, expectedType, credential, requireUserVerification = true, advancedFIDOConfig, } = options; const { id, rawId, type: credentialType, response: assertionResponse } = response; // Ensure credential specified an ID if (!id) { throw new Error('Missing credential ID'); } // Ensure ID is base64url-encoded if (id !== rawId) { throw new Error('Credential ID was not base64url-encoded'); } // Make sure credential type is public-key if (credentialType !== 'public-key') { throw new Error(`Unexpected credential type ${credentialType}, expected "public-key"`); } if (!response) { throw new Error('Credential missing response'); } if (typeof assertionResponse?.clientDataJSON !== 'string') { throw new Error('Credential response clientDataJSON was not a string'); } const clientDataJSON = (0, decodeClientDataJSON_js_1.decodeClientDataJSON)(assertionResponse.clientDataJSON); const { type, origin, challenge, tokenBinding } = clientDataJSON; // Make sure we're handling an authentication if (Array.isArray(expectedType)) { if (!expectedType.includes(type)) { const joinedExpectedType = expectedType.join(', '); throw new Error(`Unexpected authentication response type "${type}", expected one of: ${joinedExpectedType}`); } } else if (expectedType) { if (type !== expectedType) { throw new Error(`Unexpected authentication response type "${type}", expected "${expectedType}"`); } } else if (type !== 'webauthn.get') { throw new Error(`Unexpected authentication response type: ${type}`); } // Ensure the device provided the challenge we gave it if (typeof expectedChallenge === 'function') { if (!(await expectedChallenge(challenge))) { throw new Error(`Custom challenge verifier returned false for registration response challenge "${challenge}"`); } } else if (challenge !== expectedChallenge) { throw new Error(`Unexpected authentication response challenge "${challenge}", expected "${expectedChallenge}"`); } // Check that the origin is our site if (Array.isArray(expectedOrigin)) { if (!expectedOrigin.includes(origin)) { const joinedExpectedOrigin = expectedOrigin.join(', '); throw new Error(`Unexpected authentication response origin "${origin}", expected one of: ${joinedExpectedOrigin}`); } } else { if (origin !== expectedOrigin) { throw new Error(`Unexpected authentication response origin "${origin}", expected "${expectedOrigin}"`); } } if (!index_js_1.isoBase64URL.isBase64URL(assertionResponse.authenticatorData)) { throw new Error('Credential response authenticatorData was not a base64url string'); } if (!index_js_1.isoBase64URL.isBase64URL(assertionResponse.signature)) { throw new Error('Credential response signature was not a base64url string'); } if (assertionResponse.userHandle && typeof assertionResponse.userHandle !== 'string') { throw new Error('Credential response userHandle was not a string'); } if (tokenBinding) { if (typeof tokenBinding !== 'object') { throw new Error('ClientDataJSON tokenBinding was not an object'); } if (['present', 'supported', 'notSupported'].indexOf(tokenBinding.status) < 0) { throw new Error(`Unexpected tokenBinding status ${tokenBinding.status}`); } } const authDataBuffer = index_js_1.isoBase64URL.toBuffer(assertionResponse.authenticatorData); const parsedAuthData = (0, parseAuthenticatorData_js_1.parseAuthenticatorData)(authDataBuffer); const { rpIdHash, flags, counter, extensionsData } = parsedAuthData; // Make sure the response's RP ID is ours let expectedRPIDs = []; if (typeof expectedRPID === 'string') { expectedRPIDs = [expectedRPID]; } else { expectedRPIDs = expectedRPID; } const matchedRPID = await (0, matchExpectedRPID_js_1.matchExpectedRPID)(rpIdHash, expectedRPIDs); if (advancedFIDOConfig !== undefined) { const { userVerification: fidoUserVerification } = advancedFIDOConfig; /** * Use FIDO Conformance-defined rules for verifying UP and UV flags */ if (fidoUserVerification === 'required') { // Require `flags.uv` be true (implies `flags.up` is true) if (!flags.uv) { throw new Error('User verification required, but user could not be verified'); } } else if (fidoUserVerification === 'preferred' || fidoUserVerification === 'discouraged') { // Ignore `flags.uv` } } else { /** * Use WebAuthn spec-defined rules for verifying UP and UV flags */ // WebAuthn only requires the user presence flag be true if (!flags.up) { throw new Error('User not present during authentication'); } // Enforce user verification if required if (requireUserVerification && !flags.uv) { throw new Error('User verification required, but user could not be verified'); } } const clientDataHash = await (0, toHash_js_1.toHash)(index_js_1.isoBase64URL.toBuffer(assertionResponse.clientDataJSON)); const signatureBase = index_js_1.isoUint8Array.concat([authDataBuffer, clientDataHash]); const signature = index_js_1.isoBase64URL.toBuffer(assertionResponse.signature); if ((counter > 0 || credential.counter > 0) && counter <= credential.counter) { // Error out when the counter in the DB is greater than or equal to the counter in the // dataStruct. It's related to how the authenticator maintains the number of times its been // used for this client. If this happens, then someone's somehow increased the counter // on the device without going through this site throw new Error(`Response counter value ${counter} was lower than expected ${credential.counter}`); } const { credentialDeviceType, credentialBackedUp } = (0, parseBackupFlags_js_1.parseBackupFlags)(flags); const toReturn = { verified: await (0, verifySignature_js_1.verifySignature)({ signature, data: signatureBase, credentialPublicKey: credential.publicKey, }), authenticationInfo: { newCounter: counter, credentialID: credential.id, userVerified: flags.uv, credentialDeviceType, credentialBackedUp, authenticatorExtensionResults: extensionsData, origin: clientDataJSON.origin, rpID: matchedRPID, }, }; return toReturn; }