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>
This commit is contained in:
Philip KorsholmandOliver Windall Juhl authored and GitHub committed 2023-03-19 10:52:10 +01:00
1 parent aa690beed7
commit aed7805c0e
6 files changed
+79 -17

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): Error messages for reset tokens
@@ -171,6 +171,16 @@ describe("/admin/users", () => {
}) })
describe("Password reset", () => { 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 () => { it("Doesn't fail when generating password reset token (unauthorized endpoint)", async () => {
const api = useApi() const api = useApi()
@@ -521,5 +521,40 @@ describe("/store/customers", () => {
expect(response.status).toEqual(204) 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",
})
})
}) })
}) })
@@ -66,8 +66,11 @@ export default async (req, res) => {
const validated = await validator(AdminResetPasswordTokenRequest, req.body) const validated = await validator(AdminResetPasswordTokenRequest, req.body)
const userService: UserService = req.scope.resolve("userService") const userService: UserService = req.scope.resolve("userService")
const user = await userService.retrieveByEmail(validated.email) const user = await userService
.retrieveByEmail(validated.email)
.catch(() => undefined)
if (user) {
// Should call a email service provider that sends the token to the user // Should call a email service provider that sends the token to the user
const manager: EntityManager = req.scope.resolve("manager") const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => { await manager.transaction(async (transactionManager) => {
@@ -75,6 +78,7 @@ export default async (req, res) => {
.withTransaction(transactionManager) .withTransaction(transactionManager)
.generateResetPasswordToken(user.id) .generateResetPasswordToken(user.id)
}) })
}
res.sendStatus(204) res.sendStatus(204)
} }
@@ -66,10 +66,11 @@ export default async (req, res) => {
"customerService" "customerService"
) as CustomerService ) as CustomerService
const customer = await customerService.retrieveRegisteredByEmail( const customer = await customerService
validated.email .retrieveRegisteredByEmail(validated.email)
) .catch(() => undefined)
if (customer) {
// Will generate a token and send it to the customer via an email provider // Will generate a token and send it to the customer via an email provider
const manager: EntityManager = req.scope.resolve("manager") const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => { await manager.transaction(async (transactionManager) => {
@@ -77,6 +78,7 @@ export default async (req, res) => {
.withTransaction(transactionManager) .withTransaction(transactionManager)
.generateResetPasswordToken(customer.id) .generateResetPasswordToken(customer.id)
}) })
}
res.sendStatus(204) res.sendStatus(204)
} }
@@ -4,6 +4,7 @@ import jwt, { JwtPayload } from "jsonwebtoken"
import CustomerService from "../../../../services/customer" import CustomerService from "../../../../services/customer"
import { validator } from "../../../../utils/validator" import { validator } from "../../../../utils/validator"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
/** /**
* @oas [post] /store/customers/password-reset * @oas [post] /store/customers/password-reset
@@ -70,20 +71,25 @@ export default async (req, res) => {
)) as StorePostCustomersResetPasswordReq )) as StorePostCustomersResetPasswordReq
const customerService: CustomerService = req.scope.resolve("customerService") const customerService: CustomerService = req.scope.resolve("customerService")
let customer = await customerService.retrieveRegisteredByEmail( let customer
validated.email,
{
select: ["id", "password_hash"],
}
)
const decodedToken = jwt.verify( customer = await customerService
validated.token, .retrieveRegisteredByEmail(validated.email, {
customer.password_hash select: ["id", "password_hash"],
) as JwtPayload })
if (!decodedToken || customer.id !== decodedToken.customer_id) { .catch(() => undefined)
res.status(401).send("Invalid or expired password reset token")
return 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") const manager: EntityManager = req.scope.resolve("manager")