Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rename functions and refactor native library calls into separate functions #126

107 changes: 82 additions & 25 deletions lib/ts/recipe/webauthn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ import { GeneralErrorResponse, User } from "../../types";
import { getNormalisedUserContext } from "../../utils";
import { RecipeFunctionOptions } from "../recipeModule/types";
import Recipe from "./recipe";
import { CredentialPayload, ResidentKey, UserInput, UserVerification } from "./types";
import {
CredentialPayload,
ResidentKey,
UserInput,
UserVerification,
RegistrationOptions,
AuthenticationOptions,
} from "./types";

export default class RecipeWrapper {
static init(config?: UserInput) {
Expand All @@ -39,7 +46,7 @@ export default class RecipeWrapper {
*
* @returns `{ status: "OK", ...}` if successful along a description of the created webauthn details (challenge, etc.)
*/
static registerOptions(
static getRegisterOptions(
input: { options?: RecipeFunctionOptions; userContext: any } & (
| { email: string }
| { recoverAccountToken: string }
Expand Down Expand Up @@ -90,7 +97,7 @@ export default class RecipeWrapper {
fetchResponse: Response;
}
> {
return Recipe.getInstanceOrThrow().recipeImplementation.registerOptions({
return Recipe.getInstanceOrThrow().recipeImplementation.getRegisterOptions({
...input,
userContext: getNormalisedUserContext(input?.userContext),
});
Expand All @@ -108,7 +115,7 @@ export default class RecipeWrapper {
*
* @returns `{ status: "OK", ...}` if successful along a description of the webauthn options (challenge, etc.)
*/
static signInOptions(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<
static getSignInOptions(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<
| {
status: "OK";
webauthnGeneratedOptionsId: string;
Expand All @@ -123,7 +130,7 @@ export default class RecipeWrapper {
}
| GeneralErrorResponse
> {
return Recipe.getInstanceOrThrow().recipeImplementation.signInOptions({
return Recipe.getInstanceOrThrow().recipeImplementation.getSignInOptions({
...input,
userContext: getNormalisedUserContext(input?.userContext),
});
Expand Down Expand Up @@ -217,14 +224,14 @@ export default class RecipeWrapper {
*
* @returns `{ status: "OK", ...}` if successful along with a boolean indicating existence
*/
static emailExists(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<
static getEmailExists(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<
| {
status: "OK";
exists: boolean;
}
| GeneralErrorResponse
> {
return Recipe.getInstanceOrThrow().recipeImplementation.emailExists({
return Recipe.getInstanceOrThrow().recipeImplementation.getEmailExists({
...input,
userContext: input?.userContext,
});
Expand Down Expand Up @@ -298,6 +305,45 @@ export default class RecipeWrapper {
});
}

/**
* Register user with the passed options by using native webauthn functions.
*
* It uses `@simplewebauthn/browser` to make the webauthn calls.
*
* @param registrationOptions Options to pass for the registration.
*
* @returns `{ status: "OK", ...}` if successful along with registration response received
*/
static registerUser(input: { registrationOptions: RegistrationOptions }): Promise<
| {
status: "OK";
registrationResponse: RegistrationResponseJSON;
}
| { status: "AUTHENTICATOR_ALREADY_REGISTERED" }
| { status: "FAILED_TO_REGISTER_USER"; error: any }
> {
return Recipe.getInstanceOrThrow().recipeImplementation.registerUser(input);
}

/**
* Authenticate the user with the passed options by using native webauthn functions.
*
* It uses `@simplewebauthn/browser` to make the webauthn calls.
*
* @param authenticationOptions Options to pass for the authentication.
*
* @returns `{ status: "OK", ...}` if successful along with authentication response received
*/
static authenticateUser(input: { authenticationOptions: AuthenticationOptions }): Promise<
| {
status: "OK";
authenticationResponse: AuthenticationResponseJSON;
}
| { status: "FAILED_TO_AUTHENTICATE_USER"; error: any }
> {
return Recipe.getInstanceOrThrow().recipeImplementation.authenticateUser(input);
}

/**
* Register the new device and signup the user with the passed email ID.
*
Expand All @@ -311,7 +357,7 @@ export default class RecipeWrapper {
*
* @returns `{ status: "OK", ...}` if successful along a description of the user details (id, etc.) and email
*/
static registerAndSignUp(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<
static registerUserWithSignUp(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this name sounds a bit confusing. What would be the difference between register and signUp ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with the changes, I have made them.

| {
status: "OK";
user: User;
Expand All @@ -338,8 +384,9 @@ export default class RecipeWrapper {
| { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string; fetchResponse: Response }
| { status: "EMAIL_ALREADY_EXISTS_ERROR"; fetchResponse: Response }
| { status: "AUTHENTICATOR_ALREADY_REGISTERED" }
| { status: "FAILED_TO_REGISTER_USER"; error: any }
> {
return Recipe.getInstanceOrThrow().recipeImplementation.registerAndSignUp({
return Recipe.getInstanceOrThrow().recipeImplementation.registerUserWithSignUp({
...input,
userContext: input?.userContext,
});
Expand All @@ -358,7 +405,11 @@ export default class RecipeWrapper {
*
* @returns `{ status: "OK", ...}` if successful along a description of the user details (id, etc.) and email
*/
static authenticateAndSignIn(input: { email: string; options?: RecipeFunctionOptions; userContext: any }): Promise<
static authenticateUserWithSignIn(input: {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Sounds a bit confusing

email: string;
options?: RecipeFunctionOptions;
userContext: any;
}): Promise<
| {
status: "OK";
user: User;
Expand All @@ -374,9 +425,10 @@ export default class RecipeWrapper {
reason: string;
fetchResponse: Response;
}
| { status: "FAILED_TO_AUTHENTICATE_USER"; error: any }
| GeneralErrorResponse
> {
return Recipe.getInstanceOrThrow().recipeImplementation.authenticateAndSignIn({
return Recipe.getInstanceOrThrow().recipeImplementation.authenticateUserWithSignIn({
...input,
userContext: input?.userContext,
});
Expand All @@ -395,7 +447,7 @@ export default class RecipeWrapper {
*
* @returns `{ status: "OK", ...}` if successful along a description of the user details (id, etc.) and email
*/
static registerAndRecoverAccount(input: {
static registerUserWithRecoverAccount(input: {
recoverAccountToken: string;
options?: RecipeFunctionOptions;
userContext: any;
Expand All @@ -421,36 +473,41 @@ export default class RecipeWrapper {
| { status: "INVALID_GENERATED_OPTIONS_ERROR"; fetchResponse: Response }
| { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string; fetchResponse: Response }
| { status: "AUTHENTICATOR_ALREADY_REGISTERED" }
| { status: "FAILED_TO_REGISTER_USER"; error: any }
> {
return Recipe.getInstanceOrThrow().recipeImplementation.registerAndRecoverAccount({
return Recipe.getInstanceOrThrow().recipeImplementation.registerUserWithRecoverAccount({
...input,
userContext: input?.userContext,
});
}
}

const init = RecipeWrapper.init;
const registerOptions = RecipeWrapper.registerOptions;
const signInOptions = RecipeWrapper.signInOptions;
const getRegisterOptions = RecipeWrapper.getRegisterOptions;
const getSignInOptions = RecipeWrapper.getSignInOptions;
const signUp = RecipeWrapper.signUp;
const signIn = RecipeWrapper.signIn;
const emailExists = RecipeWrapper.emailExists;
const getEmailExists = RecipeWrapper.getEmailExists;
const generateRecoverAccountToken = RecipeWrapper.generateRecoverAccountToken;
const recoverAccount = RecipeWrapper.recoverAccount;
const registerAndSignup = RecipeWrapper.registerAndSignUp;
const authenticateAndSignIn = RecipeWrapper.authenticateAndSignIn;
const registerAndRecoverAccount = RecipeWrapper.registerAndRecoverAccount;
const registerUserWithSignUp = RecipeWrapper.registerUserWithSignUp;
const authenticateUserWithSignIn = RecipeWrapper.authenticateUserWithSignIn;
const registerUserWithRecoverAccount = RecipeWrapper.registerUserWithRecoverAccount;
const registerUser = RecipeWrapper.registerUser;
const authenticateUser = RecipeWrapper.authenticateUser;

export {
init,
registerOptions,
signInOptions,
getRegisterOptions,
getSignInOptions,
signUp,
signIn,
emailExists,
getEmailExists,
generateRecoverAccountToken,
recoverAccount,
registerAndSignup,
authenticateAndSignIn,
registerAndRecoverAccount,
registerUserWithSignUp,
authenticateUserWithSignIn,
registerUserWithRecoverAccount,
registerUser,
authenticateUser,
};
93 changes: 57 additions & 36 deletions lib/ts/recipe/webauthn/recipeImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function getRecipeImplementation(
const querier = new Querier(recipeImplInput.recipeId, recipeImplInput.appInfo);

return {
registerOptions: async function ({
getRegisterOptions: async function ({
options,
userContext,
email,
Expand Down Expand Up @@ -115,7 +115,7 @@ export default function getRecipeImplementation(
fetchResponse,
};
},
signInOptions: async function ({ email, options, userContext }) {
getSignInOptions: async function ({ email, options, userContext }) {
const { jsonBody, fetchResponse } = await querier.post<
| {
status: "OK";
Expand Down Expand Up @@ -244,7 +244,7 @@ export default function getRecipeImplementation(
fetchResponse,
};
},
emailExists: async function ({ email, options, userContext }) {
getEmailExists: async function ({ email, options, userContext }) {
const { jsonBody, fetchResponse } = await querier.get<
| {
status: "OK";
Expand Down Expand Up @@ -354,9 +354,29 @@ export default function getRecipeImplementation(
fetchResponse,
};
},
registerAndSignUp: async function ({ email, options, userContext }) {
registerUser: async function ({ registrationOptions }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this should contain some credential reference in the name, as the main purpose of this method is to generate the credential for a user (in order for the credential to be registered - either on sign up or, in the future, on registerCredential in their "profile" )

let registrationResponse: RegistrationResponseJSON;
try {
registrationResponse = await startRegistration({ optionsJSON: registrationOptions });
} catch (error: any) {
if (error.name === "InvalidStateError") {
return { status: "AUTHENTICATOR_ALREADY_REGISTERED" };
}

return {
status: "FAILED_TO_REGISTER_USER",
error: error,
};
}

return {
status: "OK",
registrationResponse,
};
},
registerUserWithSignUp: async function ({ email, options, userContext }) {
// Get the registration options by using the passed email ID.
const registrationOptions = await this.registerOptions({ options, userContext, email });
const registrationOptions = await this.getRegisterOptions({ options, userContext, email });
if (registrationOptions?.status !== "OK") {
// If we did not get an OK status, we need to return the error as is.

Expand All @@ -371,56 +391,63 @@ export default function getRecipeImplementation(
}

// 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;
const registerUserResponse = await this.registerUser({ registrationOptions });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. The name should indicate that this is about the credential and not the user.

if (registerUserResponse.status !== "OK") {
return registerUserResponse;
}

// We should have a valid registration response for the passed credentials
// and we are good to go ahead and verify them.
return await this.signUp({
webauthnGeneratedOptionsId: registrationOptions.webauthnGeneratedOptionsId,
credential: registrationResponse,
credential: registerUserResponse.registrationResponse,
options,
userContext,
});
},
authenticateAndSignIn: async function ({ email, options, userContext }) {
authenticateUser: async function ({ authenticationOptions }) {
let authenticationResponse: AuthenticationResponseJSON;
try {
authenticationResponse = await startAuthentication({ optionsJSON: authenticationOptions });
} catch (error: any) {
return {
status: "FAILED_TO_AUTHENTICATE_USER",
error: error,
};
}

return {
status: "OK",
authenticationResponse: authenticationResponse,
};
},
authenticateUserWithSignIn: async function ({ email, options, userContext }) {
// Make a call to get the sign in options using the entered email ID.
const signInOptions = await this.signInOptions({ email, options, userContext });
const signInOptions = await this.getSignInOptions({ email, options, userContext });
if (signInOptions?.status !== "OK") {
// We want to return the error as is if status was not "OK"
return signInOptions;
}

// We should have the options ready and are good to start the authentication
let authenticationResponse: AuthenticationResponseJSON;
try {
authenticationResponse = await startAuthentication({ optionsJSON: signInOptions });
} catch (error: any) {
// TODO: Do we need to do something with the error besides throwing it?
throw error;
const authenticateUserResponse = await this.authenticateUser({ authenticationOptions: signInOptions });
if (authenticateUserResponse.status !== "OK") {
return authenticateUserResponse;
}

// We should have a valid authentication response at this point so we can
// go ahead and sign in the user.
return await this.signIn({
webauthnGeneratedOptionsId: signInOptions.webauthnGeneratedOptionsId,
credential: authenticationResponse,
credential: authenticateUserResponse.authenticationResponse,
options: options,
userContext: userContext,
});
},
registerAndRecoverAccount: async function ({ recoverAccountToken, options, userContext }) {
registerUserWithRecoverAccount: 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 });
const registrationOptions = await this.getRegisterOptions({ options, userContext, recoverAccountToken });
if (registrationOptions?.status !== "OK") {
// If we did not get an OK status, we need to return the error as is.

Expand All @@ -435,21 +462,15 @@ export default function getRecipeImplementation(
}

// 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;
const registerUserResponse = await this.registerUser({ registrationOptions });
if (registerUserResponse.status !== "OK") {
return registerUserResponse;
}

return await this.recoverAccount({
token: recoverAccountToken,
webauthnGeneratedOptionsId: registrationOptions.webauthnGeneratedOptionsId,
credential: registrationResponse,
credential: registerUserResponse.registrationResponse,
options,
userContext,
});
Expand Down
Loading
Loading