diff --git a/integration-tests/http/__tests__/auth/admin/auth.spec.ts b/integration-tests/http/__tests__/auth/admin/auth.spec.ts index fa7e3a5019..bb44c6bc3f 100644 --- a/integration-tests/http/__tests__/auth/admin/auth.spec.ts +++ b/integration-tests/http/__tests__/auth/admin/auth.spec.ts @@ -230,6 +230,57 @@ medusaIntegrationTestRunner({ expect(login.data).toEqual({ token: expect.any(String) }) }) + it("should ensure you can only update password", async () => { + // Register user + await api.post("/auth/user/emailpass/register", { + email: "test@medusa-commerce.com", + password: "secret_password", + }) + + // The token won't be part of the Rest API response, so we need to generate it manually + const { result } = await generateResetPasswordTokenWorkflow( + container + ).run({ + input: { + entityId: "test@medusa-commerce.com", + actorType: "user", + provider: "emailpass", + secret: "test", + }, + }) + + const response = await api.post( + `/auth/user/emailpass/update?token=${result}`, + { + email: "test+new@medusa-commerce.com", + password: "new_password", + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toEqual({ success: true }) + + const failedLogin = await api + .post("/auth/user/emailpass", { + email: "test+new@medusa-commerce.com", + password: "new_password", + }) + .catch((e) => e) + + expect(failedLogin.response.status).toEqual(401) + expect(failedLogin.response.data.message).toEqual( + "Invalid email or password" + ) + + const login = await api.post("/auth/user/emailpass", { + email: "test@medusa-commerce.com", + password: "new_password", + }) + + expect(login.status).toEqual(200) + expect(login.data).toEqual({ token: expect.any(String) }) + }) + it("should fail if token has expired", async () => { jest.useFakeTimers() @@ -286,6 +337,76 @@ medusaIntegrationTestRunner({ expect(response.response.status).toEqual(401) expect(response.response.data.message).toEqual("Invalid token") }) + + it("should fail if update is attempted on different actor type", async () => { + jest.useFakeTimers() + + // Register user + await api.post("/auth/user/emailpass/register", { + email: "test@medusa-commerce.com", + password: "secret_password", + }) + + // The token won't be part of the Rest API response, so we need to generate it manually + const { result } = await generateResetPasswordTokenWorkflow( + container + ).run({ + input: { + entityId: "test@medusa-commerce.com", + actorType: "user", + provider: "emailpass", + secret: "test", + }, + }) + + // Advance time by 15 minutes + jest.advanceTimersByTime(15 * 60 * 1000) + + const response = await api + .post(`/auth/customer/emailpass/update?token=${result}`, { + email: "test@medusa-commerce.com", + password: "new_password", + }) + .catch((e) => e) + + expect(response.response.status).toEqual(401) + expect(response.response.data.message).toEqual("Invalid token") + }) + + it("should fail if token secret is incorrect", async () => { + jest.useFakeTimers() + + // Register user + await api.post("/auth/user/emailpass/register", { + email: "test@medusa-commerce.com", + password: "secret_password", + }) + + // The token won't be part of the Rest API response, so we need to generate it manually + const { result } = await generateResetPasswordTokenWorkflow( + container + ).run({ + input: { + entityId: "test@medusa-commerce.com", + actorType: "user", + provider: "emailpass", + secret: "incorrect_secret", + }, + }) + + // Advance time by 15 minutes + jest.advanceTimersByTime(15 * 60 * 1000) + + const response = await api + .post(`/auth/user/emailpass/update?token=${result}`, { + email: "test@medusa-commerce.com", + password: "new_password", + }) + .catch((e) => e) + + expect(response.response.status).toEqual(401) + expect(response.response.data.message).toEqual("Invalid token") + }) }) it("should refresh the token successfully", async () => { diff --git a/packages/core/core-flows/src/auth/workflows/generate-reset-password-token.ts b/packages/core/core-flows/src/auth/workflows/generate-reset-password-token.ts index a52351c138..7da4db2183 100644 --- a/packages/core/core-flows/src/auth/workflows/generate-reset-password-token.ts +++ b/packages/core/core-flows/src/auth/workflows/generate-reset-password-token.ts @@ -45,6 +45,7 @@ export const generateResetPasswordTokenWorkflow = createWorkflow( { entity_id: input.entityId, provider: input.provider, + actor_type: input.actorType, }, { secret: input.secret, diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/update/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/update/route.ts index 2f910f8181..b8df9b79a0 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/update/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/update/route.ts @@ -13,9 +13,14 @@ export const POST = async ( const authService = req.scope.resolve(Modules.AUTH) + const updateData = { + ...(req.body as Record), + entity_id: req.auth_context.actor_id, // comes from the validated token + } + const { authIdentity, success, error } = await authService.updateProvider( auth_provider, - req.body as Record + updateData ) if (success && authIdentity) { diff --git a/packages/medusa/src/api/auth/utils/validate-token.ts b/packages/medusa/src/api/auth/utils/validate-token.ts index c06b9358ec..84762fd29d 100644 --- a/packages/medusa/src/api/auth/utils/validate-token.ts +++ b/packages/medusa/src/api/auth/utils/validate-token.ts @@ -46,6 +46,11 @@ export const validateToken = () => { return next(errorObject) } + // E.g. token was requested for a customer, but attempted used for a user + if (decoded?.actor_type !== actor_type) { + return next(errorObject) + } + const [providerIdentity] = await authModule.listProviderIdentities( { entity_id: decoded.entity_id, @@ -60,17 +65,15 @@ export const validateToken = () => { return next(errorObject) } - let verified: JwtPayload | null = null - try { - verified = verify(token as string, http.jwtSecret as string) as JwtPayload + verify(token as string, http.jwtSecret as string) as JwtPayload } catch (error) { return next(errorObject) } req_.auth_context = { actor_type, - auth_identity_id: verified.auth_identity_id!, + auth_identity_id: providerIdentity.auth_identity_id!, actor_id: providerIdentity.entity_id, app_metadata: {}, } diff --git a/packages/modules/providers/auth-emailpass/src/services/emailpass.ts b/packages/modules/providers/auth-emailpass/src/services/emailpass.ts index 6f9743b9ec..7819eecf6e 100644 --- a/packages/modules/providers/auth-emailpass/src/services/emailpass.ts +++ b/packages/modules/providers/auth-emailpass/src/services/emailpass.ts @@ -43,15 +43,15 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { } async update( - data: { email: string; password: string }, + data: { password: string; entity_id: string }, authIdentityService: AuthIdentityProviderService ) { - const { email, password } = data ?? {} + const { password, entity_id } = data ?? {} - if (!email || !isString(email)) { + if (!entity_id) { return { success: false, - error: `Cannot update ${this.provider} provider identity without email`, + error: `Cannot update ${this.provider} provider identity without entity_id`, } } @@ -64,7 +64,7 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { try { const passwordHash = await this.hashPassword(password) - authIdentity = await authIdentityService.update(email, { + authIdentity = await authIdentityService.update(entity_id, { provider_metadata: { password: passwordHash, },