feat(api-key): Add api-key authentication to middleware (#6521)

Also did a bit of a cleanup on the auth middleware. There should be no behavioral changes, just moved code around.
This commit is contained in:
Stevche Radevski
2024-02-27 13:44:37 +00:00
committed by GitHub
parent 3ee0f599c1
commit 753bd93ba1
3 changed files with 210 additions and 48 deletions
@@ -1,4 +1,4 @@
import { AuthUserDTO, IUserModuleService } from "@medusajs/types"
import { AuthUserDTO } from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaRequest,
@@ -7,19 +7,22 @@ import {
import { NextFunction, RequestHandler } from "express"
import jwt, { JwtPayload } from "jsonwebtoken"
import { StringChain } from "lodash"
import { stringEqualsOrRegexMatch } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IApiKeyModuleService } from "@medusajs/types"
import { ApiKeyDTO } from "@medusajs/types"
const SESSION_AUTH = "session"
const BEARER_AUTH = "bearer"
const API_KEY_AUTH = "api-key"
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
type MedusaSession = {
auth_user: AuthUserDTO
scope: string
}
type AuthType = "session" | "bearer"
export const authenticate = (
authScope: string | RegExp,
authType: AuthType | AuthType[],
@@ -32,50 +35,39 @@ export const authenticate = (
): Promise<void> => {
const authTypes = Array.isArray(authType) ? authType : [authType]
// @ts-ignore
const session: MedusaSession = req.session || {}
let authUser: AuthUserDTO | null = null
if (authTypes.includes(SESSION_AUTH)) {
if (
session.auth_user &&
stringEqualsOrRegexMatch(authScope, session.auth_user.scope)
) {
authUser = session.auth_user
}
}
if (!authUser && authTypes.includes(BEARER_AUTH)) {
const authHeader = req.headers.authorization
if (authHeader) {
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
const { jwt_secret } =
req.scope.resolve("configModule").projectConfig
const verified = jwt.verify(token, jwt_secret) as JwtPayload
if (stringEqualsOrRegexMatch(authScope, verified.scope)) {
authUser = verified as AuthUserDTO
}
}
// We only allow authenticating using a secret API key on the admin
if (authTypes.includes(API_KEY_AUTH) && isAdminScope(authScope)) {
const apiKey = await getApiKeyInfo(req)
if (apiKey) {
;(req as AuthenticatedMedusaRequest).auth = {
actor_id: apiKey.id,
auth_user_id: "",
app_metadata: {},
// TODO: Add more limited scope once we have support for it in the API key module
scope: "admin",
}
return next()
}
}
const isMedusaScope =
stringEqualsOrRegexMatch(authScope, "admin") ||
stringEqualsOrRegexMatch(authScope, "store")
let authUser: AuthUserDTO | null = getAuthUserFromSession(
req.session,
authTypes,
authScope
)
if (!authUser) {
const { jwt_secret } = req.scope.resolve("configModule").projectConfig
authUser = getAuthUserFromJwtToken(
req.headers.authorization,
jwt_secret,
authTypes,
authScope
)
}
const isMedusaScope = isAdminScope(authScope) || isStoreScope(authScope)
const isRegistered =
!isMedusaScope ||
(authUser?.app_metadata?.user_id &&
@@ -104,6 +96,107 @@ export const authenticate = (
}
}
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 getAuthUserFromSession = (
session: Partial<MedusaSession> = {},
authTypes: AuthType[],
authScope: string | RegExp
): AuthUserDTO | null => {
if (!authTypes.includes(SESSION_AUTH)) {
return null
}
if (
session.auth_user &&
stringEqualsOrRegexMatch(authScope, session.auth_user.scope)
) {
return session.auth_user
}
return null
}
const getAuthUserFromJwtToken = (
authHeader: string | undefined,
jwtSecret: string,
authTypes: AuthType[],
authScope: string | RegExp
): AuthUserDTO | 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 (stringEqualsOrRegexMatch(authScope, verified.scope)) {
return verified as AuthUserDTO
}
} catch (err) {
console.error(err)
return null
}
}
}
return null
}
const getActorId = (
authUser: AuthUserDTO,
scope: string | RegExp
@@ -118,3 +211,11 @@ const getActorId = (
return undefined
}
const isAdminScope = (authScope: string | RegExp): boolean => {
return stringEqualsOrRegexMatch(authScope, "admin")
}
const isStoreScope = (authScope: string | RegExp): boolean => {
return stringEqualsOrRegexMatch(authScope, "store")
}