From 7d5a6f8b001e5da1405a931adfb7fcc5a090ec3a Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:23:20 +0800 Subject: [PATCH] feat(auth, medusa): Initial auth module middleware (#6271) note: This is an initial implementation Co-authored-by: Sebastian Rindom <7554214+srindom@users.noreply.github.com> --- packages/auth/src/services/auth-module.ts | 109 +++++++++++++----- packages/medusa/src/types/routing.ts | 1 + .../src/utils/authenticate-middleware.ts | 79 +++++++++++++ packages/types/src/auth/service.ts | 14 +++ 4 files changed, 171 insertions(+), 32 deletions(-) create mode 100644 packages/medusa/src/utils/authenticate-middleware.ts diff --git a/packages/auth/src/services/auth-module.ts b/packages/auth/src/services/auth-module.ts index 2add4cf324..5d6be8fbcb 100644 --- a/packages/auth/src/services/auth-module.ts +++ b/packages/auth/src/services/auth-module.ts @@ -1,3 +1,5 @@ +import jwt from "jsonwebtoken" + import { AuthenticationInput, AuthenticationResponse, @@ -8,6 +10,7 @@ import { InternalModuleDeclaration, MedusaContainer, ModuleJoinerConfig, + JWTGenerationOptions, } from "@medusajs/types" import { AuthProvider, AuthUser } from "@models" @@ -33,6 +36,15 @@ import { } from "@medusajs/types" import { ServiceTypes } from "@types" +type AuthModuleOptions = { + jwt_secret: string +} + +type AuthJWTPayload = { + id: string + scope: string +} + type InjectedDependencies = { baseRepository: DAL.RepositoryService authUserService: AuthUserService @@ -57,6 +69,7 @@ export default class AuthModuleService< protected authUserService_: AuthUserService protected authProviderService_: AuthProviderService + protected options_: AuthModuleOptions constructor( { @@ -64,12 +77,14 @@ export default class AuthModuleService< authProviderService, baseRepository, }: InjectedDependencies, + options: AuthModuleOptions, protected readonly moduleDeclaration: InternalModuleDeclaration ) { this.__container__ = arguments[0] this.baseRepository_ = baseRepository this.authUserService_ = authUserService this.authProviderService_ = authProviderService + this.options_ = options } async retrieveAuthProvider( @@ -100,9 +115,10 @@ export default class AuthModuleService< sharedContext ) - return await this.baseRepository_.serialize< - AuthTypes.AuthProviderDTO[] - >(authProviders, { populate: true }) + return await this.baseRepository_.serialize( + authProviders, + { populate: true } + ) } @InjectManager("baseRepository_") @@ -118,13 +134,54 @@ export default class AuthModuleService< ) return [ - await this.baseRepository_.serialize< - AuthTypes.AuthProviderDTO[] - >(authProviders, { populate: true }), + await this.baseRepository_.serialize( + authProviders, + { populate: true } + ), count, ] } + async generateJwtToken( + authUserId: string, + scope: string, + options: JWTGenerationOptions = {} + ): Promise { + const authUser = await this.authUserService_.retrieve(authUserId) + return jwt.sign({ id: authUser.id, scope }, this.options_.jwt_secret, { + expiresIn: options.expiresIn || "1d", + }) + } + + async retrieveAuthUserFromJwtToken( + token: string, + scope: string + ): Promise { + let decoded: AuthJWTPayload + try { + const verifiedToken = jwt.verify(token, this.options_.jwt_secret) + decoded = verifiedToken as AuthJWTPayload + } catch (err) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + "The provided JWT token is invalid" + ) + } + + if (decoded.scope !== scope) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + "The provided JWT token is invalid" + ) + } + + const authUser = await this.authUserService_.retrieve(decoded.id) + return await this.baseRepository_.serialize( + authUser, + { populate: true } + ) + } + async createAuthProvider( data: CreateAuthProviderDTO[], sharedContext?: Context @@ -139,9 +196,7 @@ export default class AuthModuleService< async createAuthProvider( data: CreateAuthProviderDTO | CreateAuthProviderDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const providers = await this.createAuthProviders_(input, sharedContext) @@ -174,13 +229,9 @@ export default class AuthModuleService< @InjectManager("baseRepository_") async updateAuthProvider( - data: - | AuthTypes.UpdateAuthProviderDTO[] - | AuthTypes.UpdateAuthProviderDTO, + data: AuthTypes.UpdateAuthProviderDTO[] | AuthTypes.UpdateAuthProviderDTO, @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const providers = await this.updateAuthProvider_(input, sharedContext) @@ -241,11 +292,12 @@ export default class AuthModuleService< sharedContext ) - return await this.baseRepository_.serialize< - AuthTypes.AuthUserDTO[] - >(authUsers, { - populate: true, - }) + return await this.baseRepository_.serialize( + authUsers, + { + populate: true, + } + ) } @InjectManager("baseRepository_") @@ -261,12 +313,9 @@ export default class AuthModuleService< ) return [ - await this.baseRepository_.serialize( - authUsers, - { - populate: true, - } - ), + await this.baseRepository_.serialize(authUsers, { + populate: true, + }), count, ] } @@ -284,9 +333,7 @@ export default class AuthModuleService< async createAuthUser( data: CreateAuthUserDTO[] | CreateAuthUserDTO, @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const authUsers = await this.createAuthUsers_(input, sharedContext) @@ -321,9 +368,7 @@ export default class AuthModuleService< async updateAuthUser( data: UpdateAuthUserDTO | UpdateAuthUserDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const updatedUsers = await this.updateAuthUsers_(input, sharedContext) diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index 54b0051f0b..a524e8a7c1 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -6,6 +6,7 @@ import type { MedusaContainer } from "./global" export interface MedusaRequest extends Request { user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer + auth_user?: { id: string; app_metadata: Record; scope: string } } export type MedusaResponse = Response diff --git a/packages/medusa/src/utils/authenticate-middleware.ts b/packages/medusa/src/utils/authenticate-middleware.ts new file mode 100644 index 0000000000..0417017ef4 --- /dev/null +++ b/packages/medusa/src/utils/authenticate-middleware.ts @@ -0,0 +1,79 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { AuthUserDTO, IAuthModuleService } from "@medusajs/types" +import { NextFunction, RequestHandler } from "express" +import { MedusaRequest, MedusaResponse } from "../types/routing" + +const SESSION_AUTH = "session" +const BEARER_AUTH = "bearer" + +type MedusaSession = { + auth: { + [authScope: string]: { + user_id: string + } + } +} + +type AuthType = "session" | "bearer" + +export default ( + authScope: string, + authType: AuthType | AuthType[], + options: { allowUnauthenticated?: boolean } = {} +): RequestHandler => { + return async ( + req: MedusaRequest, + res: MedusaResponse, + next: NextFunction + ): Promise => { + const authTypes = Array.isArray(authType) ? authType : [authType] + const authModule = req.scope.resolve( + ModuleRegistrationName.AUTH + ) + + // @ts-ignore + const session: MedusaSession = req.session || {} + + let authUser: AuthUserDTO | null = null + if (authTypes.includes(SESSION_AUTH)) { + if (session.auth && session.auth[authScope]) { + authUser = await authModule + .retrieveAuthUser(session.auth[authScope].user_id) + .catch(() => null) + } + } + + if (authTypes.includes(BEARER_AUTH)) { + const authHeader = req.headers.authorization + if (authHeader) { + const re = /(\S+)\s+(\S+)/ + const matches = authHeader.match(re) + + if (matches) { + const tokenType = matches[1] + const token = matches[2] + if (tokenType.toLowerCase() === "bearer") { + authUser = await authModule + .retrieveAuthUserFromJwtToken(token, authScope) + .catch(() => null) + } + } + } + } + + if (authUser) { + req.auth_user = { + id: authUser.id, + app_metadata: authUser.app_metadata, + scope: authScope, + } + return next() + } + + if (options.allowUnauthenticated) { + return next() + } + + res.status(401).json({ message: "Unauthorized" }) + } +} diff --git a/packages/types/src/auth/service.ts b/packages/types/src/auth/service.ts index 4f4bed7ecc..88b72362db 100644 --- a/packages/types/src/auth/service.ts +++ b/packages/types/src/auth/service.ts @@ -15,6 +15,10 @@ import { Context } from "../shared-context" import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" +export type JWTGenerationOptions = { + expiresIn?: string | number +} + export interface IAuthModuleService extends IModuleService { authenticate( provider: string, @@ -72,6 +76,16 @@ export interface IAuthModuleService extends IModuleService { sharedContext?: Context ): Promise + generateJwtToken( + authUserId: string, + scope: string, + options?: JWTGenerationOptions + ): Promise + retrieveAuthUserFromJwtToken( + token: string, + scope: string + ): Promise + listAuthUsers( filters?: FilterableAuthProviderProps, config?: FindConfig,