202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
|
import { ApiKeyDTO, ConfigModule, IApiKeyModuleService } from "@medusajs/types"
|
|
import { NextFunction, RequestHandler } from "express"
|
|
import jwt, { JwtPayload } from "jsonwebtoken"
|
|
import {
|
|
AuthContext,
|
|
AuthenticatedMedusaRequest,
|
|
MedusaRequest,
|
|
MedusaResponse,
|
|
} from "../../types/routing"
|
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
|
|
|
const SESSION_AUTH = "session"
|
|
const BEARER_AUTH = "bearer"
|
|
const API_KEY_AUTH = "api-key"
|
|
|
|
// This is the only hard-coded actor type, as API keys have special handling for now. We could also generalize API keys to carry the actor type with them.
|
|
const ADMIN_ACTOR_TYPE = "user"
|
|
|
|
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
|
|
|
|
type MedusaSession = {
|
|
auth_context: AuthContext
|
|
}
|
|
|
|
export const authenticate = (
|
|
actorType: string | string[],
|
|
authType: AuthType | AuthType[],
|
|
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
|
|
): RequestHandler => {
|
|
return async (
|
|
req: MedusaRequest,
|
|
res: MedusaResponse,
|
|
next: NextFunction
|
|
): Promise<void> => {
|
|
const authTypes = Array.isArray(authType) ? authType : [authType]
|
|
const actorTypes = Array.isArray(actorType) ? actorType : [actorType]
|
|
const req_ = req as AuthenticatedMedusaRequest
|
|
|
|
// We only allow authenticating using a secret API key on the admin
|
|
const isExclusivelyUser =
|
|
actorTypes.length === 1 && actorTypes[0] === ADMIN_ACTOR_TYPE
|
|
|
|
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyUser) {
|
|
const apiKey = await getApiKeyInfo(req)
|
|
if (apiKey) {
|
|
req_.auth_context = {
|
|
actor_id: apiKey.id,
|
|
actor_type: "api-key",
|
|
auth_identity_id: "",
|
|
app_metadata: {},
|
|
}
|
|
|
|
return next()
|
|
}
|
|
}
|
|
|
|
// We try to extract the auth context either from the session or from a JWT token
|
|
let authContext: AuthContext | null = getAuthContextFromSession(
|
|
req.session,
|
|
authTypes,
|
|
actorTypes
|
|
)
|
|
|
|
if (!authContext) {
|
|
const { http } = req.scope.resolve<ConfigModule>(
|
|
ContainerRegistrationKeys.CONFIG_MODULE
|
|
).projectConfig
|
|
|
|
authContext = getAuthContextFromJwtToken(
|
|
req.headers.authorization,
|
|
http.jwtSecret!,
|
|
authTypes,
|
|
actorTypes
|
|
)
|
|
}
|
|
|
|
// If the entity is authenticated, and it is a registered actor we can continue
|
|
if (authContext?.actor_id) {
|
|
req_.auth_context = authContext
|
|
return next()
|
|
}
|
|
|
|
// If the entity is authenticated, but there is no registered actor 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()
|
|
}
|
|
|
|
res.status(401).json({ message: "Unauthorized" })
|
|
}
|
|
}
|
|
|
|
const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
|
|
const authHeader = req.headers.authorization
|
|
if (!authHeader) {
|
|
return null
|
|
}
|
|
|
|
const [tokenType, token] = authHeader.split(" ")
|
|
if (tokenType.toLowerCase() !== "basic" || !token) {
|
|
return null
|
|
}
|
|
|
|
// The token could have been base64 encoded, we want to decode it first.
|
|
let normalizedToken = token
|
|
if (!token.startsWith("sk_")) {
|
|
normalizedToken = Buffer.from(token, "base64").toString("utf-8")
|
|
}
|
|
|
|
// Basic auth is defined as a username:password set, and since the token is set to the username we need to trim the colon
|
|
if (normalizedToken.endsWith(":")) {
|
|
normalizedToken = normalizedToken.slice(0, -1)
|
|
}
|
|
|
|
// Secret tokens start with 'sk_', and if it doesn't it could be a user JWT or a malformed token
|
|
if (!normalizedToken.startsWith("sk_")) {
|
|
return null
|
|
}
|
|
|
|
const apiKeyModule = req.scope.resolve(
|
|
ModuleRegistrationName.API_KEY
|
|
) as IApiKeyModuleService
|
|
try {
|
|
const apiKey = await apiKeyModule.authenticate(normalizedToken)
|
|
if (!apiKey) {
|
|
return null
|
|
}
|
|
|
|
return apiKey
|
|
} catch (error) {
|
|
console.error(error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
const getAuthContextFromSession = (
|
|
session: Partial<MedusaSession> = {},
|
|
authTypes: AuthType[],
|
|
actorTypes: string[]
|
|
): AuthContext | null => {
|
|
if (!authTypes.includes(SESSION_AUTH)) {
|
|
return null
|
|
}
|
|
|
|
if (
|
|
session.auth_context &&
|
|
(actorTypes.includes("*") ||
|
|
actorTypes.includes(session.auth_context.actor_type))
|
|
) {
|
|
return session.auth_context
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const getAuthContextFromJwtToken = (
|
|
authHeader: string | undefined,
|
|
jwtSecret: string,
|
|
authTypes: AuthType[],
|
|
actorTypes: string[]
|
|
): AuthContext | null => {
|
|
if (!authTypes.includes(BEARER_AUTH)) {
|
|
return null
|
|
}
|
|
|
|
if (!authHeader) {
|
|
return null
|
|
}
|
|
|
|
const re = /(\S+)\s+(\S+)/
|
|
const matches = authHeader.match(re)
|
|
|
|
// TODO: figure out how to obtain token (and store correct data in token)
|
|
if (matches) {
|
|
const tokenType = matches[1]
|
|
const token = matches[2]
|
|
if (tokenType.toLowerCase() === BEARER_AUTH) {
|
|
// get config jwt secret
|
|
// verify token and set authUser
|
|
try {
|
|
const verified = jwt.verify(token, jwtSecret) as JwtPayload
|
|
if (
|
|
actorTypes.includes("*") ||
|
|
actorTypes.includes(verified.actor_type)
|
|
) {
|
|
return verified as AuthContext
|
|
}
|
|
} catch (err) {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|