From 1f91693345940a5582030ae169f5e570131a5cef Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 10:53:16 +0530 Subject: [PATCH 1/7] Add changes to support unknown type in formField values --- .../emailpassword/api/implementation.js | 62 ++++++- lib/build/recipe/emailpassword/api/utils.d.ts | 2 +- lib/build/recipe/emailpassword/api/utils.js | 4 +- lib/build/recipe/emailpassword/types.d.ts | 8 +- .../api/generatePasswordResetToken.ts | 2 +- .../emailpassword/api/implementation.ts | 162 ++++++++++++++---- .../recipe/emailpassword/api/passwordReset.ts | 2 +- lib/ts/recipe/emailpassword/api/signin.ts | 2 +- lib/ts/recipe/emailpassword/api/signup.ts | 2 +- lib/ts/recipe/emailpassword/api/utils.ts | 6 +- lib/ts/recipe/emailpassword/types.ts | 8 +- test/with-typescript/index.ts | 22 ++- 12 files changed, 226 insertions(+), 56 deletions(-) diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 0d4ebc815..f115a5a19 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -40,7 +40,16 @@ function getAPIImplementation() { }; }, generatePasswordResetTokenPOST: async function ({ formFields, tenantId, options, userContext }) { - const email = formFields.filter((f) => f.id === "email")[0].value; + // 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. + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + if (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + const email = emailAsUnknown; // this function will be reused in different parts of the flow below.. async function generateAndSendPasswordResetToken(primaryUserId, recipeUserId) { // the user ID here can be primary or recipe level. @@ -355,7 +364,16 @@ function getAPIImplementation() { }; } } - let newPassword = formFields.filter((f) => f.id === "password")[0].value; + // NOTE: Check for password 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. + const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + if (typeof newPasswordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + let newPassword = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, tenantId, @@ -371,7 +389,7 @@ function getAPIImplementation() { // 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 email password - // user does not exist, but it means that there is no reicpe or primary user + // user does not exist, but it means that there is no recipe or primary user // for whom the token was generated. return { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", @@ -503,8 +521,23 @@ function getAPIImplementation() { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_012)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + // 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 (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + if (typeof passwordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + let email = emailAsUnknown; + let password = passwordAsUnknown; const recipeId = "emailpassword"; const checkCredentialsOnTenant = async (tenantId) => { return ( @@ -635,8 +668,23 @@ function getAPIImplementation() { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_016)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + // 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 (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + if (typeof passwordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + let email = emailAsUnknown; + let password = passwordAsUnknown; const preAuthCheckRes = await authUtils_1.AuthUtils.preAuthChecks({ authenticatingAccountInfo: { recipeId: "emailpassword", diff --git a/lib/build/recipe/emailpassword/api/utils.d.ts b/lib/build/recipe/emailpassword/api/utils.d.ts index d797f42e3..0f67df755 100644 --- a/lib/build/recipe/emailpassword/api/utils.d.ts +++ b/lib/build/recipe/emailpassword/api/utils.d.ts @@ -9,6 +9,6 @@ export declare function validateFormFieldsOrThrowError( ): Promise< { id: string; - value: string; + value: unknown; }[] >; diff --git a/lib/build/recipe/emailpassword/api/utils.js b/lib/build/recipe/emailpassword/api/utils.js index afbca42de..4e612889e 100644 --- a/lib/build/recipe/emailpassword/api/utils.js +++ b/lib/build/recipe/emailpassword/api/utils.js @@ -35,7 +35,9 @@ async function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw, t // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 formFields = formFields.map((field) => { if (field.id === constants_1.FORM_FIELD_EMAIL_ID) { - return Object.assign(Object.assign({}, field), { value: field.value.trim() }); + return Object.assign(Object.assign({}, field), { + value: typeof field.value === "string" ? field.value.trim() : field.value, + }); } return field; }); diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index ee247490e..fa3eb1408 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -227,7 +227,7 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; options: APIOptions; @@ -247,7 +247,7 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; token: string; tenantId: string; @@ -273,7 +273,7 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; @@ -300,7 +300,7 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; diff --git a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts index fb600bb05..85da1aca2 100644 --- a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts +++ b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts @@ -35,7 +35,7 @@ export default async function generatePasswordResetToken( // step 1 let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, requestBody.formFields, diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index e08c0ea09..d05a05bf8 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -9,6 +9,7 @@ import RecipeUserId from "../../../recipeUserId"; import { getPasswordResetLink } from "../utils"; import { AuthUtils } from "../../../authUtils"; import { isFakeEmail } from "../../thirdparty/utils"; +import { SessionContainerInterface } from "../../session/types"; export default function getAPIImplementation(): APIInterface { return { @@ -23,9 +24,9 @@ export default function getAPIImplementation(): APIInterface { userContext: UserContext; }): Promise< | { - status: "OK"; - exists: boolean; - } + status: "OK"; + exists: boolean; + } | GeneralErrorResponse > { // even if the above returns true, we still need to check if there @@ -59,12 +60,21 @@ export default function getAPIImplementation(): APIInterface { userContext, }): Promise< | { - status: "OK"; - } + status: "OK"; + } | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { - const email = formFields.filter((f) => f.id === "email")[0].value; + // 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. + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + if (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + const email: string = emailAsUnknown; // this function will be reused in different parts of the flow below.. async function generateAndSendPasswordResetToken( @@ -72,8 +82,8 @@ export default function getAPIImplementation(): APIInterface { recipeUserId: RecipeUserId | undefined ): Promise< | { - status: "OK"; - } + status: "OK"; + } | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { @@ -86,8 +96,7 @@ export default function getAPIImplementation(): APIInterface { }); if (response.status === "UNKNOWN_USER_ID_ERROR") { logDebugMessage( - `Password reset email not sent, unknown user id: ${ - recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() + `Password reset email not sent, unknown user id: ${recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() }` ); return { @@ -196,9 +205,9 @@ export default function getAPIImplementation(): APIInterface { emailPasswordAccount !== undefined ? emailPasswordAccount : { - recipeId: "emailpassword", - email, - }, + recipeId: "emailpassword", + email, + }, primaryUserAssociatedWithEmail, undefined, tenantId, @@ -321,7 +330,7 @@ export default function getAPIImplementation(): APIInterface { }: { formFields: { id: string; - value: string; + value: unknown; }[]; token: string; tenantId: string; @@ -329,10 +338,10 @@ export default function getAPIImplementation(): APIInterface { userContext: UserContext; }): Promise< | { - status: "OK"; - user: User; - email: string; - } + status: "OK"; + user: User; + email: string; + } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse @@ -366,10 +375,10 @@ export default function getAPIImplementation(): APIInterface { recipeUserId: RecipeUserId ): Promise< | { - status: "OK"; - user: User; - email: string; - } + status: "OK"; + user: User; + email: string; + } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse @@ -450,7 +459,16 @@ export default function getAPIImplementation(): APIInterface { } } - let newPassword = formFields.filter((f) => f.id === "password")[0].value; + // NOTE: Check for password 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. + const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + if (typeof newPasswordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + let newPassword: string = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, @@ -471,7 +489,7 @@ export default function getAPIImplementation(): APIInterface { // 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 email password - // user does not exist, but it means that there is no reicpe or primary user + // user does not exist, but it means that there is no recipe or primary user // for whom the token was generated. return { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", @@ -593,7 +611,31 @@ export default function getAPIImplementation(): APIInterface { shouldTryLinkingWithSessionUser, options, userContext, - }) { + }: { + formFields: { + id: string; + value: unknown; + }[]; + tenantId: string; + session?: SessionContainerInterface; + shouldTryLinkingWithSessionUser: boolean | undefined, + options: APIOptions; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + session: SessionContainerInterface; + user: User; + } + | { + status: "WRONG_CREDENTIALS_ERROR"; + } + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } + | GeneralErrorResponse + > { const errorCodeMap = { SIGN_IN_NOT_ALLOWED: "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", @@ -608,8 +650,26 @@ export default function getAPIImplementation(): APIInterface { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_012)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + + // 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 (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + + if (typeof passwordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + + let email: string = emailAsUnknown; + let password: string = passwordAsUnknown; const recipeId = "emailpassword"; @@ -722,7 +782,31 @@ export default function getAPIImplementation(): APIInterface { shouldTryLinkingWithSessionUser, options, userContext, - }) { + }: { + formFields: { + id: string; + value: unknown; + }[]; + tenantId: string; + session?: SessionContainerInterface; + shouldTryLinkingWithSessionUser: boolean | undefined, + options: APIOptions; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + session: SessionContainerInterface; + user: User; + } + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + | GeneralErrorResponse + > { 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)", @@ -737,8 +821,26 @@ export default function getAPIImplementation(): APIInterface { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_016)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + + // 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 (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + + if (typeof passwordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + + let email: string = emailAsUnknown; + let password: string = passwordAsUnknown; const preAuthCheckRes = await AuthUtils.preAuthChecks({ authenticatingAccountInfo: { diff --git a/lib/ts/recipe/emailpassword/api/passwordReset.ts b/lib/ts/recipe/emailpassword/api/passwordReset.ts index c6a26c010..9ab19df6b 100644 --- a/lib/ts/recipe/emailpassword/api/passwordReset.ts +++ b/lib/ts/recipe/emailpassword/api/passwordReset.ts @@ -39,7 +39,7 @@ export default async function passwordReset( // a password that meets the password policy. let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, requestBody.formFields, diff --git a/lib/ts/recipe/emailpassword/api/signin.ts b/lib/ts/recipe/emailpassword/api/signin.ts index 2899715cf..899d06bc0 100644 --- a/lib/ts/recipe/emailpassword/api/signin.ts +++ b/lib/ts/recipe/emailpassword/api/signin.ts @@ -38,7 +38,7 @@ export default async function signInAPI( // step 1 let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.signInFeature.formFields, body.formFields, diff --git a/lib/ts/recipe/emailpassword/api/signup.ts b/lib/ts/recipe/emailpassword/api/signup.ts index 67a4b49e1..bb4c1efa2 100644 --- a/lib/ts/recipe/emailpassword/api/signup.ts +++ b/lib/ts/recipe/emailpassword/api/signup.ts @@ -41,7 +41,7 @@ export default async function signUpAPI( // step 1 let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.signUpFeature.formFields, requestBody.formFields, diff --git a/lib/ts/recipe/emailpassword/api/utils.ts b/lib/ts/recipe/emailpassword/api/utils.ts index 23023ae1f..fa7959539 100644 --- a/lib/ts/recipe/emailpassword/api/utils.ts +++ b/lib/ts/recipe/emailpassword/api/utils.ts @@ -25,7 +25,7 @@ export async function validateFormFieldsOrThrowError( ): Promise< { id: string; - value: string; + value: unknown; }[] > { // first we check syntax ---------------------------- @@ -39,7 +39,7 @@ export async function validateFormFieldsOrThrowError( let formFields: { id: string; - value: string; + value: unknown; }[] = []; for (let i = 0; i < formFieldsRaw.length; i++) { @@ -63,7 +63,7 @@ export async function validateFormFieldsOrThrowError( if (field.id === FORM_FIELD_EMAIL_ID) { return { ...field, - value: field.value.trim(), + value: typeof field.value === "string" ? field.value.trim() : field.value, }; } return field; diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 563c95d4f..0fdfbe088 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -230,7 +230,7 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; options: APIOptions; @@ -251,7 +251,7 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; token: string; tenantId: string; @@ -275,7 +275,7 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; @@ -303,7 +303,7 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index c60e07968..437f15fe0 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -1348,8 +1348,26 @@ EmailPassword.init({ signInPOST: async (input) => { let formFields = input.formFields; let options = input.options; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + + // 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 (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + + if (typeof passwordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + + let email: string = emailAsUnknown; + let password: string = passwordAsUnknown; let response = await options.recipeImplementation.signIn({ email, From e4a172cd368ca305b9e4139398a8fb616ebd7b89 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 11:16:10 +0530 Subject: [PATCH 2/7] Update email/password invalid type error message to be same as go/python sdk --- lib/build/recipe/emailpassword/api/utils.js | 2 +- lib/ts/recipe/emailpassword/api/utils.ts | 2 +- test/emailpassword/signinFeature.test.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/build/recipe/emailpassword/api/utils.js b/lib/build/recipe/emailpassword/api/utils.js index 4e612889e..69da81aba 100644 --- a/lib/build/recipe/emailpassword/api/utils.js +++ b/lib/build/recipe/emailpassword/api/utils.js @@ -27,7 +27,7 @@ async function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw, t } if (curr.id === constants_1.FORM_FIELD_EMAIL_ID || curr.id === constants_1.FORM_FIELD_PASSWORD_ID) { if (typeof curr.value !== "string") { - throw newBadRequestError("The value of formFields with id = " + curr.id + " must be a string"); + throw newBadRequestError(`${curr.id} value must be a string`); } } formFields.push(curr); diff --git a/lib/ts/recipe/emailpassword/api/utils.ts b/lib/ts/recipe/emailpassword/api/utils.ts index fa7959539..71204187d 100644 --- a/lib/ts/recipe/emailpassword/api/utils.ts +++ b/lib/ts/recipe/emailpassword/api/utils.ts @@ -52,7 +52,7 @@ export async function validateFormFieldsOrThrowError( } if (curr.id === FORM_FIELD_EMAIL_ID || curr.id === FORM_FIELD_PASSWORD_ID) { if (typeof curr.value !== "string") { - throw newBadRequestError("The value of formFields with id = " + curr.id + " must be a string"); + throw newBadRequestError(`${curr.id} value must be a string`); } } formFields.push(curr); diff --git a/test/emailpassword/signinFeature.test.js b/test/emailpassword/signinFeature.test.js index 21e6e0d5c..15f95f7d6 100644 --- a/test/emailpassword/signinFeature.test.js +++ b/test/emailpassword/signinFeature.test.js @@ -351,7 +351,7 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] } }) ); - assert(JSON.parse(res.text).message === "The value of formFields with id = password must be a string"); + assert(JSON.parse(res.text).message === "password value must be a string"); }); it("test email must be of type string in input", async function () { @@ -404,7 +404,7 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] } }) ); - assert(JSON.parse(res.text).message === "The value of formFields with id = email must be a string"); + assert(JSON.parse(res.text).message === "email value must be a string"); }); /* From ff97e72a22dc22f3fd8488d90371c6881932631f Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 11:20:38 +0530 Subject: [PATCH 3/7] Add tests for invalid email/password in signup API --- test/emailpassword/signupFeature.test.js | 96 ++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/emailpassword/signupFeature.test.js b/test/emailpassword/signupFeature.test.js index 4f337ef8b..c9a5299ab 100644 --- a/test/emailpassword/signupFeature.test.js +++ b/test/emailpassword/signupFeature.test.js @@ -357,6 +357,102 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] assert.strictEqual(badInputResponse.message, "Missing input param: formFields"); }); + it("test bad input, invalid password type in /signup API", async function () { + const connectionURI = await startST(); + + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let badInputResponse = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "random@gmail.com", + }, + { + id: "password", + value: 1234, + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(badInputResponse.message === "password value must be a string"); + }); + + it("test bad input, invalid email type in /signup API", async function () { + const connectionURI = await startST(); + + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let badInputResponse = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: 12435, + }, + { + id: "password", + value: "1234", + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(badInputResponse.message === "email value must be a string"); + }); + it("test bad input, formFields is not an array in /signup API", async function () { const connectionURI = await startST(); From 71d31e7f9b8b93ec4bc6ba2d66fac196f86bfbb0 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 11:24:45 +0530 Subject: [PATCH 4/7] Add tests for invalid email/password in token reset and reset API --- test/emailpassword/passwordreset.test.js | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/test/emailpassword/passwordreset.test.js b/test/emailpassword/passwordreset.test.js index f4d43b106..e8a579f6f 100644 --- a/test/emailpassword/passwordreset.test.js +++ b/test/emailpassword/passwordreset.test.js @@ -111,6 +111,48 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] assert(response.body.formFields[0].id === "email"); }); + it("test invalid email type in generate token API", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: 123456, + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response.body.message === "email value must be a string"); + }); + it("test that generated password link is correct", async function () { const connectionURI = await startST(); @@ -255,6 +297,49 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] assert(response.status !== "FIELD_ERROR"); }); + it("test invalid type of password", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: 12345, + }, + ], + token: "randomToken", + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(response.message === "password value must be a string"); + }); + it("test token missing from input", async function () { const connectionURI = await startST(); STExpress.init({ From 3754acb641746d2c6e3eada9d925101232f56da0 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 12:04:10 +0530 Subject: [PATCH 5/7] Update some errors to indicate unreachable errors --- .../emailpassword/api/implementation.ts | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index d05a05bf8..531f7cd9e 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -70,10 +70,7 @@ export default function getAPIImplementation(): APIInterface { // in validation but kept here to be safe. const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; if (typeof emailAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "email value needs to be a string", - }; + throw new Error("Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError"); const email: string = emailAsUnknown; // this function will be reused in different parts of the flow below.. @@ -464,10 +461,7 @@ export default function getAPIImplementation(): APIInterface { // in validation but kept here to be safe. const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; if (typeof newPasswordAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "password value needs to be a string", - }; + throw new Error("Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError"); let newPassword: string = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ @@ -657,16 +651,10 @@ export default function getAPIImplementation(): APIInterface { // check for type is done in a parent function but they are kept // here to be on the safe side. if (typeof emailAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "email value needs to be a string", - }; + throw new Error("Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError"); if (typeof passwordAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "password value needs to be a string", - }; + throw new Error("Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError"); let email: string = emailAsUnknown; let password: string = passwordAsUnknown; @@ -828,16 +816,10 @@ export default function getAPIImplementation(): APIInterface { // check for type is done in a parent function but they are kept // here to be on the safe side. if (typeof emailAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "email value needs to be a string", - }; + throw new Error("Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError"); if (typeof passwordAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "password value needs to be a string", - }; + throw new Error("Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError"); let email: string = emailAsUnknown; let password: string = passwordAsUnknown; From 0e22c945e939320ae4553bc01c6eeaa1346e5257 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 14:08:02 +0530 Subject: [PATCH 6/7] Run build to generate build files --- .../emailpassword/api/implementation.js | 42 +++---- .../emailpassword/api/implementation.ts | 103 ++++++++++-------- 2 files changed, 76 insertions(+), 69 deletions(-) diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index f115a5a19..6619c14cf 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -45,10 +45,9 @@ function getAPIImplementation() { // in validation but kept here to be safe. const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; if (typeof emailAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "email value needs to be a string", - }; + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); const email = emailAsUnknown; // this function will be reused in different parts of the flow below.. async function generateAndSendPasswordResetToken(primaryUserId, recipeUserId) { @@ -369,10 +368,9 @@ function getAPIImplementation() { // in validation but kept here to be safe. const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; if (typeof newPasswordAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "password value needs to be a string", - }; + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); let newPassword = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, @@ -527,15 +525,13 @@ function getAPIImplementation() { // check for type is done in a parent function but they are kept // here to be on the safe side. if (typeof emailAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "email value needs to be a string", - }; + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); if (typeof passwordAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "password value needs to be a string", - }; + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); let email = emailAsUnknown; let password = passwordAsUnknown; const recipeId = "emailpassword"; @@ -674,15 +670,13 @@ function getAPIImplementation() { // check for type is done in a parent function but they are kept // here to be on the safe side. if (typeof emailAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "email value needs to be a string", - }; + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); if (typeof passwordAsUnknown !== "string") - return { - status: "GENERAL_ERROR", - message: "password value needs to be a string", - }; + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); let email = emailAsUnknown; let password = passwordAsUnknown; const preAuthCheckRes = await authUtils_1.AuthUtils.preAuthChecks({ diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 531f7cd9e..635ff2bd6 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -24,9 +24,9 @@ export default function getAPIImplementation(): APIInterface { userContext: UserContext; }): Promise< | { - status: "OK"; - exists: boolean; - } + status: "OK"; + exists: boolean; + } | GeneralErrorResponse > { // even if the above returns true, we still need to check if there @@ -60,8 +60,8 @@ export default function getAPIImplementation(): APIInterface { userContext, }): Promise< | { - status: "OK"; - } + status: "OK"; + } | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { @@ -70,7 +70,9 @@ export default function getAPIImplementation(): APIInterface { // in validation but kept here to be safe. const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; if (typeof emailAsUnknown !== "string") - throw new Error("Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError"); + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); const email: string = emailAsUnknown; // this function will be reused in different parts of the flow below.. @@ -79,8 +81,8 @@ export default function getAPIImplementation(): APIInterface { recipeUserId: RecipeUserId | undefined ): Promise< | { - status: "OK"; - } + status: "OK"; + } | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { @@ -93,7 +95,8 @@ export default function getAPIImplementation(): APIInterface { }); if (response.status === "UNKNOWN_USER_ID_ERROR") { logDebugMessage( - `Password reset email not sent, unknown user id: ${recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() + `Password reset email not sent, unknown user id: ${ + recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() }` ); return { @@ -202,9 +205,9 @@ export default function getAPIImplementation(): APIInterface { emailPasswordAccount !== undefined ? emailPasswordAccount : { - recipeId: "emailpassword", - email, - }, + recipeId: "emailpassword", + email, + }, primaryUserAssociatedWithEmail, undefined, tenantId, @@ -335,10 +338,10 @@ export default function getAPIImplementation(): APIInterface { userContext: UserContext; }): Promise< | { - status: "OK"; - user: User; - email: string; - } + status: "OK"; + user: User; + email: string; + } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse @@ -372,10 +375,10 @@ export default function getAPIImplementation(): APIInterface { recipeUserId: RecipeUserId ): Promise< | { - status: "OK"; - user: User; - email: string; - } + status: "OK"; + user: User; + email: string; + } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse @@ -461,7 +464,9 @@ export default function getAPIImplementation(): APIInterface { // in validation but kept here to be safe. const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; if (typeof newPasswordAsUnknown !== "string") - throw new Error("Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError"); + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); let newPassword: string = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ @@ -612,22 +617,22 @@ export default function getAPIImplementation(): APIInterface { }[]; tenantId: string; session?: SessionContainerInterface; - shouldTryLinkingWithSessionUser: boolean | undefined, + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }): Promise< | { - status: "OK"; - session: SessionContainerInterface; - user: User; - } + status: "OK"; + session: SessionContainerInterface; + user: User; + } | { - status: "WRONG_CREDENTIALS_ERROR"; - } + status: "WRONG_CREDENTIALS_ERROR"; + } | { - status: "SIGN_IN_NOT_ALLOWED"; - reason: string; - } + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse > { const errorCodeMap = { @@ -651,10 +656,14 @@ export default function getAPIImplementation(): APIInterface { // check for type is done in a parent function but they are kept // here to be on the safe side. if (typeof emailAsUnknown !== "string") - throw new Error("Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError"); + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); if (typeof passwordAsUnknown !== "string") - throw new Error("Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError"); + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); let email: string = emailAsUnknown; let password: string = passwordAsUnknown; @@ -777,22 +786,22 @@ export default function getAPIImplementation(): APIInterface { }[]; tenantId: string; session?: SessionContainerInterface; - shouldTryLinkingWithSessionUser: boolean | undefined, + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }): Promise< | { - status: "OK"; - session: SessionContainerInterface; - user: User; - } + status: "OK"; + session: SessionContainerInterface; + user: User; + } | { - status: "SIGN_UP_NOT_ALLOWED"; - reason: string; - } + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } | GeneralErrorResponse > { const errorCodeMap = { @@ -816,10 +825,14 @@ export default function getAPIImplementation(): APIInterface { // check for type is done in a parent function but they are kept // here to be on the safe side. if (typeof emailAsUnknown !== "string") - throw new Error("Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError"); + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); if (typeof passwordAsUnknown !== "string") - throw new Error("Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError"); + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); let email: string = emailAsUnknown; let password: string = passwordAsUnknown; From 2cbab88ccebb09fb095297d799c22449923c6824 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 25 Sep 2024 14:24:58 +0530 Subject: [PATCH 7/7] Add detail about breaking change regarding formField value type change to unknown --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 175e36eeb..1cd1ce9dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added OAuth2Provider recipe +### Breaking change + +- Changes type of value in formField object to be `unknown` instead of `string` to add support for accepting any type of value in form fields. + ## [20.1.2] - 2024-09-14 - Fixes formFields to accept non string types as well.