diff --git a/package.json b/package.json index f94d8ef..0143479 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@authsignal/browser", - "version": "0.4.1", + "version": "0.4.2", "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 abadcb8..16f11a9 100644 --- a/src/api/passkey-api-client.ts +++ b/src/api/passkey-api-client.ts @@ -3,6 +3,7 @@ import { AddAuthenticatorResponse, AuthenticationOptsRequest, AuthenticationOptsResponse, + ChallengeResponse, PasskeyAuthenticatorResponse, RegistrationOptsRequest, RegistrationOptsResponse, @@ -26,12 +27,12 @@ export class PasskeyApiClient { async registrationOptions({ token, - userName, + username, authenticatorAttachment, - }: RegistrationOptsRequest): Promise { - const body = Boolean(authenticatorAttachment) - ? {username: userName, authenticatorAttachment} - : {username: userName}; + }: {token: string} & RegistrationOptsRequest): Promise { + const body: RegistrationOptsRequest = Boolean(authenticatorAttachment) + ? {username, authenticatorAttachment} + : {username}; const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey/registration-options`, { method: "POST", @@ -42,31 +43,52 @@ export class PasskeyApiClient { return (await response).json(); } - async authenticationOptions({token}: AuthenticationOptsRequest): Promise { + async authenticationOptions({ + token, + challengeId, + }: {token?: string} & AuthenticationOptsRequest): Promise { + const body: AuthenticationOptsRequest = {challengeId}; + const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey/authentication-options`, { method: "POST", headers: this.buildHeaders(token), - body: JSON.stringify({}), + body: JSON.stringify(body), }); return (await response).json(); } - async addAuthenticator({token, ...rest}: AddAuthenticatorRequest): Promise { + async addAuthenticator({ + token, + challengeId, + registrationCredential, + }: {token: string} & AddAuthenticatorRequest): Promise { + const body: AddAuthenticatorRequest = { + challengeId, + registrationCredential, + }; + const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey`, { method: "POST", headers: this.buildHeaders(token), - body: JSON.stringify(rest), + body: JSON.stringify(body), }); return (await response).json(); } - async verify({token, ...rest}: VerifyRequest): Promise { + async verify({ + token, + challengeId, + authenticationCredential, + deviceId, + }: {token?: string} & VerifyRequest): Promise { + const body: VerifyRequest = {challengeId, authenticationCredential, deviceId}; + const response = fetch(`${this.baseUrl}/client/verify/passkey`, { method: "POST", headers: this.buildHeaders(token), - body: JSON.stringify(rest), + body: JSON.stringify(body), }); return (await response).json(); @@ -85,6 +107,16 @@ export class PasskeyApiClient { return response.json(); } + async challenge(action: string): Promise { + const response = fetch(`${this.baseUrl}/client/challenge`, { + method: "POST", + headers: this.buildHeaders(), + body: JSON.stringify({action}), + }); + + return (await response).json(); + } + private buildHeaders(token?: string) { const authorizationHeader = token ? `Bearer ${token}` : `Basic ${window.btoa(encodeURIComponent(this.tenantId))}`; diff --git a/src/api/types.ts b/src/api/types.ts index 2f293d8..e21cc48 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -6,8 +6,7 @@ import { } from "@simplewebauthn/types"; export type RegistrationOptsRequest = { - userName?: string; - token: string; + username?: string; authenticatorAttachment?: AuthenticatorAttachment | null; }; @@ -17,7 +16,7 @@ export type RegistrationOptsResponse = { }; export type AuthenticationOptsRequest = { - token?: string; + challengeId?: string; }; export type AuthenticationOptsResponse = { @@ -26,7 +25,6 @@ export type AuthenticationOptsResponse = { }; export type AddAuthenticatorRequest = { - token: string; challengeId: string; registrationCredential: RegistrationResponseJSON; }; @@ -38,9 +36,9 @@ export type AddAuthenticatorResponse = { }; export type VerifyRequest = { - token?: string; challengeId: string; authenticationCredential: AuthenticationResponseJSON; + deviceId?: string; }; export type VerifyResponse = { @@ -52,3 +50,7 @@ export type PasskeyAuthenticatorResponse = { credentialId: string; verifiedAt: string; }; + +export type ChallengeResponse = { + challengeId: string; +}; diff --git a/src/authsignal.ts b/src/authsignal.ts index c5ec335..f8ce1bc 100644 --- a/src/authsignal.ts +++ b/src/authsignal.ts @@ -42,8 +42,6 @@ export class Authsignal { throw new Error("tenantId is required"); } - this.passkey = new Passkey({tenantId, baseUrl}); - const idCookie = getCookie(this.anonymousIdCookieName); if (idCookie) { @@ -59,6 +57,8 @@ export class Authsignal { secure: document.location.protocol !== "http:", }); } + + this.passkey = new Passkey({tenantId, baseUrl, anonymousId: this.anonymousId}); } launch(url: string, options?: {mode?: "redirect"} & LaunchOptions): undefined; diff --git a/src/passkey.ts b/src/passkey.ts index 155bf5c..709301f 100644 --- a/src/passkey.ts +++ b/src/passkey.ts @@ -6,6 +6,7 @@ import {AuthenticationResponseJSON, RegistrationResponseJSON, AuthenticatorAttac type PasskeyOptions = { baseUrl: string; tenantId: string; + anonymousId: string; }; type SignUpParams = { @@ -17,13 +18,15 @@ type SignUpParams = { export class Passkey { public api: PasskeyApiClient; private passkeyLocalStorageKey = "as_passkey_credential_id"; + private anonymousId: string; - constructor({baseUrl, tenantId}: PasskeyOptions) { + constructor({baseUrl, tenantId, anonymousId}: PasskeyOptions) { this.api = new PasskeyApiClient({baseUrl, tenantId}); + this.anonymousId = anonymousId; } async signUp({userName, token, authenticatorAttachment = "platform"}: SignUpParams) { - const optionsResponse = await this.api.registrationOptions({userName, token, authenticatorAttachment}); + const optionsResponse = await this.api.registrationOptions({username: userName, token, authenticatorAttachment}); const registrationResponse = await startRegistration(optionsResponse.options); @@ -41,14 +44,24 @@ export class Passkey { } async signIn(): Promise; + async signIn(params?: {action: string; autofill?: boolean}): Promise; async signIn(params?: {token: string}): Promise; async signIn(params?: {autofill: boolean}): Promise; - async signIn(params?: {token?: string; autofill?: boolean} | undefined) { + async signIn(params?: {token?: string; autofill?: boolean; action?: string} | undefined) { if (params?.token && params.autofill) { - throw new Error("Autofill is not supported when providing a token"); + throw new Error("autofill is not supported when providing a token"); } - const optionsResponse = await this.api.authenticationOptions({token: params?.token}); + if (params?.action && params.token) { + throw new Error("action is not supported when providing a token"); + } + + const challengeResponse = params?.action ? await this.api.challenge(params.action) : null; + + const optionsResponse = await this.api.authenticationOptions({ + token: params?.token, + challengeId: challengeResponse?.challengeId, + }); const authenticationResponse = await startAuthentication(optionsResponse.options, params?.autofill); @@ -56,6 +69,7 @@ export class Passkey { challengeId: optionsResponse.challengeId, authenticationCredential: authenticationResponse, token: params?.token, + deviceId: this.anonymousId, }); if (verifyResponse?.isVerified) {