feat(medusa): Authentication overhaul (#4064)
* implemented bearer auth * changed naming strat * changed session auth to not use jwt * typo * changed auth header prefix for admin api token auth * fixed supporting functions to work with new session type * removed database calls for bearer auth improving performance * removed unused deps * changed auth in tests * added integration tests * Accepted suggested change Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> * Typo Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * more typos Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * proper formatting Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * removed endregion Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * removed startregion Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * fixed admin JWT integration test * added more fixes to integration tests * Update OAS * Create fluffy-donkeys-hope.md * created API reference for new auth * implemented getToken in medusa-js * Apply suggestions from code review Co-authored-by: Shahed Nasser <shahednasser@gmail.com> * Apply suggestions from code review Co-authored-by: Shahed Nasser <shahednasser@gmail.com> * deleted files which should be autogenerated * Update fluffy-donkeys-hope.md * JSDoc update Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * added missing route exports * implemented runtime domain safety in jwt token manager * fixed jwt manager * lint get-token files * Update fluffy-donkeys-hope.md * Revert "deleted files which should be autogenerated" This reverts commit cd5e86623b822e6a6ac37322b952143ccc493df9. * Revert "Apply suggestions from code review" This reverts commit f02f07ce58fd9fcc2dfc80cadbb9df2665108d65. * Revert "created API reference for new auth" This reverts commit c9eafbb36453f5cf8047c79e94f470cb2d023c7d. * renamed header for sending api access tokens * medusa-js - changed apiKey header --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Co-authored-by: olivermrbl <oliver@mrbltech.com> Co-authored-by: Shahed Nasser <shahednasser@gmail.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import passport from "passport"
|
||||
export default (): RequestHandler => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
passport.authenticate(
|
||||
["store-jwt", "bearer"],
|
||||
["store-session", "store-bearer"],
|
||||
{ session: false },
|
||||
(err, user) => {
|
||||
if (err) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import passport from "passport"
|
||||
|
||||
export default (): RequestHandler => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
passport.authenticate(["admin-jwt", "bearer"], { session: false })(
|
||||
passport.authenticate(["admin-session", "admin-bearer", "admin-api-token"], { session: false })(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
|
||||
@@ -7,7 +7,7 @@ export default (): RequestHandler => {
|
||||
return next()
|
||||
}
|
||||
|
||||
passport.authenticate(["store-jwt", "bearer"], { session: false })(
|
||||
passport.authenticate(["store-session", "store-bearer"], { session: false })(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
|
||||
@@ -66,15 +66,6 @@ import { validator } from "../../../../utils/validator"
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
projectConfig: { jwt_secret },
|
||||
} = req.scope.resolve("configModule")
|
||||
if (!jwt_secret) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Please configure jwt_secret in your environment"
|
||||
)
|
||||
}
|
||||
const validated = await validator(AdminPostAuthReq, req.body)
|
||||
|
||||
const authService: AuthService = req.scope.resolve("authService")
|
||||
@@ -86,10 +77,8 @@ export default async (req, res) => {
|
||||
})
|
||||
|
||||
if (result.success && result.user) {
|
||||
// Add JWT to cookie
|
||||
req.session.jwt = jwt.sign({ userId: result.user.id }, jwt_secret, {
|
||||
expiresIn: "24h",
|
||||
})
|
||||
// Set user id on session, this is stored on the server.
|
||||
req.session.user_id = result.user.id
|
||||
|
||||
const cleanRes = _.omit(result.user, ["password_hash"])
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
req.session.destroy()
|
||||
res.status(200).end()
|
||||
if (req.session.customer_id) { // if we are also logged in as a customer, persist that session
|
||||
delete req.session.user_id
|
||||
} else { // otherwise, destroy the session
|
||||
req.session.destroy()
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -52,8 +52,10 @@ import _ from "lodash"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id || req.user.userId
|
||||
|
||||
const userService: UserService = req.scope.resolve("userService")
|
||||
const user = await userService.retrieve(req.user.userId)
|
||||
const user = await userService.retrieve(userId)
|
||||
|
||||
const cleanRes = _.omit(user, ["password_hash"])
|
||||
res.status(200).json({ user: cleanRes })
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import jwt from "jsonwebtoken"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import AuthService from "../../../../services/auth"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { AdminPostAuthReq } from "./create-session"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/token
|
||||
* operationId: "PostToken"
|
||||
* summary: "User Login (JWT)"
|
||||
* x-authenticated: false
|
||||
* description: "After a successful login, a JWT token is returned for subsequent authorization."
|
||||
* parameters:
|
||||
* - (body) email=* {string} The User's email.
|
||||
* - (body) password=* {string} The User's password.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/AdminPostAuthReq"
|
||||
* x-codegen:
|
||||
* method: getToken
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
* source: |
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
* medusa.admin.auth.getToken({
|
||||
* email: 'user@example.com',
|
||||
* password: 'supersecret'
|
||||
* })
|
||||
* .then(({ accessToken }) => {
|
||||
* console.log(accessToekn);
|
||||
* });
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request POST 'https://medusa-url.com/admin/auth/token' \
|
||||
* --header 'Content-Type: application/json' \
|
||||
* --data-raw '{
|
||||
* "email": "user@example.com",
|
||||
* "password": "supersecret"
|
||||
* }'
|
||||
* tags:
|
||||
* - Auth
|
||||
* responses:
|
||||
* "200":
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/AdminBearerAuthRes"
|
||||
* "400":
|
||||
* $ref: "#/components/responses/400_error"
|
||||
* "401":
|
||||
* $ref: "#/components/responses/incorrect_credentials"
|
||||
* "404":
|
||||
* $ref: "#/components/responses/not_found_error"
|
||||
* "409":
|
||||
* $ref: "#/components/responses/invalid_state_error"
|
||||
* "422":
|
||||
* $ref: "#/components/responses/invalid_request_error"
|
||||
* "500":
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
projectConfig: { jwt_secret },
|
||||
} = req.scope.resolve("configModule")
|
||||
if (!jwt_secret) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Please configure jwt_secret in your environment"
|
||||
)
|
||||
}
|
||||
const validated = await validator(AdminPostAuthReq, req.body)
|
||||
|
||||
const authService: AuthService = req.scope.resolve("authService")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const result = await manager.transaction(async (transactionManager) => {
|
||||
return await authService
|
||||
.withTransaction(transactionManager)
|
||||
.authenticate(validated.email, validated.password)
|
||||
})
|
||||
|
||||
if (result.success && result.user) {
|
||||
// Create jwt token to send back
|
||||
const token = jwt.sign(
|
||||
{ user_id: result.user.id, domain: "admin" },
|
||||
jwt_secret,
|
||||
{
|
||||
expiresIn: "24h",
|
||||
}
|
||||
)
|
||||
|
||||
res.json({ access_token: token })
|
||||
} else {
|
||||
res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export default (app) => {
|
||||
middlewares.authenticate(),
|
||||
middlewares.wrap(require("./get-session").default)
|
||||
)
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-session").default))
|
||||
|
||||
route.delete(
|
||||
@@ -20,6 +21,8 @@ export default (app) => {
|
||||
middlewares.wrap(require("./delete-session").default)
|
||||
)
|
||||
|
||||
route.post("/token", middlewares.wrap(require("./get-token").default))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -37,6 +40,19 @@ export type AdminAuthRes = {
|
||||
user: Omit<User, "password_hash">
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminBearerAuthRes
|
||||
* type: object
|
||||
* properties:
|
||||
* accessToken:
|
||||
* description: Access token for subsequent authorization.
|
||||
* type: string
|
||||
*/
|
||||
export type AdminBearerAuthRes = {
|
||||
access_token: string
|
||||
}
|
||||
|
||||
export * from "./create-session"
|
||||
export * from "./delete-session"
|
||||
export * from "./get-session"
|
||||
export * from "./get-token"
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("POST /invites/:invite_id/resend", () => {
|
||||
subject = await request("POST", `/admin/invites/invite_test/resend`, {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
id: "test_user",
|
||||
userId: "test_user",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -79,17 +79,8 @@ export default async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Add JWT to cookie
|
||||
const {
|
||||
projectConfig: { jwt_secret },
|
||||
} = req.scope.resolve("configModule")
|
||||
req.session.jwt_store = jwt.sign(
|
||||
{ customer_id: result.customer?.id },
|
||||
jwt_secret!,
|
||||
{
|
||||
expiresIn: "30d",
|
||||
}
|
||||
)
|
||||
// Set customer id on session, this is stored on the server.
|
||||
req.session.customer_id = result.customer?.id
|
||||
|
||||
const customerService: CustomerService = req.scope.resolve("customerService")
|
||||
const customer = await customerService.retrieve(result.customer?.id || "", {
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
req.session.jwt_store = {}
|
||||
res.json({})
|
||||
if(req.session.user_id) { // if we are also logged in as a user, persist that session
|
||||
delete req.session.customer_id
|
||||
} else { // otherwise, destroy the session
|
||||
req.session.destroy()
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import jwt from "jsonwebtoken"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import AuthService from "../../../../services/auth"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { StorePostAuthReq } from "./create-session"
|
||||
|
||||
/**
|
||||
* @oas [post] /store/token
|
||||
* operationId: "PostToken"
|
||||
* summary: "Customer Login (JWT)"
|
||||
* x-authenticated: false
|
||||
* description: "After a successful login, a JWT token is returned for subsequent authorization."
|
||||
* parameters:
|
||||
* - (body) email=* {string} The User's email.
|
||||
* - (body) password=* {string} The User's password.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/StorePostAuthReq"
|
||||
* x-codegen:
|
||||
* method: getToken
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
* source: |
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
* medusa.store.auth.getToken({
|
||||
* email: 'user@example.com',
|
||||
* password: 'supersecret'
|
||||
* })
|
||||
* .then(({ accessToken }) => {
|
||||
* console.log(accessToken);
|
||||
* });
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request POST 'https://medusa-url.com/store/auth/token' \
|
||||
* --header 'Content-Type: application/json' \
|
||||
* --data-raw '{
|
||||
* "email": "user@example.com",
|
||||
* "password": "supersecret"
|
||||
* }'
|
||||
* tags:
|
||||
* - Auth
|
||||
* responses:
|
||||
* "200":
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/StoreBearerAuthRes"
|
||||
* "400":
|
||||
* $ref: "#/components/responses/400_error"
|
||||
* "401":
|
||||
* $ref: "#/components/responses/incorrect_credentials"
|
||||
* "404":
|
||||
* $ref: "#/components/responses/not_found_error"
|
||||
* "409":
|
||||
* $ref: "#/components/responses/invalid_state_error"
|
||||
* "422":
|
||||
* $ref: "#/components/responses/invalid_request_error"
|
||||
* "500":
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
projectConfig: { jwt_secret },
|
||||
} = req.scope.resolve("configModule")
|
||||
if (!jwt_secret) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Please configure jwt_secret in your environment"
|
||||
)
|
||||
}
|
||||
const validated = await validator(StorePostAuthReq, req.body)
|
||||
|
||||
const authService: AuthService = req.scope.resolve("authService")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const result = await manager.transaction(async (transactionManager) => {
|
||||
return await authService
|
||||
.withTransaction(transactionManager)
|
||||
.authenticateCustomer(validated.email, validated.password)
|
||||
})
|
||||
|
||||
if (result.success && result.customer) {
|
||||
// Create jwt token to send back
|
||||
const token = jwt.sign(
|
||||
{ customer_id: result.customer.id, domain: "store" },
|
||||
jwt_secret,
|
||||
{
|
||||
expiresIn: "30d",
|
||||
}
|
||||
)
|
||||
|
||||
res.json({ access_token: token })
|
||||
} else {
|
||||
res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default (app) => {
|
||||
route.get("/:email", middlewares.wrap(require("./exists").default))
|
||||
route.delete("/", middlewares.wrap(require("./delete-session").default))
|
||||
route.post("/", middlewares.wrap(require("./create-session").default))
|
||||
route.post("/token", middlewares.wrap(require("./get-token").default))
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -41,6 +42,18 @@ export type StoreAuthRes = {
|
||||
customer: Customer
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema StoreBearerAuthRes
|
||||
* type: object
|
||||
* properties:
|
||||
* accessToken:
|
||||
* description: Access token for subsequent authorization.
|
||||
* type: string
|
||||
*/
|
||||
export type StoreBearerAuthRes = {
|
||||
access_token: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema StoreGetAuthEmailRes
|
||||
* type: object
|
||||
@@ -59,3 +72,4 @@ export * from "./create-session"
|
||||
export * from "./delete-session"
|
||||
export * from "./exists"
|
||||
export * from "./get-session"
|
||||
export * from "./get-token"
|
||||
|
||||
@@ -103,13 +103,7 @@ export default async (req, res) => {
|
||||
select: defaultStoreCustomersFields,
|
||||
})
|
||||
|
||||
// Add JWT to cookie
|
||||
const {
|
||||
projectConfig: { jwt_secret },
|
||||
} = req.scope.resolve("configModule")
|
||||
req.session.jwt_store = jwt.sign({ customer_id: customer.id }, jwt_secret!, {
|
||||
expiresIn: "30d",
|
||||
})
|
||||
req.session.customer_id = customer.id
|
||||
|
||||
res.status(200).json({ customer })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user