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:
co-authored by
Carlos R. L. Rodrigues
Oliver Windall Juhl
Shahed Nasser
olivermrbl
parent
07e65f5aba
commit
2caff2efc7
@@ -0,0 +1,11 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import { SetRelation, Merge } from "../core/ModelUtils"
|
||||
|
||||
export interface AdminBearerAuthRes {
|
||||
/**
|
||||
* Access token for subsequent authorization.
|
||||
*/
|
||||
accessToken?: string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import { SetRelation, Merge } from "../core/ModelUtils"
|
||||
|
||||
export interface StoreBearerAuthRes {
|
||||
/**
|
||||
* Access token for subsequent authorization.
|
||||
*/
|
||||
accessToken?: string
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export type { AdminAppsRes } from "./AdminAppsRes"
|
||||
export type { AdminAuthRes } from "./AdminAuthRes"
|
||||
export type { AdminBatchJobListRes } from "./AdminBatchJobListRes"
|
||||
export type { AdminBatchJobRes } from "./AdminBatchJobRes"
|
||||
export type { AdminBearerAuthRes } from "./AdminBearerAuthRes"
|
||||
export type { AdminCollectionsDeleteRes } from "./AdminCollectionsDeleteRes"
|
||||
export type { AdminCollectionsListRes } from "./AdminCollectionsListRes"
|
||||
export type { AdminCollectionsRes } from "./AdminCollectionsRes"
|
||||
@@ -396,6 +397,7 @@ export type { StockLocationDTO } from "./StockLocationDTO"
|
||||
export type { StockLocationExpandedDTO } from "./StockLocationExpandedDTO"
|
||||
export type { Store } from "./Store"
|
||||
export type { StoreAuthRes } from "./StoreAuthRes"
|
||||
export type { StoreBearerAuthRes } from "./StoreBearerAuthRes"
|
||||
export type { StoreCartShippingOptionsListRes } from "./StoreCartShippingOptionsListRes"
|
||||
export type { StoreCartsRes } from "./StoreCartsRes"
|
||||
export type { StoreCollectionsListRes } from "./StoreCollectionsListRes"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* `JwtTokenManager` holds JWT tokens in state.
|
||||
*/
|
||||
class JwtTokenManager {
|
||||
private adminJwt: string | null = null;
|
||||
private storeJwt: string | null = null;
|
||||
|
||||
/**
|
||||
* Set a store or admin jwt token to be sent with each request.
|
||||
*/
|
||||
public registerJwt(token: string, domain: "admin" | "store") {
|
||||
if (domain === "admin") {
|
||||
this.adminJwt = token;
|
||||
} else if (domain === "store") {
|
||||
this.storeJwt = token;
|
||||
} else {
|
||||
throw new Error(`'domain' must be wither 'admin' or 'store' received ${domain}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the store or admin jwt token
|
||||
*/
|
||||
public getJwt(domain: "admin" | "store") {
|
||||
if (domain === "admin") {
|
||||
return this.adminJwt;
|
||||
} else if (domain === "store") {
|
||||
return this.storeJwt;
|
||||
} else {
|
||||
throw new Error(`'domain' must be wither 'admin' or 'store' received ${domain}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export singleton instance.
|
||||
*/
|
||||
export default new JwtTokenManager()
|
||||
@@ -3,6 +3,7 @@ import * as rax from "retry-axios"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
import KeyManager from "./key-manager"
|
||||
import JwtTokenManager from "./jwt-token-manager"
|
||||
|
||||
const unAuthenticatedAdminEndpoints = {
|
||||
"/admin/auth": "POST",
|
||||
@@ -125,7 +126,16 @@ class Client {
|
||||
if (this.config.apiKey && this.requiresAuthentication(path, method)) {
|
||||
defaultHeaders = {
|
||||
...defaultHeaders,
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
"x-medusa-access-token": this.config.apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
const domain: "admin" | "store" = path.includes("admin") ? "admin" : "store"
|
||||
|
||||
if (JwtTokenManager.getJwt(domain)) {
|
||||
defaultHeaders = {
|
||||
...defaultHeaders,
|
||||
Authorization: `Bearer ${JwtTokenManager.getJwt(domain)}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AdminAuthRes, AdminPostAuthReq } from "@medusajs/medusa"
|
||||
import { AdminAuthRes, AdminPostAuthReq, AdminBearerAuthRes } from "@medusajs/medusa"
|
||||
import { ResponsePromise } from "../../typings"
|
||||
import JwtTokenManager from "../../jwt-token-manager"
|
||||
import BaseResource from "../base"
|
||||
|
||||
class AdminAuthResource extends BaseResource {
|
||||
@@ -41,6 +42,25 @@ class AdminAuthResource extends BaseResource {
|
||||
const path = `/admin/auth`
|
||||
return this.client.request("POST", path, payload, {}, customHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Retrieves a new JWT access token
|
||||
* @param {AdminPostAuthReq} payload
|
||||
* @param customHeaders
|
||||
* @return {ResponsePromise<AdminBearerAuthRes>}
|
||||
*/
|
||||
getToken(
|
||||
payload: AdminPostAuthReq,
|
||||
customHeaders: Record<string, any> = {}
|
||||
): ResponsePromise<AdminBearerAuthRes> {
|
||||
const path = `/admin/auth/token`
|
||||
return this.client.request("POST", path, payload, {}, customHeaders)
|
||||
.then((res) => {
|
||||
JwtTokenManager.registerJwt(res.access_token, "admin");
|
||||
|
||||
return res
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminAuthResource
|
||||
|
||||
@@ -2,8 +2,10 @@ import {
|
||||
StoreGetAuthEmailRes,
|
||||
StorePostAuthReq,
|
||||
StoreAuthRes,
|
||||
StoreBearerAuthRes,
|
||||
} from "@medusajs/medusa"
|
||||
import { ResponsePromise } from "../typings"
|
||||
import JwtTokenManager from "../jwt-token-manager"
|
||||
import BaseResource from "./base"
|
||||
|
||||
class AuthResource extends BaseResource {
|
||||
@@ -48,6 +50,25 @@ class AuthResource extends BaseResource {
|
||||
const path = `/store/auth/${email}`
|
||||
return this.client.request("GET", path, undefined, {}, customHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Retrieves a new JWT access token
|
||||
* @param {AdminPostAuthReq} payload
|
||||
* @param customHeaders
|
||||
* @return {ResponsePromise<AdminBearerAuthRes>}
|
||||
*/
|
||||
getToken(
|
||||
payload: StorePostAuthReq,
|
||||
customHeaders: Record<string, any> = {}
|
||||
): ResponsePromise<StoreBearerAuthRes> {
|
||||
const path = `/store/auth/token`
|
||||
return this.client.request("POST", path, payload, {}, customHeaders)
|
||||
.then((res) => {
|
||||
JwtTokenManager.registerJwt(res.data.access_token, "store");
|
||||
|
||||
return res
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthResource
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"node-schedule": "^2.1.1",
|
||||
"papaparse": "5.3.2",
|
||||
"passport": "^0.6.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^8.11.2",
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ container.register("modulesHelper", asValue(moduleHelper))
|
||||
container.register("configModule", asValue(config))
|
||||
container.register({
|
||||
logger: asValue({
|
||||
error: () => {},
|
||||
error: () => { },
|
||||
}),
|
||||
manager: asValue(MockManager),
|
||||
})
|
||||
@@ -145,31 +145,20 @@ export async function request(method, url, opts = {}) {
|
||||
)
|
||||
headers.Cookie = headers.Cookie || ""
|
||||
if (opts.adminSession) {
|
||||
const adminSession = { ...opts.adminSession }
|
||||
const token = jwt.sign(
|
||||
{ user_id: opts.adminSession.userId || opts.adminSession.jwt?.userId, domain: "admin" },
|
||||
config.projectConfig.jwt_secret
|
||||
)
|
||||
|
||||
if (adminSession.jwt) {
|
||||
adminSession.jwt = jwt.sign(
|
||||
adminSession.jwt,
|
||||
config.projectConfig.jwt_secret,
|
||||
{
|
||||
expiresIn: "30m",
|
||||
}
|
||||
)
|
||||
}
|
||||
headers.Cookie = JSON.stringify(adminSession) || ""
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
if (opts.clientSession) {
|
||||
if (opts.clientSession.jwt) {
|
||||
opts.clientSession.jwt_store = jwt.sign(
|
||||
opts.clientSession.jwt,
|
||||
config.projectConfig.jwt_secret,
|
||||
{
|
||||
expiresIn: "30d",
|
||||
}
|
||||
)
|
||||
}
|
||||
const token = jwt.sign(
|
||||
{ customer_id: opts.clientSession.customer_id || opts.clientSession.jwt?.customer_id, domain: "store" },
|
||||
config.projectConfig.jwt_secret
|
||||
)
|
||||
|
||||
headers.Cookie = JSON.stringify(opts.clientSession) || ""
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
for (const name in headers) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Express } from "express"
|
||||
import passport from "passport"
|
||||
import { Strategy as BearerStrategy } from "passport-http-bearer"
|
||||
import { Strategy as JWTStrategy } from "passport-jwt"
|
||||
import { Strategy as JWTStrategy, ExtractJwt } from "passport-jwt"
|
||||
import { Strategy as LocalStrategy } from "passport-local"
|
||||
import { Strategy as CustomStrategy } from "passport-custom"
|
||||
import { AuthService } from "../services"
|
||||
import { ConfigModule, MedusaContainer } from "../types/global"
|
||||
|
||||
@@ -46,43 +46,105 @@ export default async ({
|
||||
// calls will be authenticated based on the JWT
|
||||
const { jwt_secret } = configModule.projectConfig
|
||||
passport.use(
|
||||
"admin-jwt",
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: (req) => req.session.jwt,
|
||||
secretOrKey: jwt_secret,
|
||||
},
|
||||
async (jwtPayload, done) => {
|
||||
return done(null, jwtPayload)
|
||||
"admin-session",
|
||||
new CustomStrategy(
|
||||
async (req, done) => {
|
||||
// @ts-ignore
|
||||
if(req.session?.user_id) {
|
||||
// @ts-ignore
|
||||
return done(null, { userId: req.session.user_id })
|
||||
}
|
||||
|
||||
return done(null, false)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
passport.use(
|
||||
"store-jwt",
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: (req) => req.session.jwt_store,
|
||||
secretOrKey: jwt_secret,
|
||||
},
|
||||
async (jwtPayload, done) => {
|
||||
return done(null, jwtPayload)
|
||||
"store-session",
|
||||
new CustomStrategy(
|
||||
async (req, done) => {
|
||||
// @ts-ignore
|
||||
if(req.session?.customer_id) {
|
||||
// @ts-ignore
|
||||
return done(null, { customer_id: req.session.customer_id })
|
||||
}
|
||||
|
||||
return done(null, false)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Alternatively use bearer token to authenticate to the admin api
|
||||
// Alternatively use API token to authenticate to the admin api
|
||||
passport.use(
|
||||
new BearerStrategy(async (token, done) => {
|
||||
const auth = await authService.authenticateAPIToken(token)
|
||||
"admin-api-token",
|
||||
new CustomStrategy(async (req, done) => {
|
||||
// extract the token from the header
|
||||
const token = req.headers["x-medusa-access-token"];
|
||||
|
||||
// check if header exists and is string
|
||||
// typescript will complain if we don't check for type
|
||||
if (!token || typeof token !== "string") {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
const auth = await authService.authenticateAPIToken(token);
|
||||
if (auth.success) {
|
||||
done(null, auth.user)
|
||||
} else {
|
||||
done(auth.error)
|
||||
done(null, false)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Admin bearer JWT token authentication strategy, best suited for web SPAs or mobile apps
|
||||
passport.use(
|
||||
"admin-bearer",
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: jwt_secret,
|
||||
},
|
||||
(token, done) => {
|
||||
if (token.domain !== "admin") {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!token.user_id) {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
done(null, { userId: token.user_id })
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Store bearer JWT token authentication strategy, best suited for web SPAs or mobile apps
|
||||
passport.use(
|
||||
"store-bearer",
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: jwt_secret,
|
||||
},
|
||||
(token, done) => {
|
||||
if (token.domain !== "store") {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!token.customer_id) {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
done(null, { customer_id: token.customer_id })
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
}
|
||||
|
||||
@@ -51,20 +51,6 @@ class AuthService extends TransactionBaseService {
|
||||
*/
|
||||
async authenticateAPIToken(token: string): Promise<AuthenticateResult> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
if (process.env.NODE_ENV?.startsWith("dev")) {
|
||||
try {
|
||||
const user: User = await this.userService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(token)
|
||||
return {
|
||||
success: true,
|
||||
user,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const user: User = await this.userService_
|
||||
.withTransaction(transactionManager)
|
||||
@@ -138,7 +124,7 @@ class AuthService extends TransactionBaseService {
|
||||
* @param {string} password - the password of the user
|
||||
* @return {{ success: (bool), customer: (object | undefined) }}
|
||||
* success: whether authentication succeeded
|
||||
* user: the user document if authentication succeeded
|
||||
* customer: the customer document if authentication succeded
|
||||
* error: a string with the error message
|
||||
*/
|
||||
async authenticateCustomer(
|
||||
|
||||
Reference in New Issue
Block a user