diff --git a/lib/build/recipe/accountlinking/types.d.ts b/lib/build/recipe/accountlinking/types.d.ts index 3530940ee..845780080 100644 --- a/lib/build/recipe/accountlinking/types.d.ts +++ b/lib/build/recipe/accountlinking/types.d.ts @@ -182,9 +182,12 @@ export declare type AccountInfo = { id: string; userId: string; }; + webauthn?: { + credentialIds: string[]; + }; }; export declare type AccountInfoWithRecipeId = { - recipeId: "emailpassword" | "thirdparty" | "passwordless"; + recipeId: "emailpassword" | "thirdparty" | "passwordless" | "webauthn"; } & AccountInfo; export declare type RecipeLevelUser = { tenantIds: string[]; diff --git a/lib/build/recipe/multifactorauth/index.d.ts b/lib/build/recipe/multifactorauth/index.d.ts index b62940921..b2d06cd51 100644 --- a/lib/build/recipe/multifactorauth/index.d.ts +++ b/lib/build/recipe/multifactorauth/index.d.ts @@ -9,6 +9,7 @@ export default class Wrapper { static MultiFactorAuthClaim: import("./multiFactorAuthClaim").MultiFactorAuthClaimClass; static FactorIds: { EMAILPASSWORD: string; + WEBAUTHN: string; OTP_EMAIL: string; OTP_PHONE: string; LINK_EMAIL: string; diff --git a/lib/build/recipe/multifactorauth/types.d.ts b/lib/build/recipe/multifactorauth/types.d.ts index 53f7ac093..e02cbadb4 100644 --- a/lib/build/recipe/multifactorauth/types.d.ts +++ b/lib/build/recipe/multifactorauth/types.d.ts @@ -136,6 +136,7 @@ export declare type GetPhoneNumbersForFactorsFromOtherRecipesFunc = ( }; export declare const FactorIds: { EMAILPASSWORD: string; + WEBAUTHN: string; OTP_EMAIL: string; OTP_PHONE: string; LINK_EMAIL: string; diff --git a/lib/build/recipe/multifactorauth/types.js b/lib/build/recipe/multifactorauth/types.js index ef0ec1829..e4e0f5219 100644 --- a/lib/build/recipe/multifactorauth/types.js +++ b/lib/build/recipe/multifactorauth/types.js @@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.FactorIds = void 0; exports.FactorIds = { EMAILPASSWORD: "emailpassword", + WEBAUTHN: "webauthn", OTP_EMAIL: "otp-email", OTP_PHONE: "otp-phone", LINK_EMAIL: "link-email", diff --git a/lib/build/recipe/webauthn/api/emailExists.d.ts b/lib/build/recipe/webauthn/api/emailExists.d.ts new file mode 100644 index 000000000..2f55b6d3b --- /dev/null +++ b/lib/build/recipe/webauthn/api/emailExists.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; +export default function emailExists( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/emailExists.js b/lib/build/recipe/webauthn/api/emailExists.js new file mode 100644 index 000000000..b76ac08c7 --- /dev/null +++ b/lib/build/recipe/webauthn/api/emailExists.js @@ -0,0 +1,45 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const error_1 = __importDefault(require("../error")); +async function emailExists(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 + if (apiImplementation.emailExistsGET === undefined) { + return false; + } + let email = options.req.getKeyValueFromQuery("email"); + if (email === undefined || typeof email !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the email as a GET param", + }); + } + let result = await apiImplementation.emailExistsGET({ + email, + tenantId, + options, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = emailExists; diff --git a/lib/build/recipe/webauthn/api/generateRecoverAccountToken.d.ts b/lib/build/recipe/webauthn/api/generateRecoverAccountToken.d.ts new file mode 100644 index 000000000..ca836c5b4 --- /dev/null +++ b/lib/build/recipe/webauthn/api/generateRecoverAccountToken.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; +export default function generateRecoverAccountToken( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/generateRecoverAccountToken.js b/lib/build/recipe/webauthn/api/generateRecoverAccountToken.js new file mode 100644 index 000000000..651c5a398 --- /dev/null +++ b/lib/build/recipe/webauthn/api/generateRecoverAccountToken.js @@ -0,0 +1,45 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const error_1 = __importDefault(require("../error")); +async function generateRecoverAccountToken(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.generateRecoverAccountTokenPOST === undefined) { + return false; + } + const requestBody = await options.req.getJSONBody(); + const email = requestBody.email; + if (email === undefined || typeof email !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the email", + }); + } + let result = await apiImplementation.generateRecoverAccountTokenPOST({ + email, + tenantId, + options, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = generateRecoverAccountToken; diff --git a/lib/build/recipe/webauthn/api/implementation.d.ts b/lib/build/recipe/webauthn/api/implementation.d.ts new file mode 100644 index 000000000..2f382b449 --- /dev/null +++ b/lib/build/recipe/webauthn/api/implementation.d.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +import { APIInterface } from ".."; +export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/webauthn/api/implementation.js b/lib/build/recipe/webauthn/api/implementation.js new file mode 100644 index 000000000..6eef006ec --- /dev/null +++ b/lib/build/recipe/webauthn/api/implementation.js @@ -0,0 +1,886 @@ +"use strict"; +var __rest = + (this && this.__rest) || + function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; + } + return t; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../../emailverification/recipe")); +const authUtils_1 = require("../../../authUtils"); +const utils_1 = require("../../thirdparty/utils"); +const constants_1 = require("../constants"); +const recipeUserId_1 = __importDefault(require("../../../recipeUserId")); +const utils_2 = require("../utils"); +const logger_1 = require("../../../logger"); +const __1 = require("../../.."); +function getAPIImplementation() { + return { + registerOptionsPOST: async function (_a) { + var { tenantId, options, userContext } = _a, + props = __rest(_a, ["tenantId", "options", "userContext"]); + const relyingPartyId = await options.config.relyingPartyId({ + tenantId, + request: options.req, + userContext, + }); + const relyingPartyName = await options.config.relyingPartyName({ + tenantId, + userContext, + }); + const origin = await options.config.getOrigin({ + tenantId, + request: options.req, + userContext, + }); + const timeout = constants_1.DEFAULT_REGISTER_OPTIONS_TIMEOUT; + const attestation = constants_1.DEFAULT_REGISTER_OPTIONS_ATTESTATION; + const requireResidentKey = constants_1.DEFAULT_REGISTER_OPTIONS_REQUIRE_RESIDENT_KEY; + const residentKey = constants_1.DEFAULT_REGISTER_OPTIONS_RESIDENT_KEY; + const userVerification = constants_1.DEFAULT_REGISTER_OPTIONS_USER_VERIFICATION; + const supportedAlgorithmIds = constants_1.DEFAULT_REGISTER_OPTIONS_SUPPORTED_ALGORITHM_IDS; + let response = await options.recipeImplementation.registerOptions( + Object.assign(Object.assign({}, props), { + attestation, + requireResidentKey, + residentKey, + userVerification, + origin, + relyingPartyId, + relyingPartyName, + timeout, + tenantId, + userContext, + supportedAlgorithmIds, + }) + ); + if (response.status !== "OK") { + return response; + } + return { + status: "OK", + webauthnGeneratedOptionsId: response.webauthnGeneratedOptionsId, + challenge: response.challenge, + timeout: response.timeout, + attestation: response.attestation, + pubKeyCredParams: response.pubKeyCredParams, + excludeCredentials: response.excludeCredentials, + rp: response.rp, + user: response.user, + authenticatorSelection: response.authenticatorSelection, + }; + }, + signInOptionsPOST: async function ({ email, tenantId, options, userContext }) { + const relyingPartyId = await options.config.relyingPartyId({ + tenantId, + request: options.req, + userContext, + }); + // use this to get the full url instead of only the domain url + const origin = await options.config.getOrigin({ + tenantId, + request: options.req, + userContext, + }); + const timeout = constants_1.DEFAULT_SIGNIN_OPTIONS_TIMEOUT; + const userVerification = constants_1.DEFAULT_SIGNIN_OPTIONS_USER_VERIFICATION; + let response = await options.recipeImplementation.signInOptions({ + email, + userVerification, + origin, + relyingPartyId, + timeout, + tenantId, + userContext, + }); + if (response.status !== "OK") { + return response; + } + return { + status: "OK", + webauthnGeneratedOptionsId: response.webauthnGeneratedOptionsId, + challenge: response.challenge, + timeout: response.timeout, + userVerification: response.userVerification, + }; + }, + signUpPOST: async function ({ + webauthnGeneratedOptionsId, + credential, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }) { + const errorCodeMap = { + SIGN_UP_NOT_ALLOWED: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + INVALID_AUTHENTICATOR_ERROR: { + // TODO: add more cases + }, + WRONG_CREDENTIALS_ERROR: "The sign up credentials are incorrect. Please use a different authenticator.", + LINKING_TO_SESSION_USER_FAILED: { + EMAIL_VERIFICATION_REQUIRED: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_013)", + RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_014)", + ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_015)", + SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_016)", + }, + }; + const generatedOptions = await options.recipeImplementation.getGeneratedOptions({ + webauthnGeneratedOptionsId, + tenantId, + userContext, + }); + if (generatedOptions.status !== "OK") { + return { status: "WRONG_CREDENTIALS_ERROR" }; + } + const email = generatedOptions.email; + // NOTE: Following checks will likely never throw an error as the + // check for type is done in a parent function but they are kept + // here to be on the safe side. + if (!email) { + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + } + const preAuthCheckRes = await authUtils_1.AuthUtils.preAuthChecks({ + authenticatingAccountInfo: { + recipeId: "webauthn", + email, + }, + factorIds: ["webauthn"], + isSignUp: true, + isVerified: utils_1.isFakeEmail(email), + signInVerifiesLoginMethod: false, + skipSessionUserUpdateInCore: false, + authenticatingUser: undefined, + tenantId, + userContext, + session, + shouldTryLinkingWithSessionUser, + }); + if (preAuthCheckRes.status === "SIGN_UP_NOT_ALLOWED") { + const conflictingUsers = await recipe_1.default + .getInstance() + .recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: { + email, + }, + doUnionOfAccountInfo: false, + userContext, + }); + if ( + conflictingUsers.some((u) => + u.loginMethods.some((lm) => lm.recipeId === "webauthn" && lm.hasSameEmailAs(email)) + ) + ) { + return { + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; + } + } + if (preAuthCheckRes.status !== "OK") { + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + preAuthCheckRes, + errorCodeMap, + "SIGN_UP_NOT_ALLOWED" + ); + } + if (utils_1.isFakeEmail(email) && preAuthCheckRes.isFirstFactor) { + // Fake emails cannot be used as a first factor + return { + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; + } + // we are using the email from the register options + const signUpResponse = await options.recipeImplementation.signUp({ + webauthnGeneratedOptionsId, + credential, + tenantId, + session, + shouldTryLinkingWithSessionUser, + userContext, + }); + if (signUpResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + return signUpResponse; + } + if (signUpResponse.status !== "OK") { + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + signUpResponse, + errorCodeMap, + "SIGN_UP_NOT_ALLOWED" + ); + } + const postAuthChecks = await authUtils_1.AuthUtils.postAuthChecks({ + authenticatedUser: signUpResponse.user, + recipeUserId: signUpResponse.recipeUserId, + isSignUp: true, + factorId: "webauthn", + session, + req: options.req, + res: options.res, + tenantId, + userContext, + }); + if (postAuthChecks.status !== "OK") { + // It should never actually come here, but we do it cause of consistency. + // If it does come here (in case there is a bug), it would make this func throw + // anyway, cause there is no SIGN_IN_NOT_ALLOWED in the errorCodeMap. + authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + postAuthChecks, + errorCodeMap, + "SIGN_UP_NOT_ALLOWED" + ); + throw new Error("This should never happen"); + } + return { + status: "OK", + session: postAuthChecks.session, + user: postAuthChecks.user, + }; + }, + signInPOST: async function ({ + webauthnGeneratedOptionsId, + credential, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }) { + const errorCodeMap = { + SIGN_IN_NOT_ALLOWED: + "Cannot sign in due to security reasons. Please try recovering your account, use a different login method or contact support. (ERR_CODE_008)", + LINKING_TO_SESSION_USER_FAILED: { + EMAIL_VERIFICATION_REQUIRED: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_009)", + RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_010)", + ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_011)", + SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR: + "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_012)", + }, + }; + const recipeId = "webauthn"; + // do the verification before in order to retrieve the user email + const verifyCredentialsResponse = await options.recipeImplementation.verifyCredentials({ + credential, + webauthnGeneratedOptionsId, + tenantId, + userContext, + }); + const checkCredentialsOnTenant = async () => { + return verifyCredentialsResponse.status === "OK"; + }; + // doing it like this because the email is only available after verifyCredentials is called + let email; + if (verifyCredentialsResponse.status == "OK") { + const loginMethod = verifyCredentialsResponse.user.loginMethods.find((lm) => lm.recipeId === recipeId); + // there should be a webauthn login method and an email when trying to sign in using webauthn + if (!loginMethod || !loginMethod.email) { + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + verifyCredentialsResponse, + errorCodeMap, + "SIGN_IN_NOT_ALLOWED" + ); + } + email = loginMethod === null || loginMethod === void 0 ? void 0 : loginMethod.email; + } else { + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + const authenticatingUser = await authUtils_1.AuthUtils.getAuthenticatingUserAndAddToCurrentTenantIfRequired( + { + accountInfo: { email }, + userContext, + recipeId, + session, + tenantId, + checkCredentialsOnTenant, + } + ); + const isVerified = authenticatingUser !== undefined && authenticatingUser.loginMethod.verified; + // We check this before preAuthChecks, because that function assumes that if isSignUp is false, + // then authenticatingUser is defined. While it wouldn't technically cause any problems with + // the implementation of that function, this way we can guarantee that either isSignInAllowed or + // isSignUpAllowed will be called as expected. + if (authenticatingUser === undefined) { + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + const preAuthChecks = await authUtils_1.AuthUtils.preAuthChecks({ + authenticatingAccountInfo: { + recipeId, + email, + }, + factorIds: [recipeId], + isSignUp: false, + authenticatingUser: + authenticatingUser === null || authenticatingUser === void 0 ? void 0 : authenticatingUser.user, + isVerified, + signInVerifiesLoginMethod: false, + skipSessionUserUpdateInCore: false, + tenantId, + userContext, + session, + shouldTryLinkingWithSessionUser, + }); + if (preAuthChecks.status === "SIGN_UP_NOT_ALLOWED") { + throw new Error("This should never happen: pre-auth checks should not fail for sign in"); + } + if (preAuthChecks.status !== "OK") { + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + preAuthChecks, + errorCodeMap, + "SIGN_IN_NOT_ALLOWED" + ); + } + if (utils_1.isFakeEmail(email) && preAuthChecks.isFirstFactor) { + // Fake emails cannot be used as a first factor + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + const signInResponse = await options.recipeImplementation.signIn({ + webauthnGeneratedOptionsId, + credential, + session, + shouldTryLinkingWithSessionUser, + tenantId, + userContext, + }); + if (signInResponse.status === "WRONG_CREDENTIALS_ERROR") { + return signInResponse; + } + if (signInResponse.status !== "OK") { + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + signInResponse, + errorCodeMap, + "SIGN_IN_NOT_ALLOWED" + ); + } + const postAuthChecks = await authUtils_1.AuthUtils.postAuthChecks({ + authenticatedUser: signInResponse.user, + recipeUserId: signInResponse.recipeUserId, + isSignUp: false, + factorId: recipeId, + session, + req: options.req, + res: options.res, + tenantId, + userContext, + }); + if (postAuthChecks.status !== "OK") { + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + postAuthChecks, + errorCodeMap, + "SIGN_IN_NOT_ALLOWED" + ); + } + return { + status: "OK", + session: postAuthChecks.session, + user: postAuthChecks.user, + }; + }, + emailExistsGET: async function ({ email, tenantId, userContext }) { + // even if the above returns true, we still need to check if there + // exists an webauthn user with the same email cause the function + // above does not check for that. + let users = await recipe_1.default.getInstance().recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: { + email, + }, + doUnionOfAccountInfo: false, + userContext, + }); + let webauthnUserExists = + users.find((u) => { + return ( + u.loginMethods.find((lm) => lm.recipeId === "webauthn" && lm.hasSameEmailAs(email)) !== + undefined + ); + }) !== undefined; + return { + status: "OK", + exists: webauthnUserExists, + }; + }, + generateRecoverAccountTokenPOST: async function ({ email, tenantId, options, userContext }) { + // NOTE: Check for email being a non-string value. This check will likely + // never evaluate to `true` as there is an upper-level check for the type + // in validation but kept here to be safe. + if (typeof email !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + // this function will be reused in different parts of the flow below.. + async function generateAndSendRecoverAccountToken(primaryUserId, recipeUserId) { + // the user ID here can be primary or recipe level. + let response = await options.recipeImplementation.generateRecoverAccountToken({ + tenantId, + userId: recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString(), + email, + userContext, + }); + if (response.status === "UNKNOWN_USER_ID_ERROR") { + logger_1.logDebugMessage( + `Recover account email not sent, unknown user id: ${ + recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() + }` + ); + return { + status: "OK", + }; + } + let recoverAccountLink = utils_2.getRecoverAccountLink({ + appInfo: options.appInfo, + token: response.token, + tenantId, + request: options.req, + userContext, + }); + logger_1.logDebugMessage(`Sending recover account email to ${email}`); + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + tenantId, + type: "RECOVER_ACCOUNT", + user: { + id: primaryUserId, + recipeUserId, + email, + }, + recoverAccountLink, + userContext, + }); + return { + status: "OK", + }; + } + /** + * check if primaryUserId is linked with this email + */ + let users = await recipe_1.default.getInstance().recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: { + email, + }, + doUnionOfAccountInfo: false, + userContext, + }); + // we find the recipe user ID of the webauthn account from the user's list + // for later use. + let webauthnAccount = undefined; + for (let i = 0; i < users.length; i++) { + let webauthnAccountTmp = users[i].loginMethods.find( + (l) => l.recipeId === "webauthn" && l.hasSameEmailAs(email) + ); + if (webauthnAccountTmp !== undefined) { + webauthnAccount = webauthnAccountTmp; + break; + } + } + // we find the primary user ID from the user's list for later use. + let primaryUserAssociatedWithEmail = users.find((u) => u.isPrimaryUser); + // first we check if there even exists a primary user that has the input email + // if not, then we do the regular flow for recover account + if (primaryUserAssociatedWithEmail === undefined) { + if (webauthnAccount === undefined) { + logger_1.logDebugMessage(`Recover account email not sent, unknown user email: ${email}`); + return { + status: "OK", + }; + } + return await generateAndSendRecoverAccountToken( + webauthnAccount.recipeUserId.getAsString(), + webauthnAccount.recipeUserId + ); + } + // Next we check if there is any login method in which the input email is verified. + // If that is the case, then it's proven that the user owns the email and we can + // trust linking of the webauthn account. + let emailVerified = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.hasSameEmailAs(email) && lm.verified; + }) !== undefined; + // finally, we check if the primary user has any other email / phone number + // associated with this account - and if it does, then it means that + // there is a risk of account takeover, so we do not allow the token to be generated + let hasOtherEmailOrPhone = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + // we do the extra undefined check below cause + // hasSameEmailAs returns false if the lm.email is undefined, and + // we want to check that the email is different as opposed to email + // not existing in lm. + return (lm.email !== undefined && !lm.hasSameEmailAs(email)) || lm.phoneNumber !== undefined; + }) !== undefined; + if (!emailVerified && hasOtherEmailOrPhone) { + return { + status: "RECOVER_ACCOUNT_NOT_ALLOWED", + reason: + "Recover account link was not created because of account take over risk. Please contact support. (ERR_CODE_001)", + }; + } + let shouldDoAccountLinkingResponse = await recipe_1.default + .getInstance() + .config.shouldDoAutomaticAccountLinking( + webauthnAccount !== undefined + ? webauthnAccount + : { + recipeId: "webauthn", + email, + }, + primaryUserAssociatedWithEmail, + undefined, + tenantId, + userContext + ); + // Now we need to check that if there exists any webauthn user at all + // for the input email. If not, then it implies that when the token is consumed, + // then we will create a new user - so we should only generate the token if + // the criteria for the new user is met. + if (webauthnAccount === undefined) { + // this means that there is no webauthn user that exists for the input email. + // So we check for the sign up condition and only go ahead if that condition is + // met. + // But first we must check if account linking is enabled at all - cause if it's + // not, then the new webauthn user that will be created in recover account + // code consume cannot be linked to the primary user - therefore, we should + // not generate a recover account reset token + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + `Recover account email not sent, since webauthn user didn't exist, and account linking not enabled` + ); + return { + status: "OK", + }; + } + let isSignUpAllowed = await recipe_1.default.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "webauthn", + email, + }, + isVerified: true, + session: undefined, + tenantId, + userContext, + }); + if (isSignUpAllowed) { + // notice that we pass in the primary user ID here. This means that + // we will be creating a new webauthn account when the token + // is consumed and linking it to this primary user. + return await generateAndSendRecoverAccountToken(primaryUserAssociatedWithEmail.id, undefined); + } else { + logger_1.logDebugMessage( + `Recover account email not sent, isSignUpAllowed returned false for email: ${email}` + ); + return { + status: "OK", + }; + } + } + // At this point, we know that some webauthn user exists with this email + // and also some primary user ID exist. We now need to find out if they are linked + // together or not. If they are linked together, then we can just generate the token + // else we check for more security conditions (since we will be linking them post token generation) + let areTheTwoAccountsLinked = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.recipeUserId.getAsString() === webauthnAccount.recipeUserId.getAsString(); + }) !== undefined; + if (areTheTwoAccountsLinked) { + return await generateAndSendRecoverAccountToken( + primaryUserAssociatedWithEmail.id, + webauthnAccount.recipeUserId + ); + } + // Here we know that the two accounts are NOT linked. We now need to check for an + // extra security measure here to make sure that the input email in the primary user + // is verified, and if not, we need to make sure that there is no other email / phone number + // associated with the primary user account. If there is, then we do not proceed. + /* + This security measure helps prevent the following attack: + An attacker has email A and they create an account using TP and it doesn't matter if A is verified or not. Now they create another account using the webauthn with email A and verifies it. Both these accounts are linked. Now the attacker changes the email for webauthn recipe to B which makes the webauthn account unverified, but it's still linked. + + If the real owner of B tries to signup using webauthn, it will say that the account already exists so they may try to recover the account which should be denied because then they will end up getting access to attacker's account and verify the webauthn account. + + The problem with this situation is if the webauthn account is verified, it will allow further sign-ups with email B which will also be linked to this primary account (that the attacker had created with email A). + + It is important to realize that the attacker had created another account with A because if they hadn't done that, then they wouldn't have access to this account after the real user recovers the account which is why it is important to check there is another non-webauthn account linked to the primary such that the email is not the same as B. + + Exception to the above is that, if there is a third recipe account linked to the above two accounts and has B as verified, then we should allow recover account token generation because user has already proven that the owns the email B + */ + // But first, this only matters it the user cares about checking for email verification status.. + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + // here we will go ahead with the token generation cause + // even when the token is consumed, we will not be linking the accounts + // so no need to check for anything + return await generateAndSendRecoverAccountToken( + webauthnAccount.recipeUserId.getAsString(), + webauthnAccount.recipeUserId + ); + } + if (!shouldDoAccountLinkingResponse.shouldRequireVerification) { + // the checks below are related to email verification, and if the user + // does not care about that, then we should just continue with token generation + return await generateAndSendRecoverAccountToken( + primaryUserAssociatedWithEmail.id, + webauthnAccount.recipeUserId + ); + } + return await generateAndSendRecoverAccountToken( + primaryUserAssociatedWithEmail.id, + webauthnAccount.recipeUserId + ); + }, + recoverAccountPOST: async function ({ + webauthnGeneratedOptionsId, + credential, + token, + tenantId, + options, + userContext, + }) { + async function markEmailAsVerified(recipeUserId, email) { + const emailVerificationInstance = recipe_2.default.getInstance(); + if (emailVerificationInstance) { + const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + tenantId, + recipeUserId, + email, + userContext, + } + ); + if (tokenResponse.status === "OK") { + await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + tenantId, + token: tokenResponse.token, + attemptAccountLinking: false, + // we anyway do account linking in this API after this function is + // called. + userContext, + }); + } + } + } + async function doRegisterCredentialAndVerifyEmailAndTryLinkIfNotPrimary(recipeUserId) { + let updateResponse = await options.recipeImplementation.registerCredential({ + recipeUserId, + webauthnGeneratedOptionsId, + credential, + userContext, + }); + // todo decide how to handle these + if (updateResponse.status === "INVALID_AUTHENTICATOR_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + return { + status: "INVALID_AUTHENTICATOR_ERROR", + reason: updateResponse.reason, + }; + } else if (updateResponse.status === "WRONG_CREDENTIALS_ERROR") { + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } else { + // status: "OK" + // If the update was successful, we try to mark the email as verified. + // We do this because we assume that the recover account token was delivered by email (and to the appropriate email address) + // so consuming it means that the user actually has access to the emails we send. + // We only do this if the recover account was successful, otherwise the following scenario is possible: + // 1. User M: signs up using the email of user V with their own credential. They can't validate the email, because it is not their own. + // 2. User A: tries signing up but sees the email already exists message + // 3. User A: recovers the account, but somehow this fails + // If we verified (and linked) the existing user with the original credential, User M would get access to the current user and any linked users. + await markEmailAsVerified(recipeUserId, emailForWhomTokenWasGenerated); + // We refresh the user information here, because the verification status may be updated, which is used during linking. + const updatedUserAfterEmailVerification = await __1.getUser( + recipeUserId.getAsString(), + userContext + ); + if (updatedUserAfterEmailVerification === undefined) { + throw new Error("Should never happen - user deleted after during recover account"); + } + if (updatedUserAfterEmailVerification.isPrimaryUser) { + // If the user is already primary, we do not need to do any linking + return { + status: "OK", + email: emailForWhomTokenWasGenerated, + user: updatedUserAfterEmailVerification, + }; + } + // If the user was not primary: + // Now we try and link the accounts. + // The function below will try and also create a primary user of the new account, this can happen if: + // 1. the user was unverified and linking requires verification + // We do not take try linking by session here, since this is supposed to be called without a session + // Still, the session object is passed around because it is a required input for shouldDoAutomaticAccountLinking + const linkRes = await recipe_1.default.getInstance().tryLinkingByAccountInfoOrCreatePrimaryUser({ + tenantId, + inputUser: updatedUserAfterEmailVerification, + session: undefined, + userContext, + }); + const userAfterWeTriedLinking = + linkRes.status === "OK" ? linkRes.user : updatedUserAfterEmailVerification; + return { + status: "OK", + email: emailForWhomTokenWasGenerated, + user: userAfterWeTriedLinking, + }; + } + } + let tokenConsumptionResponse = await options.recipeImplementation.consumeRecoverAccountToken({ + token, + tenantId, + userContext, + }); + // todo decide how to handle these + if (tokenConsumptionResponse.status === "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR") { + return tokenConsumptionResponse; + } + let userIdForWhomTokenWasGenerated = tokenConsumptionResponse.userId; + let emailForWhomTokenWasGenerated = tokenConsumptionResponse.email; + let existingUser = await __1.getUser(tokenConsumptionResponse.userId, userContext); + if (existingUser === undefined) { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + // Also note that this being undefined doesn't mean that the webauthn + // user does not exist, but it means that there is no recipe or primary user + // for whom the token was generated. + return { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR", + }; + } + // We start by checking if the existingUser is a primary user or not. If it is, + // then we will try and create a new webauthn user and link it to the primary user (if required) + if (existingUser.isPrimaryUser) { + // If this user contains an webauthn account for whom the token was generated, + // then we update that user's credential. + let webauthnUserIsLinkedToExistingUser = + existingUser.loginMethods.find((lm) => { + // we check based on user ID and not email because the only time + // the primary user ID is used for token generation is if the webauthn + // user did not exist - in which case the value of emailPasswordUserExists will + // resolve to false anyway, and that's what we want. + // there is an edge case where if the webauthn recipe user was created + // after the recover account token generation, and it was linked to the + // primary user id (userIdForWhomTokenWasGenerated), in this case, + // we still don't allow credntials update, cause the user should try again + // and the token should be regenerated for the right recipe user. + return ( + lm.recipeUserId.getAsString() === userIdForWhomTokenWasGenerated && + lm.recipeId === "webauthn" + ); + }) !== undefined; + if (webauthnUserIsLinkedToExistingUser) { + return doRegisterCredentialAndVerifyEmailAndTryLinkIfNotPrimary( + new recipeUserId_1.default(userIdForWhomTokenWasGenerated) + ); + } else { + // this means that the existingUser does not have an webauthn user associated + // with it. It could now mean that no webauthn user exists, or it could mean that + // the the webauthn user exists, but it's not linked to the current account. + // If no webauthn user doesn't exists, we will create one, and link it to the existing account. + // If webauthn user exists, then it means there is some race condition cause + // then the token should have been generated for that user instead of the primary user, + // and it shouldn't have come into this branch. So we can simply send a recover account + // invalid error and the user can try again. + // NOTE: We do not ask the dev if we should do account linking or not here + // cause we already have asked them this when generating an recover account reset token. + // In the edge case that the dev changes account linking allowance from true to false + // when it comes here, only a new recipe user id will be created and not linked + // cause createPrimaryUserIdOrLinkAccounts will disallow linking. This doesn't + // really cause any security issue. + let createUserResponse = await options.recipeImplementation.createNewRecipeUser({ + tenantId, + webauthnGeneratedOptionsId, + credential, + userContext, + }); + // todo decide how to handle these + if (createUserResponse.status === "WRONG_CREDENTIALS_ERROR") { + return createUserResponse; + } else if (createUserResponse.status === "INVALID_AUTHENTICATOR_ERROR") { + return createUserResponse; + } else if (createUserResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // this means that the user already existed and we can just return an invalid + // token (see the above comment) + return { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR", + }; + } else { + // we mark the email as verified because recover account also requires + // access to the email to work.. This has a good side effect that + // any other login method with the same email in existingAccount will also get marked + // as verified. + await markEmailAsVerified( + createUserResponse.user.loginMethods[0].recipeUserId, + tokenConsumptionResponse.email + ); + const updatedUser = await __1.getUser(createUserResponse.user.id, userContext); + if (updatedUser === undefined) { + throw new Error("Should never happen - user deleted after during recover account"); + } + createUserResponse.user = updatedUser; + // Now we try and link the accounts. The function below will try and also + // create a primary user of the new account, and if it does that, it's OK.. + // But in most cases, it will end up linking to existing account since the + // email is shared. + // We do not take try linking by session here, since this is supposed to be called without a session + // Still, the session object is passed around because it is a required input for shouldDoAutomaticAccountLinking + const linkRes = await recipe_1.default + .getInstance() + .tryLinkingByAccountInfoOrCreatePrimaryUser({ + tenantId, + inputUser: createUserResponse.user, + session: undefined, + userContext, + }); + const userAfterLinking = linkRes.status === "OK" ? linkRes.user : createUserResponse.user; + if (linkRes.status === "OK" && linkRes.user.id !== existingUser.id) { + // this means that the account we just linked to + // was not the one we had expected to link it to. This can happen + // due to some race condition or the other.. Either way, this + // is not an issue and we can just return OK + } + return { + status: "OK", + email: tokenConsumptionResponse.email, + user: userAfterLinking, + }; + } + } + } else { + // This means that the existing user is not a primary account, which implies that + // it must be a non linked webauthn account. In this case, we simply update the credential. + // Linking to an existing account will be done after the user goes through the email + // verification flow once they log in (if applicable). + return doRegisterCredentialAndVerifyEmailAndTryLinkIfNotPrimary( + new recipeUserId_1.default(userIdForWhomTokenWasGenerated) + ); + } + }, + }; +} +exports.default = getAPIImplementation; diff --git a/lib/build/recipe/webauthn/api/recoverAccount.d.ts b/lib/build/recipe/webauthn/api/recoverAccount.d.ts new file mode 100644 index 000000000..a5038fad1 --- /dev/null +++ b/lib/build/recipe/webauthn/api/recoverAccount.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; +export default function recoverAccount( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/recoverAccount.js b/lib/build/recipe/webauthn/api/recoverAccount.js new file mode 100644 index 000000000..9e63a577b --- /dev/null +++ b/lib/build/recipe/webauthn/api/recoverAccount.js @@ -0,0 +1,66 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const utils_2 = require("./utils"); +const error_1 = __importDefault(require("../error")); +async function recoverAccount(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 + if (apiImplementation.recoverAccountPOST === undefined) { + return false; + } + const requestBody = await options.req.getJSONBody(); + let webauthnGeneratedOptionsId = await utils_2.validatewebauthnGeneratedOptionsIdOrThrowError( + requestBody.webauthnGeneratedOptionsId + ); + let credential = await utils_2.validateCredentialOrThrowError(requestBody.credential); + let token = requestBody.token; + if (token === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the recover account token", + }); + } + if (typeof token !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "The recover account token must be a string", + }); + } + let result = await apiImplementation.recoverAccountPOST({ + webauthnGeneratedOptionsId, + credential, + token, + tenantId, + options, + userContext, + }); + utils_1.send200Response( + options.res, + result.status === "OK" + ? { + status: "OK", + } + : result + ); + return true; +} +exports.default = recoverAccount; diff --git a/lib/build/recipe/webauthn/api/registerOptions.d.ts b/lib/build/recipe/webauthn/api/registerOptions.d.ts new file mode 100644 index 000000000..6f9f603b6 --- /dev/null +++ b/lib/build/recipe/webauthn/api/registerOptions.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function registerOptions( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/registerOptions.js b/lib/build/recipe/webauthn/api/registerOptions.js new file mode 100644 index 000000000..6ea9bf0ec --- /dev/null +++ b/lib/build/recipe/webauthn/api/registerOptions.js @@ -0,0 +1,62 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const error_1 = __importDefault(require("../error")); +async function registerOptions(apiImplementation, tenantId, options, userContext) { + var _a; + if (apiImplementation.registerOptionsPOST === undefined) { + return false; + } + const requestBody = await options.req.getJSONBody(); + let email = (_a = requestBody.email) === null || _a === void 0 ? void 0 : _a.trim(); + let recoverAccountToken = requestBody.recoverAccountToken; + if ( + (email === undefined || typeof email !== "string") && + (recoverAccountToken === undefined || typeof recoverAccountToken !== "string") + ) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the email or the recover account token", + }); + } + // same as for passwordless lib/ts/recipe/passwordless/api/createCode.ts + if (email !== undefined) { + const validateError = await options.config.validateEmailAddress(email, tenantId); + if (validateError !== undefined) { + utils_1.send200Response(options.res, { + status: "INVALID_EMAIL_ERROR", + err: validateError, + }); + return true; + } + } + let result = await apiImplementation.registerOptionsPOST({ + email, + recoverAccountToken, + tenantId, + options, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = registerOptions; diff --git a/lib/build/recipe/webauthn/api/signInOptions.d.ts b/lib/build/recipe/webauthn/api/signInOptions.d.ts new file mode 100644 index 000000000..1e3bc7b5f --- /dev/null +++ b/lib/build/recipe/webauthn/api/signInOptions.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function signInOptions( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/signInOptions.js b/lib/build/recipe/webauthn/api/signInOptions.js new file mode 100644 index 000000000..3c04d2808 --- /dev/null +++ b/lib/build/recipe/webauthn/api/signInOptions.js @@ -0,0 +1,30 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function signInOptions(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.signInOptionsPOST === undefined) { + return false; + } + let result = await apiImplementation.signInOptionsPOST({ + tenantId, + options, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = signInOptions; diff --git a/lib/build/recipe/webauthn/api/signin.d.ts b/lib/build/recipe/webauthn/api/signin.d.ts new file mode 100644 index 000000000..72cd6e46b --- /dev/null +++ b/lib/build/recipe/webauthn/api/signin.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function signInAPI( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/signin.js b/lib/build/recipe/webauthn/api/signin.js new file mode 100644 index 000000000..44cc8bb6f --- /dev/null +++ b/lib/build/recipe/webauthn/api/signin.js @@ -0,0 +1,61 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const utils_2 = require("./utils"); +const authUtils_1 = require("../../../authUtils"); +async function signInAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.signInPOST === undefined) { + return false; + } + const requestBody = await options.req.getJSONBody(); + const webauthnGeneratedOptionsId = await utils_2.validatewebauthnGeneratedOptionsIdOrThrowError( + requestBody.webauthnGeneratedOptionsId + ); + const credential = await utils_2.validateCredentialOrThrowError(requestBody.credential); + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag( + options.req, + requestBody + ); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( + options.req, + options.res, + shouldTryLinkingWithSessionUser, + userContext + ); + if (session !== undefined) { + tenantId = session.getTenantId(); + } + let result = await apiImplementation.signInPOST({ + webauthnGeneratedOptionsId, + credential, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign({ status: "OK" }, utils_1.getBackwardsCompatibleUserInfo(options.req, result, userContext)) + ); + } else { + utils_1.send200Response(options.res, result); + } + return true; +} +exports.default = signInAPI; diff --git a/lib/build/recipe/webauthn/api/signup.d.ts b/lib/build/recipe/webauthn/api/signup.d.ts new file mode 100644 index 000000000..afc748051 --- /dev/null +++ b/lib/build/recipe/webauthn/api/signup.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function signUpAPI( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/webauthn/api/signup.js b/lib/build/recipe/webauthn/api/signup.js new file mode 100644 index 000000000..8eb70b124 --- /dev/null +++ b/lib/build/recipe/webauthn/api/signup.js @@ -0,0 +1,80 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const utils_2 = require("./utils"); +const error_1 = __importDefault(require("../error")); +const authUtils_1 = require("../../../authUtils"); +async function signUpAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.signUpPOST === undefined) { + return false; + } + const requestBody = await options.req.getJSONBody(); + const webauthnGeneratedOptionsId = await utils_2.validatewebauthnGeneratedOptionsIdOrThrowError( + requestBody.webauthnGeneratedOptionsId + ); + const credential = await utils_2.validateCredentialOrThrowError(requestBody.credential); + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag( + options.req, + requestBody + ); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( + options.req, + options.res, + shouldTryLinkingWithSessionUser, + userContext + ); + if (session !== undefined) { + tenantId = session.getTenantId(); + } + let result = await apiImplementation.signUpPOST({ + credential, + webauthnGeneratedOptionsId, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext: userContext, + }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign({ status: "OK" }, utils_1.getBackwardsCompatibleUserInfo(options.req, result, userContext)) + ); + } else if (result.status === "GENERAL_ERROR") { + utils_1.send200Response(options.res, result); + } else if (result.status === "EMAIL_ALREADY_EXISTS_ERROR") { + throw new error_1.default({ + type: error_1.default.FIELD_ERROR, + payload: [ + { + id: "email", + error: "This email already exists. Please sign in instead.", + }, + ], + message: "Error in input formFields", + }); + } else { + utils_1.send200Response(options.res, result); + } + return true; +} +exports.default = signUpAPI; diff --git a/lib/build/recipe/webauthn/api/utils.d.ts b/lib/build/recipe/webauthn/api/utils.d.ts new file mode 100644 index 000000000..8bd411782 --- /dev/null +++ b/lib/build/recipe/webauthn/api/utils.d.ts @@ -0,0 +1,5 @@ +// @ts-nocheck +export declare function validatewebauthnGeneratedOptionsIdOrThrowError( + webauthnGeneratedOptionsId: string +): Promise; +export declare function validateCredentialOrThrowError(credential: T): Promise; diff --git a/lib/build/recipe/webauthn/api/utils.js b/lib/build/recipe/webauthn/api/utils.js new file mode 100644 index 000000000..bfc2cd2e9 --- /dev/null +++ b/lib/build/recipe/webauthn/api/utils.js @@ -0,0 +1,43 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateCredentialOrThrowError = exports.validatewebauthnGeneratedOptionsIdOrThrowError = void 0; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const error_1 = __importDefault(require("../error")); +async function validatewebauthnGeneratedOptionsIdOrThrowError(webauthnGeneratedOptionsId) { + if (webauthnGeneratedOptionsId === undefined) { + throw newBadRequestError("webauthnGeneratedOptionsId is required"); + } + return webauthnGeneratedOptionsId; +} +exports.validatewebauthnGeneratedOptionsIdOrThrowError = validatewebauthnGeneratedOptionsIdOrThrowError; +async function validateCredentialOrThrowError(credential) { + if (credential === undefined) { + throw newBadRequestError("credential is required"); + } + return credential; +} +exports.validateCredentialOrThrowError = validateCredentialOrThrowError; +function newBadRequestError(message) { + return new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message, + }); +} diff --git a/lib/build/recipe/webauthn/constants.d.ts b/lib/build/recipe/webauthn/constants.d.ts new file mode 100644 index 000000000..d58df9d60 --- /dev/null +++ b/lib/build/recipe/webauthn/constants.d.ts @@ -0,0 +1,16 @@ +// @ts-nocheck +export declare const REGISTER_OPTIONS_API = "/webauthn/options/register"; +export declare const SIGNIN_OPTIONS_API = "/webauthn/options/signin"; +export declare const SIGN_UP_API = "/webauthn/signup"; +export declare const SIGN_IN_API = "/webauthn/signin"; +export declare const GENERATE_RECOVER_ACCOUNT_TOKEN_API = "/user/webauthn/reset/token"; +export declare const RECOVER_ACCOUNT_API = "/user/webauthn/reset"; +export declare const SIGNUP_EMAIL_EXISTS_API = "/webauthn/email/exists"; +export declare const DEFAULT_REGISTER_OPTIONS_ATTESTATION = "none"; +export declare const DEFAULT_REGISTER_OPTIONS_REQUIRE_RESIDENT_KEY = false; +export declare const DEFAULT_REGISTER_OPTIONS_RESIDENT_KEY = "required"; +export declare const DEFAULT_REGISTER_OPTIONS_USER_VERIFICATION = "preferred"; +export declare const DEFAULT_REGISTER_OPTIONS_SUPPORTED_ALGORITHM_IDS: number[]; +export declare const DEFAULT_SIGNIN_OPTIONS_USER_VERIFICATION = "preferred"; +export declare const DEFAULT_REGISTER_OPTIONS_TIMEOUT = 5000; +export declare const DEFAULT_SIGNIN_OPTIONS_TIMEOUT = 5000; diff --git a/lib/build/recipe/webauthn/constants.js b/lib/build/recipe/webauthn/constants.js new file mode 100644 index 000000000..2e86a4747 --- /dev/null +++ b/lib/build/recipe/webauthn/constants.js @@ -0,0 +1,33 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_SIGNIN_OPTIONS_TIMEOUT = exports.DEFAULT_REGISTER_OPTIONS_TIMEOUT = exports.DEFAULT_SIGNIN_OPTIONS_USER_VERIFICATION = exports.DEFAULT_REGISTER_OPTIONS_SUPPORTED_ALGORITHM_IDS = exports.DEFAULT_REGISTER_OPTIONS_USER_VERIFICATION = exports.DEFAULT_REGISTER_OPTIONS_RESIDENT_KEY = exports.DEFAULT_REGISTER_OPTIONS_REQUIRE_RESIDENT_KEY = exports.DEFAULT_REGISTER_OPTIONS_ATTESTATION = exports.SIGNUP_EMAIL_EXISTS_API = exports.RECOVER_ACCOUNT_API = exports.GENERATE_RECOVER_ACCOUNT_TOKEN_API = exports.SIGN_IN_API = exports.SIGN_UP_API = exports.SIGNIN_OPTIONS_API = exports.REGISTER_OPTIONS_API = void 0; +exports.REGISTER_OPTIONS_API = "/webauthn/options/register"; +exports.SIGNIN_OPTIONS_API = "/webauthn/options/signin"; +exports.SIGN_UP_API = "/webauthn/signup"; +exports.SIGN_IN_API = "/webauthn/signin"; +exports.GENERATE_RECOVER_ACCOUNT_TOKEN_API = "/user/webauthn/reset/token"; +exports.RECOVER_ACCOUNT_API = "/user/webauthn/reset"; +exports.SIGNUP_EMAIL_EXISTS_API = "/webauthn/email/exists"; +// defaults that can be overridden by the developer +exports.DEFAULT_REGISTER_OPTIONS_ATTESTATION = "none"; +exports.DEFAULT_REGISTER_OPTIONS_REQUIRE_RESIDENT_KEY = false; +exports.DEFAULT_REGISTER_OPTIONS_RESIDENT_KEY = "required"; +exports.DEFAULT_REGISTER_OPTIONS_USER_VERIFICATION = "preferred"; +exports.DEFAULT_REGISTER_OPTIONS_SUPPORTED_ALGORITHM_IDS = [-8, -7, -257]; +exports.DEFAULT_SIGNIN_OPTIONS_USER_VERIFICATION = "preferred"; +exports.DEFAULT_REGISTER_OPTIONS_TIMEOUT = 5000; +exports.DEFAULT_SIGNIN_OPTIONS_TIMEOUT = 5000; diff --git a/lib/build/recipe/webauthn/core-mock.d.ts b/lib/build/recipe/webauthn/core-mock.d.ts new file mode 100644 index 000000000..0bb5666c4 --- /dev/null +++ b/lib/build/recipe/webauthn/core-mock.d.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +import { Querier } from "../../querier"; +export declare const getMockQuerier: (recipeId: string) => Querier; diff --git a/lib/build/recipe/webauthn/core-mock.js b/lib/build/recipe/webauthn/core-mock.js new file mode 100644 index 000000000..723cce9f8 --- /dev/null +++ b/lib/build/recipe/webauthn/core-mock.js @@ -0,0 +1,79 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getMockQuerier = void 0; +const querier_1 = require("../../querier"); +const getMockQuerier = (recipeId) => { + const querier = querier_1.Querier.getNewInstanceOrThrowError(recipeId); + const sendPostRequest = async (path, body, userContext) => { + console.log("body", body); + console.log("userContext", userContext); + if (path.getAsStringDangerous().includes("/recipe/webauthn/options/register")) { + // @ts-ignore + return { + status: "OK", + webauthnGeneratedOptionsId: "7ab03f6a-61b8-4f65-992f-b8b8469bc18f", + rp: { id: "example.com", name: "Example App" }, + user: { id: "dummy-user-id", name: "user@example.com", displayName: "User" }, + challenge: "dummy-challenge", + timeout: 60000, + excludeCredentials: [], + attestation: "none", + pubKeyCredParams: [{ alg: -7, type: "public-key" }], + authenticatorSelection: { + requireResidentKey: false, + residentKey: "preferred", + userVerification: "preferred", + }, + }; + } else if (path.getAsStringDangerous().includes("/recipe/webauthn/options/signin")) { + // @ts-ignore + return { + status: "OK", + webauthnGeneratedOptionsId: "18302759-87c6-4d88-990d-c7cab43653cc", + challenge: "dummy-signin-challenge", + timeout: 60000, + userVerification: "preferred", + }; + // } else if (path.getAsStringDangerous().includes("/recipe/webauthn/user/recover/token")) { + // // @ts-ignore + // return { + // status: "OK", + // token: "dummy-recover-token", + // }; + // } else if (path.getAsStringDangerous().includes("/recipe/webauthn/user/recover/token/consume")) { + // // @ts-ignore + // return { + // status: "OK", + // userId: "dummy-user-id", + // email: "user@example.com", + // }; + // } + } else if (path.getAsStringDangerous().includes("/recipe/webauthn/signup")) { + // @ts-ignore + return { + status: "OK", + user: { + id: "dummy-user-id", + email: "user@example.com", + timeJoined: Date.now(), + }, + recipeUserId: "dummy-recipe-user-id", + }; + } else if (path.getAsStringDangerous().includes("/recipe/webauthn/signin")) { + // @ts-ignore + return { + status: "OK", + user: { + id: "dummy-user-id", + email: "user@example.com", + timeJoined: Date.now(), + }, + recipeUserId: "dummy-recipe-user-id", + }; + } + throw new Error(`Unmocked endpoint: ${path}`); + }; + querier.sendPostRequest = sendPostRequest; + return querier; +}; +exports.getMockQuerier = getMockQuerier; diff --git a/lib/build/recipe/webauthn/error.d.ts b/lib/build/recipe/webauthn/error.d.ts new file mode 100644 index 000000000..d4dc2cf9b --- /dev/null +++ b/lib/build/recipe/webauthn/error.d.ts @@ -0,0 +1,20 @@ +// @ts-nocheck +import STError from "../../error"; +export default class SessionError extends STError { + static FIELD_ERROR: "FIELD_ERROR"; + constructor( + options: + | { + type: "FIELD_ERROR"; + payload: { + id: string; + error: string; + }[]; + message: string; + } + | { + type: "BAD_INPUT_ERROR"; + message: string; + } + ); +} diff --git a/lib/build/recipe/webauthn/error.js b/lib/build/recipe/webauthn/error.js new file mode 100644 index 000000000..9cce55615 --- /dev/null +++ b/lib/build/recipe/webauthn/error.js @@ -0,0 +1,30 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const error_1 = __importDefault(require("../../error")); +class SessionError extends error_1.default { + constructor(options) { + super(Object.assign({}, options)); + this.fromRecipe = "webauthn"; + } +} +exports.default = SessionError; +SessionError.FIELD_ERROR = "FIELD_ERROR"; diff --git a/lib/build/recipe/webauthn/index.d.ts b/lib/build/recipe/webauthn/index.d.ts new file mode 100644 index 000000000..fc33da5b1 --- /dev/null +++ b/lib/build/recipe/webauthn/index.d.ts @@ -0,0 +1,244 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import SuperTokensError from "./error"; +import { RecipeInterface, APIOptions, APIInterface, TypeWebauthnEmailDeliveryInput, CredentialPayload } from "./types"; +import RecipeUserId from "../../recipeUserId"; +import { SessionContainerInterface } from "../session/types"; +import { User } from "../../types"; +export default class Wrapper { + static init: typeof Recipe.init; + static Error: typeof SuperTokensError; + static registerOptions( + email: string | undefined, + recoverAccountToken: string | undefined, + relyingPartyId: string, + relyingPartyName: string, + origin: string, + timeout: number, + attestation: "none" | "indirect" | "direct" | "enterprise" | undefined, + tenantId: string, + userContext: Record + ): Promise< + | { + status: "OK"; + webauthnGeneratedOptionsId: string; + rp: { + id: string; + name: string; + }; + user: { + id: string; + name: string; + displayName: string; + }; + challenge: string; + timeout: number; + excludeCredentials: { + id: string; + type: "public-key"; + transports: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[]; + }[]; + attestation: "none" | "indirect" | "direct" | "enterprise"; + pubKeyCredParams: { + alg: number; + type: "public-key"; + }[]; + authenticatorSelection: { + requireResidentKey: boolean; + residentKey: "required" | "preferred" | "discouraged"; + userVerification: "required" | "preferred" | "discouraged"; + }; + } + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + | { + status: "INVALID_EMAIL_ERROR"; + err: string; + } + >; + static signInOptions( + relyingPartyId: string, + origin: string, + timeout: number, + tenantId: string, + userContext: Record + ): Promise< + | { + status: "OK"; + webauthnGeneratedOptionsId: string; + challenge: string; + timeout: number; + userVerification: "required" | "preferred" | "discouraged"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >; + static signIn( + tenantId: string, + webauthnGeneratedOptionsId: string, + credential: CredentialPayload, + session?: undefined, + userContext?: Record + ): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >; + static signIn( + tenantId: string, + webauthnGeneratedOptionsId: string, + credential: CredentialPayload, + session: SessionContainerInterface, + userContext?: Record + ): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "LINKING_TO_SESSION_USER_FAILED"; + reason: + | "EMAIL_VERIFICATION_REQUIRED" + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + } + >; + static verifyCredentials( + tenantId: string, + webauthnGeneratedOptionsId: string, + credential: CredentialPayload, + userContext?: Record + ): Promise< + | { + status: "OK"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >; + /** + * We do not make email optional here cause we want to + * allow passing in primaryUserId. If we make email optional, + * and if the user provides a primaryUserId, then it may result in two problems: + * - there is no recipeUserId = input primaryUserId, in this case, + * this function will throw an error + * - There is a recipe userId = input primaryUserId, but that recipe has no email, + * or has wrong email compared to what the user wanted to generate a reset token for. + * + * And we want to allow primaryUserId being passed in. + */ + static generateRecoverAccountToken( + tenantId: string, + userId: string, + email: string, + userContext?: Record + ): Promise< + | { + status: "OK"; + token: string; + } + | { + status: "UNKNOWN_USER_ID_ERROR"; + } + >; + static recoverAccount( + tenantId: string, + webauthnGeneratedOptionsId: string, + token: string, + credential: CredentialPayload, + userContext?: Record + ): Promise< + | { + status: "OK" | "WRONG_CREDENTIALS_ERROR" | "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + failureReason: string; + } + >; + static consumeRecoverAccountToken( + tenantId: string, + token: string, + userContext?: Record + ): Promise< + | { + status: "OK"; + email: string; + userId: string; + } + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + >; + static registerCredential(input: { + recipeUserId: RecipeUserId; + tenantId: string; + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + userContext?: Record; + }): Promise< + | { + status: "OK" | "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + reason: string; + } + >; + static createRecoverAccountLink( + tenantId: string, + userId: string, + email: string, + userContext?: Record + ): Promise< + | { + status: "OK"; + link: string; + } + | { + status: "UNKNOWN_USER_ID_ERROR"; + } + >; + static sendRecoverAccountEmail( + tenantId: string, + userId: string, + email: string, + userContext?: Record + ): Promise<{ + status: "OK" | "UNKNOWN_USER_ID_ERROR"; + }>; + static sendEmail( + input: TypeWebauthnEmailDeliveryInput & { + userContext?: Record; + } + ): Promise; +} +export declare let init: typeof Recipe.init; +export declare let Error: typeof SuperTokensError; +export declare let registerOptions: typeof Wrapper.registerOptions; +export declare let signInOptions: typeof Wrapper.signInOptions; +export declare let signIn: typeof Wrapper.signIn; +export declare let verifyCredentials: typeof Wrapper.verifyCredentials; +export declare let generateRecoverAccountToken: typeof Wrapper.generateRecoverAccountToken; +export declare let recoverAccount: typeof Wrapper.recoverAccount; +export declare let consumeRecoverAccountToken: typeof Wrapper.consumeRecoverAccountToken; +export declare let registerCredential: typeof Wrapper.registerCredential; +export type { RecipeInterface, APIOptions, APIInterface }; +export declare let createRecoverAccountLink: typeof Wrapper.createRecoverAccountLink; +export declare let sendRecoverAccountEmail: typeof Wrapper.sendRecoverAccountEmail; +export declare let sendEmail: typeof Wrapper.sendEmail; diff --git a/lib/build/recipe/webauthn/index.js b/lib/build/recipe/webauthn/index.js new file mode 100644 index 000000000..041a297b2 --- /dev/null +++ b/lib/build/recipe/webauthn/index.js @@ -0,0 +1,228 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sendEmail = exports.sendRecoverAccountEmail = exports.createRecoverAccountLink = exports.registerCredential = exports.consumeRecoverAccountToken = exports.recoverAccount = exports.generateRecoverAccountToken = exports.verifyCredentials = exports.signIn = exports.signInOptions = exports.registerOptions = exports.Error = exports.init = void 0; +const recipe_1 = __importDefault(require("./recipe")); +const error_1 = __importDefault(require("./error")); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const constants_1 = require("../multitenancy/constants"); +const utils_1 = require("./utils"); +const __1 = require("../.."); +const utils_2 = require("../../utils"); +const constants_2 = require("./constants"); +class Wrapper { + static async registerOptions( + email, + recoverAccountToken, + relyingPartyId, + relyingPartyName, + origin, + timeout, + attestation = "none", + tenantId, + userContext + ) { + let payload = email ? { email } : recoverAccountToken ? { recoverAccountToken } : null; + if (!payload) { + return { status: "INVALID_EMAIL_ERROR", err: "Email is missing" }; + } + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.registerOptions( + Object.assign( + Object.assign( + { + requireResidentKey: constants_2.DEFAULT_REGISTER_OPTIONS_REQUIRE_RESIDENT_KEY, + residentKey: constants_2.DEFAULT_REGISTER_OPTIONS_RESIDENT_KEY, + userVerification: constants_2.DEFAULT_REGISTER_OPTIONS_USER_VERIFICATION, + supportedAlgorithmIds: constants_2.DEFAULT_REGISTER_OPTIONS_SUPPORTED_ALGORITHM_IDS, + }, + payload + ), + { + relyingPartyId, + relyingPartyName, + origin, + timeout, + attestation, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext: utils_2.getUserContext(userContext), + } + ) + ); + } + static signInOptions(relyingPartyId, origin, timeout, tenantId, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signInOptions({ + userVerification: constants_2.DEFAULT_SIGNIN_OPTIONS_USER_VERIFICATION, + relyingPartyId, + origin, + timeout, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext: utils_2.getUserContext(userContext), + }); + } + static signIn(tenantId, webauthnGeneratedOptionsId, credential, session, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signIn({ + webauthnGeneratedOptionsId, + credential, + session, + shouldTryLinkingWithSessionUser: !!session, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext: utils_2.getUserContext(userContext), + }); + } + static async verifyCredentials(tenantId, webauthnGeneratedOptionsId, credential, userContext) { + const resp = await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyCredentials({ + webauthnGeneratedOptionsId, + credential, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext: utils_2.getUserContext(userContext), + }); + // Here we intentionally skip the user and recipeUserId props, because we do not want apps to accidentally use this to sign in + return { + status: resp.status, + }; + } + /** + * We do not make email optional here cause we want to + * allow passing in primaryUserId. If we make email optional, + * and if the user provides a primaryUserId, then it may result in two problems: + * - there is no recipeUserId = input primaryUserId, in this case, + * this function will throw an error + * - There is a recipe userId = input primaryUserId, but that recipe has no email, + * or has wrong email compared to what the user wanted to generate a reset token for. + * + * And we want to allow primaryUserId being passed in. + */ + static generateRecoverAccountToken(tenantId, userId, email, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.generateRecoverAccountToken({ + userId, + email, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext: utils_2.getUserContext(userContext), + }); + } + static async recoverAccount(tenantId, webauthnGeneratedOptionsId, token, credential, userContext) { + const consumeResp = await Wrapper.consumeRecoverAccountToken(tenantId, token, userContext); + if (consumeResp.status !== "OK") { + return consumeResp; + } + let result = await Wrapper.registerCredential({ + recipeUserId: new recipeUserId_1.default(consumeResp.userId), + webauthnGeneratedOptionsId, + credential, + tenantId, + userContext, + }); + if (result.status === "INVALID_AUTHENTICATOR_ERROR") { + return { + status: "INVALID_AUTHENTICATOR_ERROR", + failureReason: result.reason, + }; + } + return { + status: result.status, + }; + } + static consumeRecoverAccountToken(tenantId, token, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeRecoverAccountToken({ + token, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext: utils_2.getUserContext(userContext), + }); + } + static registerCredential(input) { + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.registerCredential( + Object.assign(Object.assign({}, input), { userContext: utils_2.getUserContext(input.userContext) }) + ); + } + static async createRecoverAccountLink(tenantId, userId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); + let token = await this.generateRecoverAccountToken(tenantId, userId, email, ctx); + if (token.status === "UNKNOWN_USER_ID_ERROR") { + return token; + } + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return { + status: "OK", + link: utils_1.getRecoverAccountLink({ + appInfo: recipeInstance.getAppInfo(), + token: token.token, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + request: __1.getRequestFromUserContext(ctx), + userContext: ctx, + }), + }; + } + static async sendRecoverAccountEmail(tenantId, userId, email, userContext) { + const user = await __1.getUser(userId, userContext); + if (!user) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + const loginMethod = user.loginMethods.find((m) => m.recipeId === "webauthn" && m.hasSameEmailAs(email)); + if (!loginMethod) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + let link = await this.createRecoverAccountLink(tenantId, userId, email, userContext); + if (link.status === "UNKNOWN_USER_ID_ERROR") { + return link; + } + await exports.sendEmail({ + recoverAccountLink: link.link, + type: "RECOVER_ACCOUNT", + user: { + id: user.id, + recipeUserId: loginMethod.recipeUserId, + email: loginMethod.email, + }, + tenantId, + userContext, + }); + return { + status: "OK", + }; + } + static async sendEmail(input) { + let recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { + tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, + userContext: utils_2.getUserContext(input.userContext), + }) + ); + } +} +exports.default = Wrapper; +Wrapper.init = recipe_1.default.init; +Wrapper.Error = error_1.default; +exports.init = Wrapper.init; +exports.Error = Wrapper.Error; +exports.registerOptions = Wrapper.registerOptions; +exports.signInOptions = Wrapper.signInOptions; +exports.signIn = Wrapper.signIn; +exports.verifyCredentials = Wrapper.verifyCredentials; +exports.generateRecoverAccountToken = Wrapper.generateRecoverAccountToken; +exports.recoverAccount = Wrapper.recoverAccount; +exports.consumeRecoverAccountToken = Wrapper.consumeRecoverAccountToken; +exports.registerCredential = Wrapper.registerCredential; +exports.createRecoverAccountLink = Wrapper.createRecoverAccountLink; +exports.sendRecoverAccountEmail = Wrapper.sendRecoverAccountEmail; +exports.sendEmail = Wrapper.sendEmail; diff --git a/lib/build/recipe/webauthn/recipe.d.ts b/lib/build/recipe/webauthn/recipe.d.ts new file mode 100644 index 000000000..12c1bf86c --- /dev/null +++ b/lib/build/recipe/webauthn/recipe.d.ts @@ -0,0 +1,43 @@ +// @ts-nocheck +import RecipeModule from "../../recipeModule"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; +import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction, UserContext } from "../../types"; +import STError from "./error"; +import NormalisedURLPath from "../../normalisedURLPath"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; +import { TypeWebauthnEmailDeliveryInput } from "./types"; +export default class Recipe extends RecipeModule { + private static instance; + static RECIPE_ID: string; + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + emailDelivery: EmailDeliveryIngredient; + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput | undefined, + ingredients: { + emailDelivery: EmailDeliveryIngredient | undefined; + } + ); + static getInstanceOrThrowError(): Recipe; + static init(config?: TypeInput): RecipeListFunction; + static reset(): void; + getAPIsHandled: () => APIHandled[]; + handleAPIRequest: ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ) => Promise; + handleError: (err: STError, _request: BaseRequest, response: BaseResponse) => Promise; + getAllCORSHeaders: () => string[]; + isErrorFromThisRecipe: (err: any) => err is STError; +} diff --git a/lib/build/recipe/webauthn/recipe.js b/lib/build/recipe/webauthn/recipe.js new file mode 100644 index 000000000..f3a82ef7c --- /dev/null +++ b/lib/build/recipe/webauthn/recipe.js @@ -0,0 +1,313 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipeModule_1 = __importDefault(require("../../recipeModule")); +const error_1 = __importDefault(require("./error")); +const utils_1 = require("./utils"); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const constants_1 = require("./constants"); +const signup_1 = __importDefault(require("./api/signup")); +const signin_1 = __importDefault(require("./api/signin")); +const registerOptions_1 = __importDefault(require("./api/registerOptions")); +const signInOptions_1 = __importDefault(require("./api/signInOptions")); +const generateRecoverAccountToken_1 = __importDefault(require("./api/generateRecoverAccountToken")); +const recoverAccount_1 = __importDefault(require("./api/recoverAccount")); +const emailExists_1 = __importDefault(require("./api/emailExists")); +const utils_2 = require("../../utils"); +const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); +const implementation_1 = __importDefault(require("./api/implementation")); +const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); +const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); +const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const recipe_1 = __importDefault(require("../multifactorauth/recipe")); +const recipe_2 = __importDefault(require("../multitenancy/recipe")); +const utils_3 = require("../thirdparty/utils"); +const multifactorauth_1 = require("../multifactorauth"); +const core_mock_1 = require("./core-mock"); +class Recipe extends recipeModule_1.default { + constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { + super(recipeId, appInfo); + // abstract instance functions below............... + this.getAPIsHandled = () => { + return [ + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.REGISTER_OPTIONS_API), + id: constants_1.REGISTER_OPTIONS_API, + disabled: this.apiImpl.registerOptionsPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGNIN_OPTIONS_API), + id: constants_1.SIGNIN_OPTIONS_API, + disabled: this.apiImpl.signInOptionsPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_UP_API), + id: constants_1.SIGN_UP_API, + disabled: this.apiImpl.signUpPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_IN_API), + id: constants_1.SIGN_IN_API, + disabled: this.apiImpl.signInPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default( + constants_1.GENERATE_RECOVER_ACCOUNT_TOKEN_API + ), + id: constants_1.GENERATE_RECOVER_ACCOUNT_TOKEN_API, + disabled: this.apiImpl.generateRecoverAccountTokenPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.RECOVER_ACCOUNT_API), + id: constants_1.RECOVER_ACCOUNT_API, + disabled: this.apiImpl.recoverAccountPOST === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGNUP_EMAIL_EXISTS_API), + id: constants_1.SIGNUP_EMAIL_EXISTS_API, + disabled: this.apiImpl.emailExistsGET === undefined, + }, + ]; + }; + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.REGISTER_OPTIONS_API) { + return await registerOptions_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.SIGNIN_OPTIONS_API) { + return await signInOptions_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.SIGN_UP_API) { + return await signup_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.SIGN_IN_API) { + return await signin_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.GENERATE_RECOVER_ACCOUNT_TOKEN_API) { + return await generateRecoverAccountToken_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.RECOVER_ACCOUNT_API) { + return await recoverAccount_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.SIGNUP_EMAIL_EXISTS_API) { + return await emailExists_1.default(this.apiImpl, tenantId, options, userContext); + } else return false; + }; + this.handleError = async (err, _request, response) => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + if (err.type === error_1.default.FIELD_ERROR) { + return utils_2.send200Response(response, { + status: "FIELD_ERROR", + formFields: err.payload, + }); + } else { + throw err; + } + } else { + throw err; + } + }; + this.getAllCORSHeaders = () => { + return []; + }; + this.isErrorFromThisRecipe = (err) => { + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + }; + this.isInServerlessEnv = isInServerlessEnv; + this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); + { + const getWebauthnConfig = () => this.config; + // const querier = Querier.getNewInstanceOrThrowError(recipeId); + const querier = core_mock_1.getMockQuerier(recipeId); + let builder = new supertokens_js_override_1.default( + recipeImplementation_1.default(querier, getWebauthnConfig) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new supertokens_js_override_1.default(implementation_1.default()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + /** + * emailDelivery will always needs to be declared after isInServerlessEnv + * and recipeInterfaceImpl values are set + */ + this.emailDelivery = + ingredients.emailDelivery === undefined + ? new emaildelivery_1.default(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) + : ingredients.emailDelivery; + // todo check correctness + postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = recipe_1.default.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addFuncToGetAllAvailableSecondaryFactorIdsFromOtherRecipes(() => { + return ["webauthn"]; + }); + mfaInstance.addFuncToGetFactorsSetupForUserFromOtherRecipes(async (user) => { + for (const loginMethod of user.loginMethods) { + // We don't check for tenantId here because if we find the user + // with emailpassword loginMethod from different tenant, then + // we assume the factor is setup for this user. And as part of factor + // completion, we associate that loginMethod with the session's tenantId + if (loginMethod.recipeId === Recipe.RECIPE_ID) { + return ["webauthn"]; + } + } + return []; + }); + mfaInstance.addFuncToGetEmailsForFactorFromOtherRecipes((user, sessionRecipeUserId) => { + // This function is called in the MFA info endpoint API. + // Based on https://github.com/supertokens/supertokens-node/pull/741#discussion_r1432749346 + // preparing some reusable variables for the logic below... + let sessionLoginMethod = user.loginMethods.find((lM) => { + return lM.recipeUserId.getAsString() === sessionRecipeUserId.getAsString(); + }); + if (sessionLoginMethod === undefined) { + // this can happen maybe cause this login method + // was unlinked from the user or deleted entirely... + return { + status: "UNKNOWN_SESSION_RECIPE_USER_ID", + }; + } + // We order the login methods based on timeJoined (oldest first) + const orderedLoginMethodsByTimeJoinedOldestFirst = user.loginMethods.sort((a, b) => { + return a.timeJoined - b.timeJoined; + }); + // Then we take the ones that belong to this recipe + const recipeLoginMethodsOrderedByTimeJoinedOldestFirst = orderedLoginMethodsByTimeJoinedOldestFirst.filter( + (lm) => lm.recipeId === Recipe.RECIPE_ID + ); + let result; + if (recipeLoginMethodsOrderedByTimeJoinedOldestFirst.length !== 0) { + // If there are login methods belonging to this recipe, the factor is set up + // In this case we only list email addresses that have a password associated with them + result = [ + // First we take the verified real emails associated with emailpassword login methods ordered by timeJoined (oldest first) + ...recipeLoginMethodsOrderedByTimeJoinedOldestFirst + .filter((lm) => !utils_3.isFakeEmail(lm.email) && lm.verified === true) + .map((lm) => lm.email), + // Then we take the non-verified real emails associated with emailpassword login methods ordered by timeJoined (oldest first) + ...recipeLoginMethodsOrderedByTimeJoinedOldestFirst + .filter((lm) => !utils_3.isFakeEmail(lm.email) && lm.verified === false) + .map((lm) => lm.email), + // Lastly, fake emails associated with emailpassword login methods ordered by timeJoined (oldest first) + // We also add these into the list because they already have a password added to them so they can be a valid choice when signing in + // We do not want to remove the previously added "MFA password", because a new email password user was linked + // E.g.: + // 1. A discord user adds a password for MFA (which will use the fake email associated with the discord user) + // 2. Later they also sign up and (manually) link a full emailpassword user that they intend to use as a first factor + // 3. The next time they sign in using Discord, they could be asked for a secondary password. + // In this case, they'd be checked against the first user that they originally created for MFA, not the one later linked to the account + ...recipeLoginMethodsOrderedByTimeJoinedOldestFirst + .filter((lm) => utils_3.isFakeEmail(lm.email)) + .map((lm) => lm.email), + ]; + // We handle moving the session email to the top of the list later + } else { + // This factor hasn't been set up, we list all emails belonging to the user + if ( + orderedLoginMethodsByTimeJoinedOldestFirst.some( + (lm) => lm.email !== undefined && !utils_3.isFakeEmail(lm.email) + ) + ) { + // If there is at least one real email address linked to the user, we only suggest real addresses + result = orderedLoginMethodsByTimeJoinedOldestFirst + .filter((lm) => lm.email !== undefined && !utils_3.isFakeEmail(lm.email)) + .map((lm) => lm.email); + } else { + // Else we use the fake ones + result = orderedLoginMethodsByTimeJoinedOldestFirst + .filter((lm) => lm.email !== undefined && utils_3.isFakeEmail(lm.email)) + .map((lm) => lm.email); + } + // We handle moving the session email to the top of the list later + // Since in this case emails are not guaranteed to be unique, we de-duplicate the results, keeping the oldest one in the list. + // The Set constructor keeps the original insertion order (OrderedByTimeJoinedOldestFirst), but de-duplicates the items, + // keeping the first one added (so keeping the older one if there are two entries with the same email) + // e.g.: [4,2,3,2,1] -> [4,2,3,1] + result = Array.from(new Set(result)); + } + // If the loginmethod associated with the session has an email address, we move it to the top of the list (if it's already in the list) + if (sessionLoginMethod.email !== undefined && result.includes(sessionLoginMethod.email)) { + result = [ + sessionLoginMethod.email, + ...result.filter((email) => email !== sessionLoginMethod.email), + ]; + } + // If the list is empty we generate an email address to make the flow where the user is never asked for + // an email address easier to implement. In many cases when the user adds an email-password factor, they + // actually only want to add a password and do not care about the associated email address. + // Custom implementations can choose to ignore this, and ask the user for the email anyway. + if (result.length === 0) { + result.push(`${sessionRecipeUserId.getAsString()}@stfakeemail.supertokens.com`); + } + return { + status: "OK", + factorIdToEmailsMap: { + webauthn: result, + }, + }; + }); + } + const mtRecipe = recipe_2.default.getInstance(); + if (mtRecipe !== undefined) { + mtRecipe.allAvailableFirstFactors.push(multifactorauth_1.FactorIds.WEBAUTHN); + } + }); + } + static getInstanceOrThrowError() { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the Webauthn.init function?"); + } + static init(config) { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { + emailDelivery: undefined, + }); + return Recipe.instance; + } else { + throw new Error("Webauthn recipe has already been initialised. Please check your code for bugs."); + } + }; + } + static reset() { + if (!utils_2.isTestEnv()) { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } +} +exports.default = Recipe; +Recipe.instance = undefined; +Recipe.RECIPE_ID = "webauthn"; diff --git a/lib/build/recipe/webauthn/recipeImplementation.d.ts b/lib/build/recipe/webauthn/recipeImplementation.d.ts new file mode 100644 index 000000000..b6421a667 --- /dev/null +++ b/lib/build/recipe/webauthn/recipeImplementation.d.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +import { RecipeInterface, TypeNormalisedInput } from "./types"; +import { Querier } from "../../querier"; +export default function getRecipeInterface( + querier: Querier, + getWebauthnConfig: () => TypeNormalisedInput +): RecipeInterface; diff --git a/lib/build/recipe/webauthn/recipeImplementation.js b/lib/build/recipe/webauthn/recipeImplementation.js new file mode 100644 index 000000000..f664098c1 --- /dev/null +++ b/lib/build/recipe/webauthn/recipeImplementation.js @@ -0,0 +1,404 @@ +"use strict"; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + } + : function (o, v) { + o["default"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +var __rest = + (this && this.__rest) || + function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; + } + return t; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const __1 = require("../.."); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const constants_1 = require("../multitenancy/constants"); +const user_1 = require("../../user"); +const authUtils_1 = require("../../authUtils"); +const jose = __importStar(require("jose")); +function getRecipeInterface(querier, getWebauthnConfig) { + return { + registerOptions: async function (_a) { + var { + relyingPartyId, + relyingPartyName, + origin, + timeout, + attestation = "none", + tenantId, + userContext, + supportedAlgorithmIds, + } = _a, + rest = __rest(_a, [ + "relyingPartyId", + "relyingPartyName", + "origin", + "timeout", + "attestation", + "tenantId", + "userContext", + "supportedAlgorithmIds", + ]); + const emailInput = "email" in rest ? rest.email : undefined; + const recoverAccountTokenInput = "recoverAccountToken" in rest ? rest.recoverAccountToken : undefined; + let email; + if (emailInput !== undefined) { + email = emailInput; + } else if (recoverAccountTokenInput !== undefined) { + // todo check if should decode using Core or using sdk; atm decided on usinng the sdk so to not make another roundtrip to the server + // the actual verification of the token will be done during consumeRecoverAccountToken + let decoded; + try { + decoded = await jose.decodeJwt(recoverAccountTokenInput); + } catch (e) { + console.error(e); + return { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR", + }; + } + email = decoded === null || decoded === void 0 ? void 0 : decoded.email; + } + if (!email) { + return { + status: "INVALID_EMAIL_ERROR", + err: "The email is missing", + }; + } + const err = await getWebauthnConfig().validateEmailAddress(email, tenantId); + if (err) { + return { + status: "INVALID_EMAIL_ERROR", + err, + }; + } + return await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/options/register` + ), + { + email, + relyingPartyName, + relyingPartyId, + origin, + timeout, + attestation, + supportedAlgorithmIds, + }, + userContext + ); + }, + signInOptions: async function ({ relyingPartyId, origin, timeout, tenantId, userContext }) { + // the input user ID can be a recipe or a primary user ID. + return await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/options/signin` + ), + { + relyingPartyId, + origin, + timeout, + }, + userContext + ); + }, + signUp: async function ({ + webauthnGeneratedOptionsId, + credential, + tenantId, + session, + shouldTryLinkingWithSessionUser, + userContext, + }) { + const response = await this.createNewRecipeUser({ + credential, + webauthnGeneratedOptionsId, + tenantId, + userContext, + }); + if (response.status !== "OK") { + return response; + } + let updatedUser = response.user; + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( + { + tenantId, + inputUser: response.user, + recipeUserId: response.recipeUserId, + session, + shouldTryLinkingWithSessionUser, + userContext, + } + ); + if (linkResult.status != "OK") { + return linkResult; + } + updatedUser = linkResult.user; + return { + status: "OK", + user: updatedUser, + recipeUserId: response.recipeUserId, + }; + }, + signIn: async function ({ + credential, + webauthnGeneratedOptionsId, + tenantId, + session, + shouldTryLinkingWithSessionUser, + userContext, + }) { + const response = await this.verifyCredentials({ + credential, + webauthnGeneratedOptionsId, + tenantId, + userContext, + }); + if (response.status !== "OK") { + return response; + } + const loginMethod = response.user.loginMethods.find( + (lm) => lm.recipeUserId.getAsString() === response.recipeUserId.getAsString() + ); + if (!loginMethod.verified) { + await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, + }); + // Unlike in the sign up recipe function, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API - + // for example in their update password API. If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. + // We do this so that we get the updated user (in case the above + // function updated the verification status) and can return that + response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); + } + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( + { + tenantId, + inputUser: response.user, + recipeUserId: response.recipeUserId, + session, + shouldTryLinkingWithSessionUser, + userContext, + } + ); + if (linkResult.status === "LINKING_TO_SESSION_USER_FAILED") { + return linkResult; + } + response.user = linkResult.user; + return response; + }, + verifyCredentials: async function ({ credential, webauthnGeneratedOptionsId, tenantId, userContext }) { + const response = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId}/recipe/webauthn/signin` + ), + { + credential, + webauthnGeneratedOptionsId, + }, + userContext + ); + if (response.status === "OK") { + return { + status: "OK", + user: new user_1.User(response.user), + recipeUserId: new recipeUserId_1.default(response.recipeUserId), + }; + } + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + }, + createNewRecipeUser: async function (input) { + const resp = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId + }/recipe/webauthn/signup` + ), + { + webauthnGeneratedOptionsId: input.webauthnGeneratedOptionsId, + credential: input.credential, + }, + input.userContext + ); + if (resp.status === "OK") { + return { + status: "OK", + user: new user_1.User(resp.user), + recipeUserId: new recipeUserId_1.default(resp.recipeUserId), + }; + } + return resp; + }, + generateRecoverAccountToken: async function ({ userId, email, tenantId, userContext }) { + // the input user ID can be a recipe or a primary user ID. + return await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/user/recover/token` + ), + { + userId, + email, + }, + userContext + ); + }, + consumeRecoverAccountToken: async function ({ token, tenantId, userContext }) { + return await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/user/recover/token/consume` + ), + { + token, + }, + userContext + ); + }, + registerCredential: async function ({ webauthnGeneratedOptionsId, credential, userContext, recipeUserId }) { + return await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/webauthn/user/${recipeUserId}/credential/register`), + { + webauthnGeneratedOptionsId, + credential, + }, + userContext + ); + }, + decodeCredential: async function ({ credential, userContext }) { + const response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/webauthn/credential/decode`), + { + credential, + }, + userContext + ); + if (response.status === "OK") { + return response; + } + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + }, + getUserFromRecoverAccountToken: async function ({ token, tenantId, userContext }) { + return await querier.sendGetRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/user/recover/token/${token}` + ), + {}, + userContext + ); + }, + removeCredential: async function ({ webauthnCredentialId, recipeUserId, userContext }) { + return await querier.sendDeleteRequest( + new normalisedURLPath_1.default( + `/recipe/webauthn/user/${recipeUserId}/credential/${webauthnCredentialId}` + ), + {}, + {}, + userContext + ); + }, + getCredential: async function ({ webauthnCredentialId, recipeUserId, userContext }) { + return await querier.sendGetRequest( + new normalisedURLPath_1.default( + `/recipe/webauthn/user/${recipeUserId}/credential/${webauthnCredentialId}` + ), + {}, + userContext + ); + }, + listCredentials: async function ({ recipeUserId, userContext }) { + return await querier.sendGetRequest( + new normalisedURLPath_1.default(`/recipe/webauthn/user/${recipeUserId}/credential/list`), + {}, + userContext + ); + }, + removeGeneratedOptions: async function ({ webauthnGeneratedOptionsId, tenantId, userContext }) { + return await querier.sendDeleteRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/options/${webauthnGeneratedOptionsId}` + ), + {}, + {}, + userContext + ); + }, + getGeneratedOptions: async function ({ webauthnGeneratedOptionsId, tenantId, userContext }) { + return await querier.sendGetRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/webauthn/options/${webauthnGeneratedOptionsId}` + ), + {}, + userContext + ); + }, + }; +} +exports.default = getRecipeInterface; diff --git a/lib/build/recipe/webauthn/types.d.ts b/lib/build/recipe/webauthn/types.d.ts new file mode 100644 index 000000000..ff545f52b --- /dev/null +++ b/lib/build/recipe/webauthn/types.d.ts @@ -0,0 +1,673 @@ +// @ts-nocheck +import type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; +import { SessionContainerInterface } from "../session/types"; +import { + TypeInput as EmailDeliveryTypeInput, + TypeInputWithService as EmailDeliveryTypeInputWithService, +} from "../../ingredients/emaildelivery/types"; +import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; +import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; +import RecipeUserId from "../../recipeUserId"; +export declare type TypeNormalisedInput = { + relyingPartyId: TypeNormalisedInputRelyingPartyId; + relyingPartyName: TypeNormalisedInputRelyingPartyName; + getOrigin: TypeNormalisedInputGetOrigin; + getEmailDeliveryConfig: ( + isInServerlessEnv: boolean + ) => EmailDeliveryTypeInputWithService; + validateEmailAddress: TypeNormalisedInputValidateEmailAddress; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type TypeNormalisedInputRelyingPartyId = (input: { + tenantId: string; + request: BaseRequest | undefined; + userContext: UserContext; +}) => Promise; +export declare type TypeNormalisedInputRelyingPartyName = (input: { + tenantId: string; + userContext: UserContext; +}) => Promise; +export declare type TypeNormalisedInputGetOrigin = (input: { + tenantId: string; + request: BaseRequest; + userContext: UserContext; +}) => Promise; +export declare type TypeNormalisedInputValidateEmailAddress = ( + email: string, + tenantId: string +) => Promise | string | undefined; +export declare type TypeInput = { + emailDelivery?: EmailDeliveryTypeInput; + relyingPartyId?: TypeInputRelyingPartyId; + relyingPartyName?: TypeInputRelyingPartyName; + validateEmailAddress?: TypeInputValidateEmailAddress; + getOrigin?: TypeInputGetOrigin; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type TypeInputRelyingPartyId = + | string + | ((input: { tenantId: string; request: BaseRequest | undefined; userContext: UserContext }) => Promise); +export declare type TypeInputRelyingPartyName = + | string + | ((input: { tenantId: string; userContext: UserContext }) => Promise); +export declare type TypeInputGetOrigin = (input: { + tenantId: string; + request: BaseRequest; + userContext: UserContext; +}) => Promise; +export declare type TypeInputValidateEmailAddress = ( + email: string, + tenantId: string +) => Promise | string | undefined; +declare type Base64URLString = string; +export declare type RecipeInterface = { + registerOptions( + input: { + relyingPartyId: string; + relyingPartyName: string; + origin: string; + requireResidentKey: boolean | undefined; + residentKey: "required" | "preferred" | "discouraged" | undefined; + userVerification: "required" | "preferred" | "discouraged" | undefined; + attestation: "none" | "indirect" | "direct" | "enterprise" | undefined; + supportedAlgorithmIds: number[] | undefined; + timeout: number | undefined; + tenantId: string; + userContext: UserContext; + } & ( + | { + recoverAccountToken: string; + } + | { + email: string; + } + ) + ): Promise< + | { + status: "OK"; + webauthnGeneratedOptionsId: string; + rp: { + id: string; + name: string; + }; + user: { + id: string; + name: string; + displayName: string; + }; + challenge: string; + timeout: number; + excludeCredentials: { + id: string; + type: "public-key"; + transports: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[]; + }[]; + attestation: "none" | "indirect" | "direct" | "enterprise"; + pubKeyCredParams: { + alg: number; + type: "public-key"; + }[]; + authenticatorSelection: { + requireResidentKey: boolean; + residentKey: "required" | "preferred" | "discouraged"; + userVerification: "required" | "preferred" | "discouraged"; + }; + } + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + | { + status: "INVALID_EMAIL_ERROR"; + err: string; + } + >; + signInOptions(input: { + email?: string; + relyingPartyId: string; + origin: string; + userVerification: "required" | "preferred" | "discouraged" | undefined; + timeout: number | undefined; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + webauthnGeneratedOptionsId: string; + challenge: string; + timeout: number; + userVerification: "required" | "preferred" | "discouraged"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >; + signUp(input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + reason: string; + } + | { + status: "LINKING_TO_SESSION_USER_FAILED"; + reason: + | "EMAIL_VERIFICATION_REQUIRED" + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + } + >; + signIn(input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "LINKING_TO_SESSION_USER_FAILED"; + reason: + | "EMAIL_VERIFICATION_REQUIRED" + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + } + >; + verifyCredentials(input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >; + createNewRecipeUser(input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + reason: string; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + >; + /** + * We pass in the email as well to this function cause the input userId + * may not be associated with an webauthn account. In this case, we + * need to know which email to use to create an webauthn account later on. + */ + generateRecoverAccountToken(input: { + userId: string; + email: string; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + token: string; + } + | { + status: "UNKNOWN_USER_ID_ERROR"; + } + >; + consumeRecoverAccountToken(input: { + token: string; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + email: string; + userId: string; + } + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + >; + registerCredential(input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + userContext: UserContext; + recipeUserId: RecipeUserId; + }): Promise< + | { + status: "OK"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + reason: string; + } + >; + decodeCredential(input: { + credential: CredentialPayload; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + credential: { + id: string; + rawId: string; + response: { + clientDataJSON: { + type: string; + challenge: string; + origin: string; + crossOrigin?: boolean; + tokenBinding?: { + id?: string; + status: "present" | "supported" | "not-supported"; + }; + }; + attestationObject: { + fmt: "packed" | "tpm" | "android-key" | "android-safetynet" | "fido-u2f" | "none"; + authData: { + rpIdHash: string; + flags: { + up: boolean; + uv: boolean; + be: boolean; + bs: boolean; + at: boolean; + ed: boolean; + flagsInt: number; + }; + counter: number; + aaguid?: string; + credentialID?: string; + credentialPublicKey?: string; + extensionsData?: unknown; + }; + attStmt: { + sig?: Base64URLString; + x5c?: Base64URLString[]; + response?: Base64URLString; + alg?: number; + ver?: string; + certInfo?: Base64URLString; + pubArea?: Base64URLString; + size: number; + }; + }; + transports?: ("ble" | "cable" | "hybrid" | "internal" | "nfc" | "smart-card" | "usb")[]; + userHandle: string; + }; + authenticatorAttachment: "platform" | "cross-platform"; + clientExtensionResults: Record; + type: string; + }; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >; + getUserFromRecoverAccountToken(input: { + token: string; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + >; + removeCredential(input: { + webauthnCredentialId: string; + recipeUserId: RecipeUserId; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + } + | { + status: "CREDENTIAL_NOT_FOUND_ERROR"; + } + >; + getCredential(input: { + webauthnCredentialId: string; + recipeUserId: RecipeUserId; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + id: string; + relyingPartyId: string; + recipeUserId: RecipeUserId; + createdAt: number; + } + | { + status: "CREDENTIAL_NOT_FOUND_ERROR"; + } + >; + listCredentials(input: { + recipeUserId: RecipeUserId; + userContext: UserContext; + }): Promise<{ + status: "OK"; + credentials: { + id: string; + relyingPartyId: string; + createdAt: number; + }[]; + }>; + removeGeneratedOptions(input: { + webauthnGeneratedOptionsId: string; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + } + | { + status: "GENERATED_OPTIONS_NOT_FOUND_ERROR"; + } + >; + getGeneratedOptions(input: { + webauthnGeneratedOptionsId: string; + tenantId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + id: string; + relyingPartyId: string; + origin: string; + email: string; + timeout: string; + challenge: string; + } + | { + status: "GENERATED_OPTIONS_NOT_FOUND_ERROR"; + } + >; +}; +export declare type APIOptions = { + recipeImplementation: RecipeInterface; + appInfo: NormalisedAppinfo; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; + emailDelivery: EmailDeliveryIngredient; +}; +export declare type APIInterface = { + registerOptionsPOST: + | undefined + | (( + input: { + tenantId: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + email: string; + } + | { + recoverAccountToken: string; + } + ) + ) => Promise< + | { + status: "OK"; + webauthnGeneratedOptionsId: string; + rp: { + id: string; + name: string; + }; + user: { + id: string; + name: string; + displayName: string; + }; + challenge: string; + timeout: number; + excludeCredentials: { + id: string; + type: "public-key"; + transports: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[]; + }[]; + attestation: "none" | "indirect" | "direct" | "enterprise"; + pubKeyCredParams: { + alg: number; + type: string; + }[]; + authenticatorSelection: { + requireResidentKey: boolean; + residentKey: "required" | "preferred" | "discouraged"; + userVerification: "required" | "preferred" | "discouraged"; + }; + } + | GeneralErrorResponse + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + | { + status: "INVALID_EMAIL_ERROR"; + err: string; + } + >); + signInOptionsPOST: + | undefined + | ((input: { + email?: string; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + webauthnGeneratedOptionsId: string; + challenge: string; + timeout: number; + userVerification: "required" | "preferred" | "discouraged"; + } + | GeneralErrorResponse + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >); + signUpPOST: + | undefined + | ((input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + tenantId: string; + session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + user: User; + session: SessionContainerInterface; + } + | GeneralErrorResponse + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + reason: string; + } + >); + signInPOST: + | undefined + | ((input: { + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + tenantId: string; + session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + user: User; + session: SessionContainerInterface; + } + | GeneralErrorResponse + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + >); + generateRecoverAccountTokenPOST: + | undefined + | ((input: { + email: string; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + } + | GeneralErrorResponse + | { + status: "RECOVER_ACCOUNT_NOT_ALLOWED"; + reason: string; + } + >); + recoverAccountPOST: + | undefined + | ((input: { + token: string; + webauthnGeneratedOptionsId: string; + credential: CredentialPayload; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + user: User; + email: string; + } + | GeneralErrorResponse + | { + status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR"; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "INVALID_AUTHENTICATOR_ERROR"; + reason: string; + } + >); + emailExistsGET: + | undefined + | ((input: { + email: string; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + exists: boolean; + } + | GeneralErrorResponse + >); +}; +export declare type TypeWebauthnRecoverAccountEmailDeliveryInput = { + type: "RECOVER_ACCOUNT"; + user: { + id: string; + recipeUserId: RecipeUserId | undefined; + email: string; + }; + recoverAccountLink: string; + tenantId: string; +}; +export declare type TypeWebauthnEmailDeliveryInput = TypeWebauthnRecoverAccountEmailDeliveryInput; +export declare type CredentialPayload = { + id: string; + rawId: string; + response: { + clientDataJSON: string; + attestationObject: string; + transports?: ("ble" | "cable" | "hybrid" | "internal" | "nfc" | "smart-card" | "usb")[]; + userHandle: string; + }; + authenticatorAttachment: "platform" | "cross-platform"; + clientExtensionResults: Record; + type: "public-key"; +}; +export {}; diff --git a/lib/build/recipe/webauthn/types.js b/lib/build/recipe/webauthn/types.js new file mode 100644 index 000000000..a098ca1d7 --- /dev/null +++ b/lib/build/recipe/webauthn/types.js @@ -0,0 +1,16 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/webauthn/utils.d.ts b/lib/build/recipe/webauthn/utils.d.ts new file mode 100644 index 000000000..e4492d9f0 --- /dev/null +++ b/lib/build/recipe/webauthn/utils.d.ts @@ -0,0 +1,20 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import { TypeInput, TypeNormalisedInput } from "./types"; +import { NormalisedAppinfo, UserContext } from "../../types"; +import { BaseRequest } from "../../framework"; +export declare function validateAndNormaliseUserInput( + recipeInstance: Recipe, + appInfo: NormalisedAppinfo, + config?: TypeInput +): TypeNormalisedInput; +export declare function defaultEmailValidator( + value: any +): Promise<"Development bug: Please make sure the email field yields a string" | "Email is invalid" | undefined>; +export declare function getRecoverAccountLink(input: { + appInfo: NormalisedAppinfo; + token: string; + tenantId: string; + request: BaseRequest | undefined; + userContext: UserContext; +}): string; diff --git a/lib/build/recipe/webauthn/utils.js b/lib/build/recipe/webauthn/utils.js new file mode 100644 index 000000000..191551342 --- /dev/null +++ b/lib/build/recipe/webauthn/utils.js @@ -0,0 +1,163 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getRecoverAccountLink = exports.defaultEmailValidator = exports.validateAndNormaliseUserInput = void 0; +function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { + let relyingPartyId = validateAndNormaliseRelyingPartyIdConfig( + recipeInstance, + appInfo, + config === null || config === void 0 ? void 0 : config.relyingPartyId + ); + let relyingPartyName = validateAndNormaliseRelyingPartyNameConfig( + recipeInstance, + appInfo, + config === null || config === void 0 ? void 0 : config.relyingPartyName + ); + let getOrigin = validateAndNormaliseGetOriginConfig( + recipeInstance, + appInfo, + config === null || config === void 0 ? void 0 : config.getOrigin + ); + let validateEmailAddress = validateAndNormaliseValidateEmailAddressConfig( + recipeInstance, + appInfo, + config === null || config === void 0 ? void 0 : config.validateEmailAddress + ); + let override = Object.assign( + { + functions: (originalImplementation) => originalImplementation, + apis: (originalImplementation) => originalImplementation, + }, + config === null || config === void 0 ? void 0 : config.override + ); + function getEmailDeliveryConfig(isInServerlessEnv) { + var _a; + let emailService = + (_a = config === null || config === void 0 ? void 0 : config.emailDelivery) === null || _a === void 0 + ? void 0 + : _a.service; + console.log("emailService", emailService); + console.log("isInServerlessEnv", isInServerlessEnv); + /** + * If the user has not passed even that config, we use the default + * createAndSendCustomEmail implementation which calls our supertokens API + */ + // if (emailService === undefined) { + // emailService = new BackwardCompatibilityService(appInfo, isInServerlessEnv); + // } + return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { + /** + * if we do + * let emailDelivery = { + * service: emailService, + * ...config.emailDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + // todo implemenet this + service: null, + }); + } + return { + override, + getOrigin, + relyingPartyId, + relyingPartyName, + validateEmailAddress, + getEmailDeliveryConfig, + }; +} +exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; +function validateAndNormaliseRelyingPartyIdConfig(_, __, relyingPartyIdConfig) { + return (props) => { + if (typeof relyingPartyIdConfig === "string") { + return Promise.resolve(relyingPartyIdConfig); + } else if (typeof relyingPartyIdConfig === "function") { + return relyingPartyIdConfig(props); + } else { + return Promise.resolve( + __.getOrigin({ request: props.request, userContext: props.userContext }).getAsStringDangerous() + ); + } + }; +} +function validateAndNormaliseRelyingPartyNameConfig(_, __, relyingPartyNameConfig) { + return (props) => { + if (typeof relyingPartyNameConfig === "string") { + return Promise.resolve(relyingPartyNameConfig); + } else if (typeof relyingPartyNameConfig === "function") { + return relyingPartyNameConfig(props); + } else { + return Promise.resolve(__.appName); + } + }; +} +function validateAndNormaliseGetOriginConfig(_, __, getOriginConfig) { + return (props) => { + if (typeof getOriginConfig === "function") { + return getOriginConfig(props); + } else { + return Promise.resolve( + __.getOrigin({ request: props.request, userContext: props.userContext }).getAsStringDangerous() + ); + } + }; +} +function validateAndNormaliseValidateEmailAddressConfig(_, __, validateEmailAddressConfig) { + return (email, tenantId) => { + if (typeof validateEmailAddressConfig === "function") { + return validateEmailAddressConfig(email, tenantId); + } else { + return defaultEmailValidator(email); + } + }; +} +async function defaultEmailValidator(value) { + // We check if the email syntax is correct + // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 + // Regex from https://stackoverflow.com/a/46181/3867175 + if (typeof value !== "string") { + return "Development bug: Please make sure the email field yields a string"; + } + if ( + value.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ) === null + ) { + return "Email is invalid"; + } + return undefined; +} +exports.defaultEmailValidator = defaultEmailValidator; +function getRecoverAccountLink(input) { + return ( + input.appInfo + .getOrigin({ + request: input.request, + userContext: input.userContext, + }) + .getAsStringDangerous() + + input.appInfo.websiteBasePath.getAsStringDangerous() + + "/recover-account?token=" + + input.token + + "&tenantId=" + + input.tenantId + ); +} +exports.getRecoverAccountLink = getRecoverAccountLink; diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts index c45e6de40..c1abe0c67 100644 --- a/lib/build/types.d.ts +++ b/lib/build/types.d.ts @@ -87,6 +87,9 @@ export declare type User = { id: string; userId: string; }[]; + webauthn: { + credentialIds: string[]; + }; loginMethods: (RecipeLevelUser & { verified: boolean; hasSameEmailAs: (email: string | undefined) => boolean; diff --git a/lib/build/user.d.ts b/lib/build/user.d.ts index ccdddb396..9bc4835a5 100644 --- a/lib/build/user.d.ts +++ b/lib/build/user.d.ts @@ -9,6 +9,7 @@ export declare class LoginMethod implements RecipeLevelUser { readonly email?: string; readonly phoneNumber?: string; readonly thirdParty?: RecipeLevelUser["thirdParty"]; + readonly webauthn?: RecipeLevelUser["webauthn"]; readonly verified: boolean; readonly timeJoined: number; constructor(loginMethod: UserWithoutHelperFunctions["loginMethods"][number]); @@ -27,6 +28,9 @@ export declare class User implements UserType { id: string; userId: string; }[]; + readonly webauthn: { + credentialIds: string[]; + }; readonly loginMethods: LoginMethod[]; readonly timeJoined: number; constructor(user: UserWithoutHelperFunctions); @@ -43,8 +47,11 @@ export declare type UserWithoutHelperFunctions = { id: string; userId: string; }[]; + webauthn: { + credentialIds: string[]; + }; loginMethods: { - recipeId: "emailpassword" | "thirdparty" | "passwordless"; + recipeId: "emailpassword" | "thirdparty" | "passwordless" | "webauthn"; recipeUserId: string; tenantIds: string[]; email?: string; @@ -53,6 +60,9 @@ export declare type UserWithoutHelperFunctions = { id: string; userId: string; }; + webauthn?: { + credentialIds: string[]; + }; verified: boolean; timeJoined: number; }[]; diff --git a/lib/build/user.js b/lib/build/user.js index 5179ebc6f..fe045dde2 100644 --- a/lib/build/user.js +++ b/lib/build/user.js @@ -16,6 +16,7 @@ class LoginMethod { this.email = loginMethod.email; this.phoneNumber = loginMethod.phoneNumber; this.thirdParty = loginMethod.thirdParty; + this.webauthn = loginMethod.webauthn; this.timeJoined = loginMethod.timeJoined; this.verified = loginMethod.verified; } @@ -61,6 +62,7 @@ class LoginMethod { email: this.email, phoneNumber: this.phoneNumber, thirdParty: this.thirdParty, + webauthn: this.webauthn, timeJoined: this.timeJoined, verified: this.verified, }; @@ -75,6 +77,7 @@ class User { this.emails = user.emails; this.phoneNumbers = user.phoneNumbers; this.thirdParty = user.thirdParty; + this.webauthn = user.webauthn; this.timeJoined = user.timeJoined; this.loginMethods = user.loginMethods.map((m) => new LoginMethod(m)); } @@ -86,6 +89,7 @@ class User { emails: this.emails, phoneNumbers: this.phoneNumbers, thirdParty: this.thirdParty, + webauthn: this.webauthn, loginMethods: this.loginMethods.map((m) => m.toJson()), timeJoined: this.timeJoined, }; diff --git a/lib/ts/recipe/webauthn/core-mock.ts b/lib/ts/recipe/webauthn/core-mock.ts index 7ee65cb53..488b2ba5d 100644 --- a/lib/ts/recipe/webauthn/core-mock.ts +++ b/lib/ts/recipe/webauthn/core-mock.ts @@ -10,6 +10,9 @@ export const getMockQuerier = (recipeId: string) => { body: any, userContext: UserContext ): Promise => { + console.log("body", body); + console.log("userContext", userContext); + if (path.getAsStringDangerous().includes("/recipe/webauthn/options/register")) { // @ts-ignore return { diff --git a/lib/ts/recipe/webauthn/types.ts b/lib/ts/recipe/webauthn/types.ts index 6275e7464..f91c489d1 100644 --- a/lib/ts/recipe/webauthn/types.ts +++ b/lib/ts/recipe/webauthn/types.ts @@ -98,44 +98,44 @@ export type TypeInputValidateEmailAddress = ( ) => Promise | string | undefined; // centralize error types in order to prevent missing cascading errors -type RegisterOptionsErrorResponse = { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" }; +// type RegisterOptionsErrorResponse = { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" }; -type SignInOptionsErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; +// type SignInOptionsErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; -type SignUpErrorResponse = - | { status: "EMAIL_ALREADY_EXISTS_ERROR" } - | { status: "WRONG_CREDENTIALS_ERROR" } - | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; +// type SignUpErrorResponse = +// | { status: "EMAIL_ALREADY_EXISTS_ERROR" } +// | { status: "WRONG_CREDENTIALS_ERROR" } +// | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; -type SignInErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; +// type SignInErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; -type VerifyCredentialsErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; +// type VerifyCredentialsErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; -type GenerateRecoverAccountTokenErrorResponse = { status: "UNKNOWN_USER_ID_ERROR" }; +// type GenerateRecoverAccountTokenErrorResponse = { status: "UNKNOWN_USER_ID_ERROR" }; -type ConsumeRecoverAccountTokenErrorResponse = { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" }; +// type ConsumeRecoverAccountTokenErrorResponse = { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" }; -type RegisterCredentialErrorResponse = - | { status: "WRONG_CREDENTIALS_ERROR" } - // when the attestation is checked and is not valid or other cases in whcih the authenticator is not correct - | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; +// type RegisterCredentialErrorResponse = +// | { status: "WRONG_CREDENTIALS_ERROR" } +// // when the attestation is checked and is not valid or other cases in whcih the authenticator is not correct +// | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; -type CreateNewRecipeUserErrorResponse = - | { status: "WRONG_CREDENTIALS_ERROR" } - | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string } - | { status: "EMAIL_ALREADY_EXISTS_ERROR" }; +// type CreateNewRecipeUserErrorResponse = +// | { status: "WRONG_CREDENTIALS_ERROR" } +// | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string } +// | { status: "EMAIL_ALREADY_EXISTS_ERROR" }; -type DecodeCredentialErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; +// type DecodeCredentialErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; -type GetUserFromRecoverAccountTokenErrorResponse = { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" }; +// type GetUserFromRecoverAccountTokenErrorResponse = { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" }; -type RemoveCredentialErrorResponse = { status: "CREDENTIAL_NOT_FOUND_ERROR" }; +// type RemoveCredentialErrorResponse = { status: "CREDENTIAL_NOT_FOUND_ERROR" }; -type GetCredentialErrorResponse = { status: "CREDENTIAL_NOT_FOUND_ERROR" }; +// type GetCredentialErrorResponse = { status: "CREDENTIAL_NOT_FOUND_ERROR" }; -type RemoveGeneratedOptionsErrorResponse = { status: "GENERATED_OPTIONS_NOT_FOUND_ERROR" }; +// type RemoveGeneratedOptionsErrorResponse = { status: "GENERATED_OPTIONS_NOT_FOUND_ERROR" }; -type GetGeneratedOptionsErrorResponse = { status: "GENERATED_OPTIONS_NOT_FOUND_ERROR" }; +// type GetGeneratedOptionsErrorResponse = { status: "GENERATED_OPTIONS_NOT_FOUND_ERROR" }; type Base64URLString = string; @@ -510,37 +510,37 @@ export type APIOptions = { emailDelivery: EmailDeliveryIngredient; }; -type RegisterOptionsPOSTErrorResponse = - | { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" } - | { status: "INVALID_EMAIL_ERROR"; err: string }; - -type SignInOptionsPOSTErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; - -type SignUpPOSTErrorResponse = - | { - status: "SIGN_UP_NOT_ALLOWED"; - reason: string; - } - | { status: "EMAIL_ALREADY_EXISTS_ERROR" } - | { status: "WRONG_CREDENTIALS_ERROR" } - | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; - -type SignInPOSTErrorResponse = - | { status: "WRONG_CREDENTIALS_ERROR" } - | { - status: "SIGN_IN_NOT_ALLOWED"; - reason: string; - }; - -type GenerateRecoverAccountTokenPOSTErrorResponse = { - status: "RECOVER_ACCOUNT_NOT_ALLOWED"; - reason: string; -}; - -type RecoverAccountPOSTErrorResponse = - | { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" } - | { status: "WRONG_CREDENTIALS_ERROR" } - | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; +// type RegisterOptionsPOSTErrorResponse = +// | { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" } +// | { status: "INVALID_EMAIL_ERROR"; err: string }; + +// type SignInOptionsPOSTErrorResponse = { status: "WRONG_CREDENTIALS_ERROR" }; + +// type SignUpPOSTErrorResponse = +// | { +// status: "SIGN_UP_NOT_ALLOWED"; +// reason: string; +// } +// | { status: "EMAIL_ALREADY_EXISTS_ERROR" } +// | { status: "WRONG_CREDENTIALS_ERROR" } +// | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; + +// type SignInPOSTErrorResponse = +// | { status: "WRONG_CREDENTIALS_ERROR" } +// | { +// status: "SIGN_IN_NOT_ALLOWED"; +// reason: string; +// }; + +// type GenerateRecoverAccountTokenPOSTErrorResponse = { +// status: "RECOVER_ACCOUNT_NOT_ALLOWED"; +// reason: string; +// }; + +// type RecoverAccountPOSTErrorResponse = +// | { status: "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR" } +// | { status: "WRONG_CREDENTIALS_ERROR" } +// | { status: "INVALID_AUTHENTICATOR_ERROR"; reason: string }; export type APIInterface = { registerOptionsPOST: diff --git a/lib/ts/recipe/webauthn/utils.ts b/lib/ts/recipe/webauthn/utils.ts index 9f94448c0..7a0a5d733 100644 --- a/lib/ts/recipe/webauthn/utils.ts +++ b/lib/ts/recipe/webauthn/utils.ts @@ -56,6 +56,8 @@ export function validateAndNormaliseUserInput( function getEmailDeliveryConfig(isInServerlessEnv: boolean) { let emailService = config?.emailDelivery?.service; + console.log("emailService", emailService); + console.log("isInServerlessEnv", isInServerlessEnv); /** * If the user has not passed even that config, we use the default * createAndSendCustomEmail implementation which calls our supertokens API