From 0cf58b772a2a237c593a72af8cc8e31515dbd99b Mon Sep 17 00:00:00 2001 From: joel Date: Mon, 23 Sep 2024 20:10:18 +0200 Subject: [PATCH] fix: another temp commit --- src/GoTrueClient.ts | 92 +++++++++++++-- src/lib/helpers.ts | 106 ++++++++++++++++- src/lib/types.ts | 281 ++++++++++++++++++++++++++++---------------- 3 files changed, 362 insertions(+), 117 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 99dc402d..3b6e98e3 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -35,6 +35,11 @@ import { supportsLocalStorage, parseParametersFromURL, getCodeChallengeAndMethod, + base64URLStringToBuffer, + bufferToBase64URLString, + toPublicKeyCredentialDescriptor, + warnOnBrokenImplementation, + toAuthenticatorAttachment, } from './lib/helpers' import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage' import { polyfillGlobalThis } from './lib/polyfills' @@ -99,6 +104,8 @@ import type { MFAEnrollTOTPParams, MFAEnrollPhoneParams, MFAEnrollWebAuthnParams, + AuthenticatorTransportFuture, + RegistrationCredential, } from './lib/types' polyfillGlobalThis() // Make "globalThis" available @@ -2425,22 +2432,86 @@ export default class GoTrueClient { if (!challengeData) { return { data: null, error: new Error('Challenge data or options are null') } } - if (!(challengeData.type === 'webauthn' && challengeData?.credential_creation_options)) { + if (!(challengeData.type === 'webauthn' && challengeData?.options)) { return { data: null, error: new Error('Invalid challenge data for WebAuthn') } } try { // TODO: Undo this cast - let pubKey = challengeData?.credential_creation_options?.publicKey - console.log('after challenge data') - const publicKey = await navigator.credentials.create({ - publicKey: pubKey, - }) + let pubKey = challengeData?.options + const options: PublicKeyCredentialCreationOptions = { + ...pubKey, + challenge: base64URLStringToBuffer(pubKey.challenge), + user: { + ...pubKey.user, + id: base64URLStringToBuffer(pubKey.user.id), + }, + excludeCredentials: pubKey.excludeCredentials?.map(toPublicKeyCredentialDescriptor), + } + + const credential = (await navigator.credentials.create( + options + )) as RegistrationCredential - if (!publicKey) { + if (!credential) { return { data: null, error: new Error('Failed to create credentials') } } + const { id, rawId, response, type } = credential + // Continue to play it safe with `getTransports()` for now, even when L3 types say it's required + let transports: AuthenticatorTransportFuture[] | undefined = undefined + if (typeof response.getTransports === 'function') { + transports = response.getTransports() + } + + // L3 says this is required, but browser and webview support are still not guaranteed. + let responsePublicKeyAlgorithm: number | undefined = undefined + if (typeof response.getPublicKeyAlgorithm === 'function') { + try { + responsePublicKeyAlgorithm = response.getPublicKeyAlgorithm() + } catch (error) { + warnOnBrokenImplementation('getPublicKeyAlgorithm()', error as Error) + } + } + + let responsePublicKey: string | undefined = undefined + if (typeof response.getPublicKey === 'function') { + try { + const _publicKey = response.getPublicKey() + if (_publicKey !== null) { + responsePublicKey = bufferToBase64URLString(_publicKey) + } + } catch (error) { + warnOnBrokenImplementation('getPublicKey()', error as Error) + } + } - return await this._verify({ factorId, publicKey }) + // L3 says this is required, but browser and webview support are still not guaranteed. + let responseAuthenticatorData: string | undefined + if (typeof response.getAuthenticatorData === 'function') { + try { + responseAuthenticatorData = bufferToBase64URLString(response.getAuthenticatorData()) + } catch (error) { + warnOnBrokenImplementation('getAuthenticatorData()', error as Error) + } + } + const finalCredential = { + id, + rawId: bufferToBase64URLString(rawId), + response: { + attestationObject: bufferToBase64URLString(response.attestationObject), + clientDataJSON: bufferToBase64URLString(response.clientDataJSON), + transports, + publicKeyAlgorithm: responsePublicKeyAlgorithm, + publicKey: responsePublicKey, + authenticatorData: responseAuthenticatorData, + }, + type, + clientExtensionResults: credential.getClientExtensionResults(), + authenticatorAttachment: toAuthenticatorAttachment( + credential.authenticatorAttachment + ), + } + + return await this._verify({ factorId, publicKey: credential }) } catch (credentialError) { console.log(credentialError) return { @@ -2610,10 +2681,7 @@ export default class GoTrueClient { try { // TODO: This needs to chagne since ChallengeAndVerify is also for enroll - const pubKey = challengeResponse?.credential_request_options?.publicKey - const publicKey = await navigator.credentials.get({ - publicKey: pubKey, - }) + const publicKey = await navigator.credentials.get(options) // TODO: handle credential error if (!publicKey) { return { data: null, error: new AuthError('No valid credential found', 400, 'mfa_error') } diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index c34a0d0e..ee005b9d 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,5 +1,9 @@ import { API_VERSION_HEADER_NAME } from './constants' -import { SupportedStorage } from './types' +import { + SupportedStorage, + PublicKeyCredentialDescriptorJSON, + AuthenticatorAttachment, +} from './types' export function expiresAt(expiresIn: number) { const timeNow = Math.round(Date.now() / 1000) @@ -346,3 +350,103 @@ export function parseResponseAPIVersion(response: Response) { } // Taken from simplewebauthn + +/** + * Convert from a Base64URL-encoded string to an Array Buffer. Best used when converting a + * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or + * excludeCredentials + * + * Helper method to compliment `bufferToBase64URLString` + */ +export function base64URLStringToBuffer(base64URLString: string): ArrayBuffer { + // Convert from Base64URL to Base64 + const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/') + /** + * Pad with '=' until it's a multiple of four + * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding + * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding + * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding + * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding + */ + const padLength = (4 - (base64.length % 4)) % 4 + const padded = base64.padEnd(base64.length + padLength, '=') + + // Convert to a binary string + const binary = atob(padded) + + // Convert binary string to buffer + const buffer = new ArrayBuffer(binary.length) + const bytes = new Uint8Array(buffer) + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + + return buffer +} + +/** + * Convert the given array buffer into a Base64URL-encoded string. Ideal for converting various + * credential response ArrayBuffers to string for sending back to the server as JSON. + * + * Helper method to compliment `base64URLStringToBuffer` + */ +export function bufferToBase64URLString(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer) + let str = '' + + for (const charCode of bytes) { + str += String.fromCharCode(charCode) + } + + const base64String = btoa(str) + + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} + +export function toPublicKeyCredentialDescriptor( + descriptor: PublicKeyCredentialDescriptorJSON +): PublicKeyCredentialDescriptor { + const { id } = descriptor + + return { + ...descriptor, + id: base64URLStringToBuffer(id), + /** + * `descriptor.transports` is an array of our `AuthenticatorTransportFuture` that includes newer + * transports that TypeScript's DOM lib is ignorant of. Convince TS that our list of transports + * are fine to pass to WebAuthn since browsers will recognize the new value. + */ + transports: descriptor.transports as AuthenticatorTransport[], + } +} + +/** + * If possible coerce a `string` value into a known `AuthenticatorAttachment` + */ +export function toAuthenticatorAttachment( + attachment: string | null +): AuthenticatorAttachment | undefined { + const attachments: AuthenticatorAttachment[] = ['cross-platform', 'platform'] + + if (!attachment) { + return + } + + if (attachments.indexOf(attachment as AuthenticatorAttachment) < 0) { + return + } + + return attachment as AuthenticatorAttachment +} + +/** + * Visibly warn when we detect an issue related to a passkey provider intercepting WebAuthn API + * calls + */ +export function warnOnBrokenImplementation(methodName: string, cause: Error): void { + console.warn( + `The browser extension that intercepted this WebAuthn API call incorrectly implemented ${methodName}. You should report this error to them.\n`, + cause + ) +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 965139d6..8ce19f29 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -857,7 +857,7 @@ export type MFAVerifyWebAuthnParams = | { factorId: string - publicKey: Credential + publicKey: PublicKeyCredential } export type MFAChallengeTOTPParams = { @@ -1030,12 +1030,7 @@ export type AuthMFAChallengeResponse = /** Timestamp in UNIX seconds when this challenge will no longer be usable. */ expires_at: number - credential_request_options?: { - publicKey: PublicKeyCredentialRequestOptions - } - credential_creation_options?: { - publicKey: PublicKeyCredentialCreationOptions - } + options: PublicKeyCredentialCreationOptionsJSON | PublicKeyCredentialRequestOptionsJSON } error: null } @@ -1289,144 +1284,222 @@ export type SignOut = { } /** - * Available only in secure contexts. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAssertionResponse) + * https://w3c.github.io/webauthn/#dictdef-publickeycredentialdescriptorjson */ -export interface AuthenticatorAssertionResponse extends AuthenticatorResponse { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData) */ - readonly authenticatorData: ArrayBuffer - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAssertionResponse/signature) */ - readonly signature: ArrayBuffer - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAssertionResponse/userHandle) */ - readonly userHandle: ArrayBuffer | null +export interface PublicKeyCredentialDescriptorJSON { + id: Base64URLString + type: PublicKeyCredentialType + transports?: AuthenticatorTransportFuture[] +} + +export interface PublicKeyCredentialCreationOptionsJSON { + rp: PublicKeyCredentialRpEntity + user: PublicKeyCredentialUserEntityJSON + challenge: Base64URLString + pubKeyCredParams: PublicKeyCredentialParameters[] + timeout?: number + excludeCredentials?: PublicKeyCredentialDescriptorJSON[] + authenticatorSelection?: AuthenticatorSelectionCriteria + attestation?: AttestationConveyancePreference + extensions?: AuthenticationExtensionsClientInputs } /** - * Available only in secure contexts. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAttestationResponse) + * A variant of PublicKeyCredentialRequestOptions suitable for JSON transmission to the browser to + * (eventually) get passed into navigator.credentials.get(...) in the browser. */ -export interface AuthenticatorAttestationResponse extends AuthenticatorResponse { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAttestationResponse/attestationObject) */ - readonly attestationObject: ArrayBuffer - getAuthenticatorData(): ArrayBuffer - getPublicKey(): ArrayBuffer | null - getPublicKeyAlgorithm(): COSEAlgorithmIdentifier - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAttestationResponse/getTransports) */ - getTransports(): string[] +export interface PublicKeyCredentialRequestOptionsJSON { + challenge: Base64URLString + timeout?: number + rpId?: string + allowCredentials?: PublicKeyCredentialDescriptorJSON[] + userVerification?: UserVerificationRequirement + extensions?: AuthenticationExtensionsClientInputs } -export interface AuthenticationExtensionsClientInputs { - appid?: string - credProps?: boolean - hmacCreateSecret?: boolean +/** + * https://w3c.github.io/webauthn/#dictdef-publickeycredentialdescriptorjson + */ +export interface PublicKeyCredentialDescriptorJSON { + id: Base64URLString + type: PublicKeyCredentialType + transports?: AuthenticatorTransportFuture[] } -export interface AuthenticationExtensionsClientOutputs { - appid?: boolean - credProps?: CredentialPropertiesOutput - hmacCreateSecret?: boolean +/** + * https://w3c.github.io/webauthn/#dictdef-publickeycredentialuserentityjson + */ +export interface PublicKeyCredentialUserEntityJSON { + id: string + name: string + displayName: string } -export interface AuthenticatorSelectionCriteria { - authenticatorAttachment?: AuthenticatorAttachment - requireResidentKey?: boolean - residentKey?: ResidentKeyRequirement - userVerification?: UserVerificationRequirement +/** + * The value returned from navigator.credentials.create() + */ +export interface RegistrationCredential extends PublicKeyCredentialFuture { + response: AuthenticatorAttestationResponseFuture } /** - * Available only in secure contexts. + * A slightly-modified RegistrationCredential to simplify working with ArrayBuffers that + * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential) + * https://w3c.github.io/webauthn/#dictdef-registrationresponsejson */ -export interface PublicKeyCredential extends Credential { - readonly authenticatorAttachment: string | null - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential/rawId) */ - readonly rawId: ArrayBuffer - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential/response) */ - readonly response: AuthenticatorResponse - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential/getClientExtensionResults) */ - getClientExtensionResults(): AuthenticationExtensionsClientOutputs -} - -export interface PublicKeyCredentialCreationOptions { - attestation?: AttestationConveyancePreference - authenticatorSelection?: AuthenticatorSelectionCriteria - challenge: BufferSource - excludeCredentials?: PublicKeyCredentialDescriptor[] - extensions?: AuthenticationExtensionsClientInputs - pubKeyCredParams: PublicKeyCredentialParameters[] - rp: PublicKeyCredentialRpEntity - timeout?: number - user: PublicKeyCredentialUserEntity +export interface RegistrationResponseJSON { + id: Base64URLString + rawId: Base64URLString + response: AuthenticatorAttestationResponseJSON + authenticatorAttachment?: AuthenticatorAttachment + clientExtensionResults: AuthenticationExtensionsClientOutputs + type: PublicKeyCredentialType } -export interface PublicKeyCredentialDescriptor { - id: BufferSource - transports?: AuthenticatorTransport[] - type: PublicKeyCredentialType +/** + * The value returned from navigator.credentials.get() + */ +export interface AuthenticationCredential extends PublicKeyCredentialFuture { + response: AuthenticatorAssertionResponse } -export interface PublicKeyCredentialParameters { - alg: COSEAlgorithmIdentifier +/** + * 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 PublicKeyCredentialRequestOptions { - allowCredentials?: PublicKeyCredentialDescriptor[] - challenge: BufferSource - extensions?: AuthenticationExtensionsClientInputs - rpId?: string - timeout?: number - userVerification?: UserVerificationRequirement +export interface AuthenticatorAttestationResponse extends AuthenticatorResponse { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAttestationResponse/attestationObject) */ + readonly attestationObject: ArrayBuffer + getAuthenticatorData(): ArrayBuffer + getPublicKey(): ArrayBuffer | null + getPublicKeyAlgorithm(): COSEAlgorithmIdentifier + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorAttestationResponse/getTransports) */ + getTransports(): string[] } -export interface PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity { - displayName: string - id: BufferSource +/** + * 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 { + clientDataJSON: Base64URLString + attestationObject: Base64URLString + // Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation + authenticatorData?: Base64URLString + // Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation + transports?: AuthenticatorTransportFuture[] + // Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation + publicKeyAlgorithm?: COSEAlgorithmIdentifier + publicKey?: Base64URLString } /** - * Available only in secure contexts. + * 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. * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorResponse) + * https://w3c.github.io/webauthn/#dictdef-authenticatorassertionresponsejson */ -export interface AuthenticatorResponse { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AuthenticatorResponse/clientDataJSON) */ - readonly clientDataJSON: ArrayBuffer +export interface AuthenticatorAssertionResponseJSON { + clientDataJSON: Base64URLString + authenticatorData: Base64URLString + signature: Base64URLString + userHandle?: Base64URLString } -export interface CredentialPropertiesOutput { - rk?: boolean +/** + * A WebAuthn-compatible device and the information needed to verify assertions by it + */ +export type AuthenticatorDevice = { + credentialID: Base64URLString + credentialPublicKey: Uint8Array + // Number of times this authenticator is expected to have been used + counter: number + // From browser's `startRegistration()` -> RegistrationCredentialJSON.transports (API L2 and up) + transports?: AuthenticatorTransportFuture[] } /** - * Available only in secure contexts. + * An attempt to communicate that this isn't just any string, but a Base64URL-encoded string + */ +export type Base64URLString = string + +/** + * AuthenticatorAttestationResponse in TypeScript's DOM lib is outdated (up through v3.9.7). + * Maintain an augmented version here so we can implement additional properties as the WebAuthn + * spec evolves. + * + * See https://www.w3.org/TR/webauthn-2/#iface-authenticatorattestationresponse * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Credential) + * Properties marked optional are not supported in all browsers. */ -export interface Credential { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Credential/id) */ - readonly id: string - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Credential/type) */ - readonly type: string +export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse { + getTransports(): AuthenticatorTransportFuture[] } -export interface PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity { - id?: string +/** + * A super class of TypeScript's `AuthenticatorTransport` that includes support for the latest + * transports. Should eventually be replaced by TypeScript's when TypeScript gets updated to + * know about it (sometime after 4.6.3) + */ +export type AuthenticatorTransportFuture = + | 'ble' + | 'cable' + | 'hybrid' + | 'internal' + | 'nfc' + | 'smart-card' + | 'usb' + +/** + * A super class of TypeScript's `PublicKeyCredentialDescriptor` that knows about the latest + * transports. Should eventually be replaced by TypeScript's when TypeScript gets updated to + * know about it (sometime after 4.6.3) + */ +export interface PublicKeyCredentialDescriptorFuture + extends Omit { + transports?: AuthenticatorTransportFuture[] } -export interface PublicKeyCredentialEntity { - name: string +/** */ +export type PublicKeyCredentialJSON = RegistrationResponseJSON | AuthenticationResponseJSON + +/** + * A super class of TypeScript's `PublicKeyCredential` that knows about upcoming WebAuthn features + */ +export interface PublicKeyCredentialFuture extends PublicKeyCredential { + type: PublicKeyCredentialType + // See https://github.com/w3c/webauthn/issues/1745 + isConditionalMediationAvailable?(): Promise + // See https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON + parseCreationOptionsFromJSON?( + options: PublicKeyCredentialCreationOptionsJSON + ): PublicKeyCredentialCreationOptions + // See https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON + parseRequestOptionsFromJSON?( + options: PublicKeyCredentialRequestOptionsJSON + ): PublicKeyCredentialRequestOptions + // See https://w3c.github.io/webauthn/#dom-publickeycredential-tojson + toJSON?(): PublicKeyCredentialJSON } -export type AttestationConveyancePreference = 'direct' | 'enterprise' | 'indirect' | 'none' -export type AuthenticatorTransport = 'ble' | 'internal' | 'nfc' | 'usb' -export type COSEAlgorithmIdentifier = number -export type UserVerificationRequirement = 'discouraged' | 'preferred' | 'required' +/** + * The two types of credentials as defined by bit 3 ("Backup Eligibility") in authenticator data: + * - `"singleDevice"` credentials will never be backed up + * - `"multiDevice"` credentials can be backed up + */ +export type CredentialDeviceType = 'singleDevice' | 'multiDevice' export type AuthenticatorAttachment = 'cross-platform' | 'platform' -export type ResidentKeyRequirement = 'discouraged' | 'preferred' | 'required' -export type BufferSource = ArrayBufferView | ArrayBuffer -export type PublicKeyCredentialType = 'public-key'