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>
This commit is contained in:
Philip Korsholm
2024-01-30 12:23:20 +00:00
committed by GitHub
co-authored by Sebastian Rindom
parent 374b9b1fee
commit 7d5a6f8b00
4 changed files with 171 additions and 32 deletions
+77 -32
View File
@@ -1,3 +1,5 @@
import jwt from "jsonwebtoken"
import { import {
AuthenticationInput, AuthenticationInput,
AuthenticationResponse, AuthenticationResponse,
@@ -8,6 +10,7 @@ import {
InternalModuleDeclaration, InternalModuleDeclaration,
MedusaContainer, MedusaContainer,
ModuleJoinerConfig, ModuleJoinerConfig,
JWTGenerationOptions,
} from "@medusajs/types" } from "@medusajs/types"
import { AuthProvider, AuthUser } from "@models" import { AuthProvider, AuthUser } from "@models"
@@ -33,6 +36,15 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { ServiceTypes } from "@types" import { ServiceTypes } from "@types"
type AuthModuleOptions = {
jwt_secret: string
}
type AuthJWTPayload = {
id: string
scope: string
}
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
authUserService: AuthUserService<any> authUserService: AuthUserService<any>
@@ -57,6 +69,7 @@ export default class AuthModuleService<
protected authUserService_: AuthUserService<TAuthUser> protected authUserService_: AuthUserService<TAuthUser>
protected authProviderService_: AuthProviderService<TAuthProvider> protected authProviderService_: AuthProviderService<TAuthProvider>
protected options_: AuthModuleOptions
constructor( constructor(
{ {
@@ -64,12 +77,14 @@ export default class AuthModuleService<
authProviderService, authProviderService,
baseRepository, baseRepository,
}: InjectedDependencies, }: InjectedDependencies,
options: AuthModuleOptions,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
this.__container__ = arguments[0] this.__container__ = arguments[0]
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.authUserService_ = authUserService this.authUserService_ = authUserService
this.authProviderService_ = authProviderService this.authProviderService_ = authProviderService
this.options_ = options
} }
async retrieveAuthProvider( async retrieveAuthProvider(
@@ -100,9 +115,10 @@ export default class AuthModuleService<
sharedContext sharedContext
) )
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<AuthTypes.AuthProviderDTO[]>(
AuthTypes.AuthProviderDTO[] authProviders,
>(authProviders, { populate: true }) { populate: true }
)
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@@ -118,13 +134,54 @@ export default class AuthModuleService<
) )
return [ return [
await this.baseRepository_.serialize< await this.baseRepository_.serialize<AuthTypes.AuthProviderDTO[]>(
AuthTypes.AuthProviderDTO[] authProviders,
>(authProviders, { populate: true }), { populate: true }
),
count, count,
] ]
} }
async generateJwtToken(
authUserId: string,
scope: string,
options: JWTGenerationOptions = {}
): Promise<string> {
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<AuthUserDTO> {
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<AuthTypes.AuthUserDTO>(
authUser,
{ populate: true }
)
}
async createAuthProvider( async createAuthProvider(
data: CreateAuthProviderDTO[], data: CreateAuthProviderDTO[],
sharedContext?: Context sharedContext?: Context
@@ -139,9 +196,7 @@ export default class AuthModuleService<
async createAuthProvider( async createAuthProvider(
data: CreateAuthProviderDTO | CreateAuthProviderDTO[], data: CreateAuthProviderDTO | CreateAuthProviderDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise< ): Promise<AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[]> {
AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[]
> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const providers = await this.createAuthProviders_(input, sharedContext) const providers = await this.createAuthProviders_(input, sharedContext)
@@ -174,13 +229,9 @@ export default class AuthModuleService<
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
async updateAuthProvider( async updateAuthProvider(
data: data: AuthTypes.UpdateAuthProviderDTO[] | AuthTypes.UpdateAuthProviderDTO,
| AuthTypes.UpdateAuthProviderDTO[]
| AuthTypes.UpdateAuthProviderDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise< ): Promise<AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[]> {
AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[]
> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const providers = await this.updateAuthProvider_(input, sharedContext) const providers = await this.updateAuthProvider_(input, sharedContext)
@@ -241,11 +292,12 @@ export default class AuthModuleService<
sharedContext sharedContext
) )
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<AuthTypes.AuthUserDTO[]>(
AuthTypes.AuthUserDTO[] authUsers,
>(authUsers, { {
populate: true, populate: true,
}) }
)
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@@ -261,12 +313,9 @@ export default class AuthModuleService<
) )
return [ return [
await this.baseRepository_.serialize<AuthTypes.AuthUserDTO[]>( await this.baseRepository_.serialize<AuthTypes.AuthUserDTO[]>(authUsers, {
authUsers, populate: true,
{ }),
populate: true,
}
),
count, count,
] ]
} }
@@ -284,9 +333,7 @@ export default class AuthModuleService<
async createAuthUser( async createAuthUser(
data: CreateAuthUserDTO[] | CreateAuthUserDTO, data: CreateAuthUserDTO[] | CreateAuthUserDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise< ): Promise<AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[]> {
AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[]
> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const authUsers = await this.createAuthUsers_(input, sharedContext) const authUsers = await this.createAuthUsers_(input, sharedContext)
@@ -321,9 +368,7 @@ export default class AuthModuleService<
async updateAuthUser( async updateAuthUser(
data: UpdateAuthUserDTO | UpdateAuthUserDTO[], data: UpdateAuthUserDTO | UpdateAuthUserDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise< ): Promise<AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[]> {
AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[]
> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const updatedUsers = await this.updateAuthUsers_(input, sharedContext) const updatedUsers = await this.updateAuthUsers_(input, sharedContext)
+1
View File
@@ -6,6 +6,7 @@ import type { MedusaContainer } from "./global"
export interface MedusaRequest extends Request { export interface MedusaRequest extends Request {
user?: (User | Customer) & { customer_id?: string; userId?: string } user?: (User | Customer) & { customer_id?: string; userId?: string }
scope: MedusaContainer scope: MedusaContainer
auth_user?: { id: string; app_metadata: Record<string, any>; scope: string }
} }
export type MedusaResponse = Response export type MedusaResponse = Response
@@ -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<void> => {
const authTypes = Array.isArray(authType) ? authType : [authType]
const authModule = req.scope.resolve<IAuthModuleService>(
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" })
}
}
+14
View File
@@ -15,6 +15,10 @@ import { Context } from "../shared-context"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
export type JWTGenerationOptions = {
expiresIn?: string | number
}
export interface IAuthModuleService extends IModuleService { export interface IAuthModuleService extends IModuleService {
authenticate( authenticate(
provider: string, provider: string,
@@ -72,6 +76,16 @@ export interface IAuthModuleService extends IModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<AuthUserDTO> ): Promise<AuthUserDTO>
generateJwtToken(
authUserId: string,
scope: string,
options?: JWTGenerationOptions
): Promise<string>
retrieveAuthUserFromJwtToken(
token: string,
scope: string
): Promise<AuthUserDTO>
listAuthUsers( listAuthUsers(
filters?: FilterableAuthProviderProps, filters?: FilterableAuthProviderProps,
config?: FindConfig<AuthUserDTO>, config?: FindConfig<AuthUserDTO>,