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:
co-authored by
Sebastian Rindom
parent
374b9b1fee
commit
7d5a6f8b00
@@ -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)
|
||||||
|
|||||||
@@ -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" })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
|||||||
Reference in New Issue
Block a user