feat(auth): Revamp authentication setup (#7387)

* chore: Clean up authentication middlewares

* chore: Rename AuthUser to AuthIdentity

* feat: Define link between user, customer, and auth identity

* feat: Use links for auth, update auth context content

* fix: Adjust user create command with new auth setup

* fix: Make auth login more dynamic, review fixes

* fix: Change test assertions for created by
This commit is contained in:
Stevche Radevski
2024-05-22 10:27:32 +02:00
committed by GitHub
parent b7df447682
commit 5ede560f70
88 changed files with 887 additions and 1014 deletions
@@ -1,11 +0,0 @@
import { NextFunction, Request, RequestHandler, Response } from "express"
// TODO: See how this should look like for v2.
// Optional customer authentication
// If authenticated, middleware attaches customer to request (as user) otherwise we pass through
// If you want to require authentication, use `requireCustomerAuthentication` in `packages/medusa/src/api/middlewares/require-customer-authentication.ts`
export default (): RequestHandler => {
return (req: Request, res: Response, next: NextFunction): void => {
return next()
}
}
@@ -1,14 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
ApiKeyDTO,
AuthUserDTO,
ConfigModule,
IApiKeyModuleService,
} from "@medusajs/types"
import { stringEqualsOrRegexMatch } from "@medusajs/utils"
import { ApiKeyDTO, ConfigModule, IApiKeyModuleService } from "@medusajs/types"
import { NextFunction, RequestHandler } from "express"
import jwt, { JwtPayload } from "jsonwebtoken"
import {
AuthContext,
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
@@ -20,13 +15,18 @@ const API_KEY_AUTH = "api-key"
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
const ADMIN_SCOPE = "admin"
const STORE_SCOPE = "store"
const ALL_SCOPE = "*"
type Scope = typeof ADMIN_SCOPE | typeof STORE_SCOPE | typeof ALL_SCOPE
type MedusaSession = {
auth_user: AuthUserDTO
scope: string
auth_context: AuthContext
}
export const authenticate = (
authScope: string | RegExp,
authScope: Scope | Scope[],
authType: AuthType | AuthType[],
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
): RequestHandler => {
@@ -36,63 +36,58 @@ export const authenticate = (
next: NextFunction
): Promise<void> => {
const authTypes = Array.isArray(authType) ? authType : [authType]
const scopes = Array.isArray(authScope) ? authScope : [authScope]
const req_ = req as AuthenticatedMedusaRequest
// We only allow authenticating using a secret API key on the admin
if (authTypes.includes(API_KEY_AUTH) && isAdminScope(authScope)) {
const isExclusivelyAdmin =
scopes.length === 1 && scopes.includes(ADMIN_SCOPE)
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyAdmin) {
const apiKey = await getApiKeyInfo(req)
if (apiKey) {
;(req as AuthenticatedMedusaRequest).auth = {
req_.auth_context = {
actor_id: apiKey.id,
auth_user_id: "",
actor_type: "api-key",
auth_identity_id: "",
app_metadata: {},
// TODO: Add more limited scope once we have support for it in the API key module
scope: "admin",
scope: ADMIN_SCOPE,
}
return next()
}
}
let authUser: AuthUserDTO | null = getAuthUserFromSession(
// We try to extract the auth context either from the session or from a JWT token
let authContext: AuthContext | null = getAuthContextFromSession(
req.session,
authTypes,
authScope
scopes
)
if (!authUser) {
if (!authContext) {
const { http } =
req.scope.resolve<ConfigModule>("configModule").projectConfig
authUser = getAuthUserFromJwtToken(
authContext = getAuthContextFromJwtToken(
req.headers.authorization,
http.jwtSecret!,
authTypes,
authScope
scopes
)
}
const isMedusaScope = isAdminScope(authScope) || isStoreScope(authScope)
const isRegistered =
!isMedusaScope ||
(authUser?.app_metadata?.user_id &&
stringEqualsOrRegexMatch(authScope, "admin")) ||
(authUser?.app_metadata?.customer_id &&
stringEqualsOrRegexMatch(authScope, "store"))
if (
authUser &&
(isRegistered || (!isRegistered && options.allowUnregistered))
) {
;(req as AuthenticatedMedusaRequest).auth = {
actor_id: getActorId(authUser, authScope) as string, // TODO: fix types for auth_users not in the medusa system
auth_user_id: authUser.id,
app_metadata: authUser.app_metadata,
scope: authUser.scope,
}
// If the entity is authenticated, and it is a registered user/customer we can continue
if (!!authContext?.actor_id && authContext.actor_type !== "unknown") {
req_.auth_context = authContext
return next()
}
// If the entity is authenticated, but there is no user/customer yet, we can continue (eg. in the case of a user invite) if allow unregistered is set
if (authContext?.auth_identity_id && options.allowUnregistered) {
req_.auth_context = authContext
return next()
}
// If we allow unauthenticated requests (i.e public endpoints), just continue
if (options.allowUnauthenticated) {
return next()
}
@@ -144,31 +139,32 @@ const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
}
}
const getAuthUserFromSession = (
const getAuthContextFromSession = (
session: Partial<MedusaSession> = {},
authTypes: AuthType[],
authScope: string | RegExp
): AuthUserDTO | null => {
scopes: Scope[]
): AuthContext | null => {
if (!authTypes.includes(SESSION_AUTH)) {
return null
}
if (
session.auth_user &&
stringEqualsOrRegexMatch(authScope, session.auth_user.scope)
session.auth_context &&
(scopes.includes("*") ||
scopes.includes(session.auth_context.scope as Scope))
) {
return session.auth_user
return session.auth_context
}
return null
}
const getAuthUserFromJwtToken = (
const getAuthContextFromJwtToken = (
authHeader: string | undefined,
jwtSecret: string,
authTypes: AuthType[],
authScope: string | RegExp
): AuthUserDTO | null => {
scopes: Scope[]
): AuthContext | null => {
if (!authTypes.includes(BEARER_AUTH)) {
return null
}
@@ -189,8 +185,8 @@ const getAuthUserFromJwtToken = (
// verify token and set authUser
try {
const verified = jwt.verify(token, jwtSecret) as JwtPayload
if (stringEqualsOrRegexMatch(authScope, verified.scope)) {
return verified as AuthUserDTO
if (scopes.includes("*") || scopes.includes(verified.scope)) {
return verified as AuthContext
}
} catch (err) {
return null
@@ -200,26 +196,3 @@ const getAuthUserFromJwtToken = (
return null
}
const getActorId = (
authUser: AuthUserDTO,
scope: string | RegExp
): string | undefined => {
if (stringEqualsOrRegexMatch(scope, "admin")) {
return authUser.app_metadata.user_id as string
}
if (stringEqualsOrRegexMatch(scope, "store")) {
return authUser.app_metadata.customer_id as string
}
return undefined
}
const isAdminScope = (authScope: string | RegExp): boolean => {
return stringEqualsOrRegexMatch(authScope, "admin")
}
const isStoreScope = (authScope: string | RegExp): boolean => {
return stringEqualsOrRegexMatch(authScope, "store")
}
@@ -1,8 +0,0 @@
import { NextFunction, Request, RequestHandler, Response } from "express"
// TODO: See how this should look like for v2.
export default (): RequestHandler => {
return (req: Request, res: Response, next: NextFunction): void => {
return next()
}
}
@@ -1,5 +1,2 @@
export { default as authenticateLegacy } from "./authenticate"
export { authenticate } from "./authenticate-middleware"
export { default as authenticateCustomer } from "./authenticate-customer"
export { default as errorHandler } from "./error-handler"
export { default as requireCustomerAuthentication } from "./require-customer-authentication"
@@ -1,16 +0,0 @@
import { NextFunction, Request, RequestHandler, Response } from "express"
import passport from "passport"
export default (): RequestHandler => {
return (req: Request, res: Response, next: NextFunction): void => {
if (req.user) {
return next()
}
passport.authenticate(["store-session", "store-bearer"], { session: false })(
req,
res,
next
)
}
}