feat(): Add support for jwt asymetric keys (#12813)
* feat(): Add support for jwt asymetric keys * Create early-chefs-chew.md * fix unit tests * Add verify options support * feedback * fix unit tests
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
import { generateResetPasswordTokenWorkflow } from "@medusajs/core-flows"
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import path from "path"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
jest.setTimeout(300000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
medusaConfigFile: path.join(__dirname, "../../../__fixtures__/auth"),
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let container
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
describe("Full authentication lifecycle", () => {
|
||||
it("Invite + registration + authentication flow", async () => {
|
||||
// Create invite
|
||||
const { token: inviteToken } = (
|
||||
await api.post(
|
||||
"/admin/invites",
|
||||
{ email: "newadmin@medusa.js" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.invite
|
||||
|
||||
// Register identity
|
||||
const signup = await api.post("/auth/user/emailpass/register", {
|
||||
email: "newadmin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
|
||||
expect(signup.status).toEqual(200)
|
||||
expect(signup.data).toEqual({ token: expect.any(String) })
|
||||
|
||||
// Accept invite
|
||||
const response = await api.post(
|
||||
`/admin/invites/accept?token=${inviteToken}`,
|
||||
{
|
||||
email: "newadmin@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${signup.data.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({
|
||||
user: expect.objectContaining({
|
||||
email: "newadmin@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
}),
|
||||
})
|
||||
|
||||
// Sign in
|
||||
const login = await api.post("/auth/user/emailpass", {
|
||||
email: "newadmin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
expect(login.status).toEqual(200)
|
||||
expect(login.data).toEqual({ token: expect.any(String) })
|
||||
|
||||
// Convert token to session
|
||||
const createSession = await api.post(
|
||||
"/auth/session",
|
||||
{},
|
||||
{ headers: { authorization: `Bearer ${login.data.token}` } }
|
||||
)
|
||||
expect(createSession.status).toEqual(200)
|
||||
|
||||
// Extract cookie
|
||||
const [cookie] = createSession.headers["set-cookie"][0].split(";")
|
||||
expect(cookie).toEqual(expect.stringContaining("connect.sid"))
|
||||
|
||||
const cookieHeader = {
|
||||
headers: { Cookie: cookie },
|
||||
}
|
||||
|
||||
// Perform cookie authenticated request
|
||||
const authedRequest = await api.get(
|
||||
"/admin/products?limit=1",
|
||||
cookieHeader
|
||||
)
|
||||
expect(authedRequest.status).toEqual(200)
|
||||
|
||||
// Sign out
|
||||
const signOutRequest = await api.delete("/auth/session", cookieHeader)
|
||||
expect(signOutRequest.status).toEqual(200)
|
||||
|
||||
// Attempt to perform authenticated request
|
||||
const unAuthedRequest = await api
|
||||
.get("/admin/products?limit=1", cookieHeader)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(unAuthedRequest.response.status).toEqual(401)
|
||||
})
|
||||
|
||||
it("should respond with 401 on register, if email already exists", async () => {
|
||||
const signup = await api
|
||||
.post("/auth/user/emailpass/register", {
|
||||
email: "admin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(signup.response.status).toEqual(401)
|
||||
expect(signup.response.data.message).toEqual(
|
||||
"Identity with email already exists"
|
||||
)
|
||||
})
|
||||
|
||||
it("should respond with 401 on sign in, if email does not exist", async () => {
|
||||
const signup = await api
|
||||
.post("/auth/user/emailpass", {
|
||||
email: "john@doe.com",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(signup.response.status).toEqual(401)
|
||||
expect(signup.response.data.message).toEqual(
|
||||
"Invalid email or password"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Reset password flows", () => {
|
||||
it("should generate a reset password token", async () => {
|
||||
const response = await api.post("/auth/user/emailpass/reset-password", {
|
||||
identifier: "admin@medusa.js",
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should fail if identifier is not provided", async () => {
|
||||
const errResponse = await api
|
||||
.post("/auth/user/emailpass/reset-password", {})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(errResponse.response.data.message).toEqual(
|
||||
"Invalid request: Field 'identifier' is required"
|
||||
)
|
||||
expect(errResponse.response.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("should fail to generate token for non-existing user, but still respond with 201", async () => {
|
||||
const response = await api.post("/auth/user/emailpass/reset-password", {
|
||||
identifier: "non-existing-user@medusa.js",
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should fail to generate token for existing user but no provider, but still respond with 201", async () => {
|
||||
const response = await api.post(
|
||||
"/auth/user/non-existing-provider/reset-password",
|
||||
{ identifier: "admin@medusa.js" }
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should successfully reset 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 { http } = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
const { result } = await generateResetPasswordTokenWorkflow(
|
||||
container
|
||||
).run({
|
||||
input: {
|
||||
entityId: "test@medusa-commerce.com",
|
||||
actorType: "user",
|
||||
provider: "emailpass",
|
||||
secret: http.jwtSecret!,
|
||||
jwtOptions: http.jwtOptions,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await api.post(
|
||||
`/auth/user/emailpass/update`,
|
||||
{
|
||||
password: "new_password",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${result}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({ success: true })
|
||||
|
||||
const failedLogin = await api
|
||||
.post("/auth/user/emailpass", {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "secret_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 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 { http } = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
const { result } = await generateResetPasswordTokenWorkflow(
|
||||
container
|
||||
).run({
|
||||
input: {
|
||||
entityId: "test@medusa-commerce.com",
|
||||
actorType: "user",
|
||||
provider: "emailpass",
|
||||
secret: http.jwtSecret!,
|
||||
jwtOptions: http.jwtOptions,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await api.post(
|
||||
`/auth/user/emailpass/update`,
|
||||
{
|
||||
email: "test+new@medusa-commerce.com",
|
||||
password: "new_password",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${result}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
// 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/user/emailpass/update`,
|
||||
{
|
||||
password: "new_password",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${result}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.response.status).toEqual(401)
|
||||
expect(response.response.data.message).toEqual("Invalid token")
|
||||
})
|
||||
|
||||
it("should fail if no token is passed", async () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
// Register user
|
||||
await api.post("/auth/user/emailpass/register", {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "secret_password",
|
||||
})
|
||||
|
||||
// Advance time by 15 minutes
|
||||
jest.advanceTimersByTime(15 * 60 * 1000)
|
||||
|
||||
const response = await api
|
||||
.post(`/auth/user/emailpass/update`, {
|
||||
email: "test@medusa-commerce.com",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
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`,
|
||||
{
|
||||
password: "new_password",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${result}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.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`,
|
||||
{
|
||||
password: "new_password",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${result}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.response.status).toEqual(401)
|
||||
expect(response.response.data.message).toEqual("Invalid token")
|
||||
})
|
||||
})
|
||||
|
||||
it("should refresh the token successfully", async () => {
|
||||
// Make sure issue date is later than the admin one
|
||||
jest.useFakeTimers()
|
||||
jest.advanceTimersByTime(2000)
|
||||
|
||||
const resp = await api.post("/auth/token/refresh", {}, adminHeaders)
|
||||
const decodedOriginalToken = jwt.decode(
|
||||
adminHeaders.headers["authorization"].split(" ")[1]
|
||||
) as any
|
||||
const decodedRefreshedToken = jwt.decode(resp.data.token) as any
|
||||
|
||||
expect(decodedOriginalToken).toEqual(
|
||||
expect.objectContaining({
|
||||
actor_id: decodedRefreshedToken.actor_id,
|
||||
actor_type: decodedRefreshedToken.actor_type,
|
||||
auth_identity_id: decodedRefreshedToken.auth_identity_id,
|
||||
})
|
||||
)
|
||||
|
||||
expect(decodedOriginalToken.iat).toBeLessThan(decodedRefreshedToken.iat)
|
||||
expect(decodedOriginalToken.exp).toBeLessThan(decodedRefreshedToken.exp)
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user