diff --git a/lib/ts/recipe/webauthn/index.ts b/lib/ts/recipe/webauthn/index.ts index 9643144..3630d39 100644 --- a/lib/ts/recipe/webauthn/index.ts +++ b/lib/ts/recipe/webauthn/index.ts @@ -13,6 +13,7 @@ * under the License. */ +import { AuthenticationResponseJSON, RegistrationResponseJSON } from "@simplewebauthn/browser"; import { GeneralErrorResponse, User } from "../../types"; import { getNormalisedUserContext } from "../../utils"; import { RecipeFunctionOptions } from "../recipeModule/types"; @@ -144,7 +145,7 @@ export default class RecipeWrapper { */ static signUp(input: { webauthnGeneratedOptionsId: string; - credential: CredentialPayload; + credential: RegistrationResponseJSON; options?: RecipeFunctionOptions; userContext: any; }): Promise< @@ -184,7 +185,7 @@ export default class RecipeWrapper { */ static signIn(input: { webauthnGeneratedOptionsId: string; - credential: CredentialPayload; + credential: AuthenticationResponseJSON; options?: RecipeFunctionOptions; userContext: any; }): Promise< diff --git a/lib/ts/recipe/webauthn/recipeImplementation.ts b/lib/ts/recipe/webauthn/recipeImplementation.ts index 0556e63..32040fa 100644 --- a/lib/ts/recipe/webauthn/recipeImplementation.ts +++ b/lib/ts/recipe/webauthn/recipeImplementation.ts @@ -417,6 +417,43 @@ export default function getRecipeImplementation( userContext: userContext, }); }, + registerAndRecoverAccount: async function ({ recoverAccountToken, options, userContext }) { + // Get the registration options based on the recoverAccountToken and + // register the device against the user. + const registrationOptions = await this.registerOptions({ options, userContext, recoverAccountToken }); + if (registrationOptions?.status !== "OK") { + // If we did not get an OK status, we need to return the error as is. + + // If the `status` is `INVALID_EMAIL_ERROR`, we need to throw an + // error since that should never happen as we are registering with a recover account token + // and not an email ID. + if (registrationOptions?.status === "INVALID_EMAIL_ERROR") { + throw new Error("Got `INVALID_EMAIL_ERROR` status that should never happen"); + } + + return registrationOptions; + } + + // We should have received a valid registration options response. + let registrationResponse: RegistrationResponseJSON; + try { + registrationResponse = await startRegistration({ optionsJSON: registrationOptions }); + } catch (error: any) { + if (error.name === "InvalidStateError") { + return { status: "AUTHENTICATOR_ALREADY_REGISTERED" }; + } + + throw error; + } + + return await this.recoverAccount({ + token: recoverAccountToken, + webauthnGeneratedOptionsId: registrationOptions.webauthnGeneratedOptionsId, + credential: registrationResponse, + options, + userContext, + }); + }, }; } diff --git a/lib/ts/recipe/webauthn/types.ts b/lib/ts/recipe/webauthn/types.ts index 6d11f3d..f939192 100644 --- a/lib/ts/recipe/webauthn/types.ts +++ b/lib/ts/recipe/webauthn/types.ts @@ -208,7 +208,7 @@ export type RecipeInterface = { recoverAccount: (input: { token: string; webauthnGeneratedOptionsId: string; - credential: CredentialPayload; + credential: RegistrationResponseJSON; options?: RecipeFunctionOptions; userContext: any; }) => Promise< @@ -296,5 +296,6 @@ export type RecipeInterface = { | { status: "GENERATED_OPTIONS_NOT_FOUND_ERROR"; fetchResponse: Response } | { status: "INVALID_GENERATED_OPTIONS_ERROR"; fetchResponse: Response } | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string; fetchResponse: Response } + | { status: "AUTHENTICATOR_ALREADY_REGISTERED" } >; };