feat(auth): Make token auth default (#6305)

**What**
- make token auth the default being returned from authentication endpoints in api-v2
- Add `auth/session` to convert token to session based auth
- add regex-scopes to authenticate middleware 

Co-authored-by: Sebastian Rindom <7554214+srindom@users.noreply.github.com>
This commit is contained in:
Philip Korsholm
2024-02-05 16:17:08 +08:00
committed by GitHub
parent 96ba49329b
commit e2738ab91d
21 changed files with 147 additions and 138 deletions

View File

@@ -1,11 +1,12 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
jest.setTimeout(50000)
@@ -39,9 +40,11 @@ describe("POST /store/customers/me/addresses", () => {
})
it("should create a customer address", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { customer, jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
const api = useApi() as any

View File

@@ -4,6 +4,7 @@ import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import adminSeeder from "../../../../helpers/admin-seeder"
import { getContainer } from "../../../../environment-helpers/use-container"
import jwt from "jsonwebtoken"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
@@ -47,12 +48,14 @@ describe("POST /store/customers", () => {
const authService: IAuthModuleService = appContainer.resolve(
ModuleRegistrationName.AUTH
)
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const authUser = await authService.createAuthUser({
entity_id: "store_user",
provider_id: "test",
provider: "emailpass",
scope: "store",
})
const jwt = await authService.generateJwtToken(authUser.id, "store")
const token = jwt.sign(authUser, jwt_secret)
const api = useApi() as any
const response = await api.post(
@@ -62,7 +65,7 @@ describe("POST /store/customers", () => {
last_name: "Doe",
email: "john@me.com",
},
{ headers: { authorization: `Bearer ${jwt}` } }
{ headers: { authorization: `Bearer ${token}` } }
)
expect(response.status).toEqual(200)

View File

@@ -1,11 +1,12 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
const env = { MEDUSA_FF_MEDUSA_V2: true }
@@ -25,6 +26,14 @@ describe("DELETE /store/customers/me/addresses/:address_id", () => {
)
})
// TODO: delete with removal of authProvider
beforeEach(async () => {
const onStart =
appContainer.resolve(ModuleRegistrationName.AUTH).__hooks
.onApplicationStart ?? (() => Promise.resolve())
await onStart()
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
@@ -37,9 +46,11 @@ describe("DELETE /store/customers/me/addresses/:address_id", () => {
})
it("should delete a customer address", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { customer, jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
const address = await customerModuleService.addAddresses({
@@ -65,9 +76,11 @@ describe("DELETE /store/customers/me/addresses/:address_id", () => {
})
it("should fail to delete another customer's address", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
const otherCustomer = await customerModuleService.create({

View File

@@ -1,12 +1,13 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import customer from "../../../../development/database/customer"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
jest.setTimeout(50000)
@@ -34,19 +35,17 @@ describe("GET /store/customers", () => {
await shutdownServer()
})
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should retrieve auth user's customer", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { customer, jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
const api = useApi() as any

View File

@@ -1,11 +1,12 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
const env = { MEDUSA_FF_MEDUSA_V2: true }
@@ -43,9 +44,11 @@ describe("GET /store/customers/me/addresses", () => {
})
it("should get all customer addresses and its count", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { customer, jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
await customerModuleService.addAddresses([

View File

@@ -1,11 +1,12 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
const env = { MEDUSA_FF_MEDUSA_V2: true }
@@ -25,6 +26,14 @@ describe("POST /store/customers/:id/addresses/:address_id", () => {
)
})
// TODO: delete with removal of authProvider
beforeEach(async () => {
const onStart =
appContainer.resolve(ModuleRegistrationName.AUTH).__hooks
.onApplicationStart ?? (() => Promise.resolve())
await onStart()
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
@@ -37,9 +46,12 @@ describe("POST /store/customers/:id/addresses/:address_id", () => {
})
it("should update a customer address", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { customer, jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
const address = await customerModuleService.addAddresses({
@@ -69,15 +81,19 @@ describe("POST /store/customers/:id/addresses/:address_id", () => {
})
it("should fail to update another customer's address", async () => {
const { jwt_secret } = appContainer.resolve("configModule").projectConfig
const { jwt } = await createAuthenticatedCustomer(
customerModuleService,
appContainer.resolve(ModuleRegistrationName.AUTH)
appContainer.resolve(ModuleRegistrationName.AUTH),
jwt_secret
)
const otherCustomer = await customerModuleService.create({
first_name: "Jane",
last_name: "Doe",
})
const address = await customerModuleService.addAddresses({
customer_id: otherCustomer.id,
first_name: "John",

View File

@@ -1,8 +1,11 @@
import { ICustomerModuleService, IAuthModuleService } from "@medusajs/types"
import { IAuthModuleService, ICustomerModuleService } from "@medusajs/types"
import jwt from "jsonwebtoken"
export const createAuthenticatedCustomer = async (
customerModuleService: ICustomerModuleService,
authService: IAuthModuleService
authService: IAuthModuleService,
jwtSecret: string
) => {
const customer = await customerModuleService.create({
first_name: "John",
@@ -12,12 +15,12 @@ export const createAuthenticatedCustomer = async (
const authUser = await authService.createAuthUser({
entity_id: "store_user",
provider_id: "test",
provider: "emailpass",
scope: "store",
app_metadata: { customer_id: customer.id },
})
const jwt = await authService.generateJwtToken(authUser.id, "store")
const token = jwt.sign(authUser, jwtSecret)
return { customer, authUser, jwt }
return { customer, authUser, jwt: token }
}

View File

@@ -46,9 +46,6 @@ module.exports = {
scope: "internal",
resources: "shared",
resolve: "@medusajs/auth",
options: {
jwt_secret: "test",
},
},
[Modules.STOCK_LOCATION]: {
scope: "internal",

View File

@@ -1,8 +1,8 @@
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
import {
AuthProviderScope,
AuthenticationInput,
AuthenticationResponse,
AuthProviderScope,
ModulesSdkTypes,
} from "@medusajs/types"
import { AuthUserService } from "@services"
@@ -82,9 +82,9 @@ class GoogleProvider extends AbstractAuthModuleProvider {
// abstractable
async verify_(refreshToken: string, scope: string) {
const jwtData = (await jwt.decode(refreshToken, {
const jwtData = jwt.decode(refreshToken, {
complete: true,
})) as JwtPayload
}) as JwtPayload
const entity_id = jwtData.payload.email
let authUser

View File

@@ -1,5 +1,3 @@
import jwt from "jsonwebtoken"
import {
AuthenticationInput,
AuthenticationResponse,
@@ -11,7 +9,6 @@ import {
CreateAuthUserDTO,
DAL,
InternalModuleDeclaration,
JWTGenerationOptions,
ModuleJoinerConfig,
ModulesSdkTypes,
UpdateAuthUserDTO,
@@ -31,15 +28,6 @@ import {
} from "@medusajs/utils"
import { ServiceTypes } from "@types"
type AuthModuleOptions = {
jwt_secret: string
}
type AuthJWTPayload = {
id: string
scope: string
}
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
authUserService: ModulesSdkTypes.InternalModuleService<any>
@@ -68,7 +56,6 @@ export default class AuthModuleService<
protected baseRepository_: DAL.RepositoryService
protected authUserService_: ModulesSdkTypes.InternalModuleService<TAuthUser>
protected authProviderService_: ModulesSdkTypes.InternalModuleService<TAuthProvider>
protected options_: AuthModuleOptions
constructor(
{
@@ -76,7 +63,6 @@ export default class AuthModuleService<
authProviderService,
baseRepository,
}: InjectedDependencies,
options: AuthModuleOptions,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
@@ -85,53 +71,12 @@ export default class AuthModuleService<
this.baseRepository_ = baseRepository
this.authUserService_ = authUserService
this.authProviderService_ = authProviderService
this.options_ = options
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
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(
data: CreateAuthProviderDTO[],
sharedContext?: Context

View File

@@ -1,8 +1,8 @@
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import jwt from "jsonwebtoken"
import { MedusaError } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { scope, authProvider } = req.params
@@ -29,10 +29,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
}
if (success) {
req.session.auth_user = authUser
req.session.scope = authUser.scope
return res.status(200).json({ authUser })
const { jwt_secret } = req.scope.resolve("configModule").projectConfig
const token = jwt.sign(authUser, jwt_secret)
return res.status(200).json({ token })
}
throw new MedusaError(

View File

@@ -1,8 +1,8 @@
import jwt from "jsonwebtoken"
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { MedusaError } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { scope, authProvider } = req.params
@@ -29,10 +29,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
}
if (success) {
req.session.auth_user = authUser
req.session.scope = authUser.scope
const { jwt_secret } = req.scope.resolve("configModule").projectConfig
return res.status(200).json({ authUser })
const token = jwt.sign(authUser, jwt_secret)
return res.status(200).json({ token })
}
throw new MedusaError(

View File

@@ -0,0 +1,10 @@
import { MiddlewareRoute } from "../../types/middlewares"
import { authenticate } from "../../utils/authenticate-middleware"
export const authRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
matcher: "/auth/session",
middlewares: [authenticate(/.*/, "bearer")],
},
]

View File

@@ -0,0 +1,7 @@
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
req.session.auth_user = req.auth_user
res.status(200).json({ user: req.auth_user })
}

View File

@@ -5,6 +5,7 @@ import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares"
import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares"
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
import { authRoutesMiddlewares } from "./auth/middlewares"
export const config: MiddlewaresConfig = {
routes: [
@@ -14,5 +15,6 @@ export const config: MiddlewaresConfig = {
...adminCampaignRoutesMiddlewares,
...storeCustomerRoutesMiddlewares,
...storeCartRoutesMiddlewares,
...authRoutesMiddlewares,
],
}

View File

@@ -228,9 +228,11 @@ class InviteService extends TransactionBaseService {
verifyToken(token): JwtPayload | string {
const { jwt_secret } = this.configModule_.projectConfig
if (jwt_secret) {
return jwt.verify(token, jwt_secret)
}
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Please configure jwt_secret"

View File

@@ -1,7 +1,5 @@
import type { Customer, User } from "../models"
import type { NextFunction, Request, Response } from "express"
import { AuthUserDTO } from "@medusajs/types"
import type { MedusaContainer } from "./global"
export interface MedusaRequest extends Request {

View File

@@ -1,8 +1,9 @@
import { AuthUserDTO, IAuthModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../types/routing"
import { NextFunction, RequestHandler } from "express"
import jwt, { JwtPayload } from "jsonwebtoken"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { AuthUserDTO } from "@medusajs/types"
import { stringEqualsOrRegexMatch } from "@medusajs/utils"
const SESSION_AUTH = "session"
const BEARER_AUTH = "bearer"
@@ -15,7 +16,7 @@ type MedusaSession = {
type AuthType = "session" | "bearer"
export const authenticate = (
authScope: string,
authScope: string | RegExp,
authType: AuthType | AuthType[],
options: { allowUnauthenticated?: boolean } = {}
): RequestHandler => {
@@ -25,22 +26,23 @@ export const authenticate = (
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_user && session.scope === authScope) {
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)
@@ -49,10 +51,17 @@ export const authenticate = (
if (matches) {
const tokenType = matches[1]
const token = matches[2]
if (tokenType.toLowerCase() === "bearer") {
authUser = await authModule
.retrieveAuthUserFromJwtToken(token, authScope)
.catch(() => null)
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
}
}
}
}
@@ -62,7 +71,7 @@ export const authenticate = (
req.auth_user = {
id: authUser.id,
app_metadata: authUser.app_metadata,
scope: authScope,
scope: authUser.scope,
}
return next()
}

View File

@@ -76,16 +76,6 @@ export interface IAuthModuleService extends IModuleService {
sharedContext?: Context
): Promise<AuthUserDTO>
generateJwtToken(
authUserId: string,
scope: string,
options?: JWTGenerationOptions
): Promise<string>
retrieveAuthUserFromJwtToken(
token: string,
scope: string
): Promise<AuthUserDTO>
listAuthUsers(
filters?: FilterableAuthProviderProps,
config?: FindConfig<AuthUserDTO>,

View File

@@ -34,6 +34,7 @@ export * from "./remove-nullisih"
export * from "./selector-constraints-to-string"
export * from "./set-metadata"
export * from "./simple-hash"
export * from "./string-or-regex-equals"
export * from "./string-to-select-relation-object"
export * from "./stringify-circular"
export * from "./to-camel-case"
@@ -42,4 +43,3 @@ export * from "./to-pascal-case"
export * from "./transaction"
export * from "./upper-case-first"
export * from "./wrap-handler"

View File

@@ -0,0 +1,9 @@
export const stringEqualsOrRegexMatch = (
stringOrRegex: string | RegExp,
testString: string
) => {
if (stringOrRegex instanceof RegExp) {
return stringOrRegex.test(testString)
}
return stringOrRegex === testString
}