Skip to content

Commit

Permalink
fix: another temp commit
Browse files Browse the repository at this point in the history
  • Loading branch information
J0 committed Sep 23, 2024
1 parent d0801e3 commit 0cf58b7
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 117 deletions.
92 changes: 80 additions & 12 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -99,6 +104,8 @@ import type {
MFAEnrollTOTPParams,
MFAEnrollPhoneParams,
MFAEnrollWebAuthnParams,
AuthenticatorTransportFuture,
RegistrationCredential,
} from './lib/types'

polyfillGlobalThis() // Make "globalThis" available
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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') }
Expand Down
106 changes: 105 additions & 1 deletion src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
)
}
Loading

0 comments on commit 0cf58b7

Please sign in to comment.