From aed7805c0e64b884007148bde90cfce7bee8aad4 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 19 Mar 2023 10:52:10 +0100 Subject: [PATCH] fix(medusa): Error messages for reset tokens (#3514) * initial * reset password token handling * Create .changeset/old-planes-cross.md --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/old-planes-cross.md | 5 +++ integration-tests/api/__tests__/admin/user.js | 10 ++++++ .../api/__tests__/store/customer.js | 35 +++++++++++++++++++ .../admin/users/reset-password-token.ts | 20 ++++++----- .../store/customers/reset-password-token.ts | 22 ++++++------ .../routes/store/customers/reset-password.ts | 32 ++++++++++------- 6 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 .changeset/old-planes-cross.md diff --git a/.changeset/old-planes-cross.md b/.changeset/old-planes-cross.md new file mode 100644 index 0000000000..60ba639f17 --- /dev/null +++ b/.changeset/old-planes-cross.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): Error messages for reset tokens diff --git a/integration-tests/api/__tests__/admin/user.js b/integration-tests/api/__tests__/admin/user.js index 5acaf2ef22..4870c6158e 100644 --- a/integration-tests/api/__tests__/admin/user.js +++ b/integration-tests/api/__tests__/admin/user.js @@ -171,6 +171,16 @@ describe("/admin/users", () => { }) describe("Password reset", () => { + it("Doesn't fail to fetch user when resetting password for an unknown email (unauthorized endpoint)", async () => { + const api = useApi() + + const resp = await api.post("/admin/users/password-token", { + email: "test-doesnt-exist@test.com", + }) + + expect(resp.status).toEqual(204) + }) + it("Doesn't fail when generating password reset token (unauthorized endpoint)", async () => { const api = useApi() diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index 191359a015..65a5fe2938 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -521,5 +521,40 @@ describe("/store/customers", () => { expect(response.status).toEqual(204) }) + + it("Returns 204 for non-existent customer", async () => { + const api = useApi() + + const response = await api.post(`/store/customers/password-token`, { + email: "non-existent@test.com", + }) + + expect(response.status).toEqual(204) + }) + }) + + describe("POST /store/customers/password-reset", () => { + afterEach(async () => { + await doAfterEach() + }) + + it("Returns 204 for non-existent customer", async () => { + const api = useApi() + + const response = await api + .post(`/store/customers/password-reset`, { + email: "non-existent@test.com", + token: "token", + password: "password", + }) + .catch((error) => { + return error + }) + expect(response.response.status).toEqual(401) + expect(response.response.data).toEqual({ + type: "unauthorized", + message: "Invalid or expired password reset token", + }) + }) }) }) diff --git a/packages/medusa/src/api/routes/admin/users/reset-password-token.ts b/packages/medusa/src/api/routes/admin/users/reset-password-token.ts index 9065bf118d..77e4e86e88 100644 --- a/packages/medusa/src/api/routes/admin/users/reset-password-token.ts +++ b/packages/medusa/src/api/routes/admin/users/reset-password-token.ts @@ -66,15 +66,19 @@ export default async (req, res) => { const validated = await validator(AdminResetPasswordTokenRequest, req.body) const userService: UserService = req.scope.resolve("userService") - const user = await userService.retrieveByEmail(validated.email) + const user = await userService + .retrieveByEmail(validated.email) + .catch(() => undefined) - // Should call a email service provider that sends the token to the user - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await userService - .withTransaction(transactionManager) - .generateResetPasswordToken(user.id) - }) + if (user) { + // Should call a email service provider that sends the token to the user + const manager: EntityManager = req.scope.resolve("manager") + await manager.transaction(async (transactionManager) => { + return await userService + .withTransaction(transactionManager) + .generateResetPasswordToken(user.id) + }) + } res.sendStatus(204) } diff --git a/packages/medusa/src/api/routes/store/customers/reset-password-token.ts b/packages/medusa/src/api/routes/store/customers/reset-password-token.ts index 2476d476d0..3231cd84c0 100644 --- a/packages/medusa/src/api/routes/store/customers/reset-password-token.ts +++ b/packages/medusa/src/api/routes/store/customers/reset-password-token.ts @@ -66,17 +66,19 @@ export default async (req, res) => { "customerService" ) as CustomerService - const customer = await customerService.retrieveRegisteredByEmail( - validated.email - ) + const customer = await customerService + .retrieveRegisteredByEmail(validated.email) + .catch(() => undefined) - // Will generate a token and send it to the customer via an email provider - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await customerService - .withTransaction(transactionManager) - .generateResetPasswordToken(customer.id) - }) + if (customer) { + // Will generate a token and send it to the customer via an email provider + const manager: EntityManager = req.scope.resolve("manager") + await manager.transaction(async (transactionManager) => { + return await customerService + .withTransaction(transactionManager) + .generateResetPasswordToken(customer.id) + }) + } res.sendStatus(204) } diff --git a/packages/medusa/src/api/routes/store/customers/reset-password.ts b/packages/medusa/src/api/routes/store/customers/reset-password.ts index 8607d001f5..cb55f744d9 100644 --- a/packages/medusa/src/api/routes/store/customers/reset-password.ts +++ b/packages/medusa/src/api/routes/store/customers/reset-password.ts @@ -4,6 +4,7 @@ import jwt, { JwtPayload } from "jsonwebtoken" import CustomerService from "../../../../services/customer" import { validator } from "../../../../utils/validator" import { EntityManager } from "typeorm" +import { MedusaError } from "medusa-core-utils" /** * @oas [post] /store/customers/password-reset @@ -70,20 +71,25 @@ export default async (req, res) => { )) as StorePostCustomersResetPasswordReq const customerService: CustomerService = req.scope.resolve("customerService") - let customer = await customerService.retrieveRegisteredByEmail( - validated.email, - { - select: ["id", "password_hash"], - } - ) + let customer - const decodedToken = jwt.verify( - validated.token, - customer.password_hash - ) as JwtPayload - if (!decodedToken || customer.id !== decodedToken.customer_id) { - res.status(401).send("Invalid or expired password reset token") - return + customer = await customerService + .retrieveRegisteredByEmail(validated.email, { + select: ["id", "password_hash"], + }) + .catch(() => undefined) + + const decodedToken = customer + ? jwt.verify(validated.token, customer.password_hash) + : undefined + if ( + !decodedToken || + customer.id !== (decodedToken as JwtPayload)?.customer_id + ) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + "Invalid or expired password reset token" + ) } const manager: EntityManager = req.scope.resolve("manager")