From 79df47d6ce8dd1e223563b427831ef90e0a62710 Mon Sep 17 00:00:00 2001 From: Hamish Meikle Date: Mon, 17 Jun 2024 15:11:19 +1000 Subject: [PATCH] Propagate server validation errors (#58) * wip * wip * wip --- package.json | 2 +- src/api/passkey-api-client.ts | 13 +++++++------ src/api/types.ts | 7 +++++++ src/helpers.ts | 18 ++++++++++++------ src/passkey.ts | 34 ++++++++++++++++++++++++++++++---- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 0143479..e9c934e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@authsignal/browser", - "version": "0.4.2", + "version": "0.4.3", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/api/passkey-api-client.ts b/src/api/passkey-api-client.ts index 16f11a9..df0eabb 100644 --- a/src/api/passkey-api-client.ts +++ b/src/api/passkey-api-client.ts @@ -3,6 +3,7 @@ import { AddAuthenticatorResponse, AuthenticationOptsRequest, AuthenticationOptsResponse, + AuthsignalResponse, ChallengeResponse, PasskeyAuthenticatorResponse, RegistrationOptsRequest, @@ -29,7 +30,7 @@ export class PasskeyApiClient { token, username, authenticatorAttachment, - }: {token: string} & RegistrationOptsRequest): Promise { + }: {token: string} & RegistrationOptsRequest): Promise> { const body: RegistrationOptsRequest = Boolean(authenticatorAttachment) ? {username, authenticatorAttachment} : {username}; @@ -46,7 +47,7 @@ export class PasskeyApiClient { async authenticationOptions({ token, challengeId, - }: {token?: string} & AuthenticationOptsRequest): Promise { + }: {token?: string} & AuthenticationOptsRequest): Promise> { const body: AuthenticationOptsRequest = {challengeId}; const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey/authentication-options`, { @@ -62,7 +63,7 @@ export class PasskeyApiClient { token, challengeId, registrationCredential, - }: {token: string} & AddAuthenticatorRequest): Promise { + }: {token: string} & AddAuthenticatorRequest): Promise> { const body: AddAuthenticatorRequest = { challengeId, registrationCredential, @@ -82,7 +83,7 @@ export class PasskeyApiClient { challengeId, authenticationCredential, deviceId, - }: {token?: string} & VerifyRequest): Promise { + }: {token?: string} & VerifyRequest): Promise> { const body: VerifyRequest = {challengeId, authenticationCredential, deviceId}; const response = fetch(`${this.baseUrl}/client/verify/passkey`, { @@ -94,7 +95,7 @@ export class PasskeyApiClient { return (await response).json(); } - async getPasskeyAuthenticator(credentialId: string): Promise { + async getPasskeyAuthenticator(credentialId: string): Promise> { const response = await fetch(`${this.baseUrl}/client/user-authenticators/passkey?credentialId=${credentialId}`, { method: "GET", headers: this.buildHeaders(), @@ -107,7 +108,7 @@ export class PasskeyApiClient { return response.json(); } - async challenge(action: string): Promise { + async challenge(action: string): Promise> { const response = fetch(`${this.baseUrl}/client/challenge`, { method: "POST", headers: this.buildHeaders(), diff --git a/src/api/types.ts b/src/api/types.ts index e21cc48..10405f0 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -54,3 +54,10 @@ export type PasskeyAuthenticatorResponse = { export type ChallengeResponse = { challengeId: string; }; + +export type ErrorResponse = { + error: string; + errorDescription?: string; +}; + +export type AuthsignalResponse = T | ErrorResponse; diff --git a/src/helpers.ts b/src/helpers.ts index 68fa143..605a722 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,3 +1,5 @@ +import {AuthsignalResponse, ErrorResponse} from "./api/types"; + type CookieOptions = { name: string; value: string; @@ -6,7 +8,7 @@ type CookieOptions = { secure: boolean; }; -export const setCookie = ({name, value, expire, domain, secure}: CookieOptions): void => { +export function setCookie({name, value, expire, domain, secure}: CookieOptions) { const expireString = expire === Infinity ? " expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + expire; document.cookie = encodeURIComponent(name) + @@ -16,13 +18,13 @@ export const setCookie = ({name, value, expire, domain, secure}: CookieOptions): expireString + (domain ? "; domain=" + domain : "") + (secure ? "; secure" : ""); -}; +} -export const getCookieDomain = (): string => { +export function getCookieDomain() { return document.location.hostname.replace("www.", ""); -}; +} -export const getCookie = (name: string) => { +export function getCookie(name: string) { if (!name) { return null; } @@ -36,4 +38,8 @@ export const getCookie = (name: string) => { ) ) || null ); -}; +} + +export function logErrorResponse(errorResponse: ErrorResponse) { + console.error(errorResponse.errorDescription ?? errorResponse.error); +} diff --git a/src/passkey.ts b/src/passkey.ts index 709301f..009f415 100644 --- a/src/passkey.ts +++ b/src/passkey.ts @@ -2,6 +2,7 @@ import {startAuthentication, startRegistration} from "@simplewebauthn/browser"; import {PasskeyApiClient} from "./api"; import {AuthenticationResponseJSON, RegistrationResponseJSON, AuthenticatorAttachment} from "@simplewebauthn/types"; +import {logErrorResponse} from "./helpers"; type PasskeyOptions = { baseUrl: string; @@ -28,6 +29,11 @@ export class Passkey { async signUp({userName, token, authenticatorAttachment = "platform"}: SignUpParams) { const optionsResponse = await this.api.registrationOptions({username: userName, token, authenticatorAttachment}); + if ("error" in optionsResponse) { + logErrorResponse(optionsResponse); + return; + } + const registrationResponse = await startRegistration(optionsResponse.options); const addAuthenticatorResponse = await this.api.addAuthenticator({ @@ -36,11 +42,16 @@ export class Passkey { token, }); - if (addAuthenticatorResponse?.isVerified) { + if ("error" in addAuthenticatorResponse) { + logErrorResponse(addAuthenticatorResponse); + return; + } + + if (addAuthenticatorResponse.isVerified) { this.storeCredentialAgainstDevice(registrationResponse); } - return addAuthenticatorResponse?.accessToken; + return addAuthenticatorResponse.accessToken; } async signIn(): Promise; @@ -58,11 +69,21 @@ export class Passkey { const challengeResponse = params?.action ? await this.api.challenge(params.action) : null; + if (challengeResponse && "error" in challengeResponse) { + logErrorResponse(challengeResponse); + return; + } + const optionsResponse = await this.api.authenticationOptions({ token: params?.token, challengeId: challengeResponse?.challengeId, }); + if ("error" in optionsResponse) { + logErrorResponse(optionsResponse); + return; + } + const authenticationResponse = await startAuthentication(optionsResponse.options, params?.autofill); const verifyResponse = await this.api.verify({ @@ -72,11 +93,16 @@ export class Passkey { deviceId: this.anonymousId, }); - if (verifyResponse?.isVerified) { + if ("error" in verifyResponse) { + logErrorResponse(verifyResponse); + return; + } + + if (verifyResponse.isVerified) { this.storeCredentialAgainstDevice(authenticationResponse); } - return verifyResponse?.accessToken; + return verifyResponse.accessToken; } async isAvailableOnDevice() {