feat(): Add support for jwt asymetric keys (#12813)
* feat(): Add support for jwt asymetric keys * Create early-chefs-chew.md * fix unit tests * Add verify options support * feedback * fix unit tests
This commit is contained in:
1 parent
a833c3c98c
commit
d517dbd66a
23 files changed
+813
-43
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
||||
import { ProjectConfigOptions } from "@medusajs/framework/types"
|
||||
|
||||
/**
|
||||
* This workflow generates a reset password token for a user. It's used by the
|
||||
@@ -45,7 +46,8 @@ export const generateResetPasswordTokenWorkflow = createWorkflow(
|
||||
entityId: string
|
||||
actorType: string
|
||||
provider: string
|
||||
secret: string
|
||||
secret: ProjectConfigOptions["http"]["jwtSecret"]
|
||||
jwtOptions?: ProjectConfigOptions["http"]["jwtOptions"]
|
||||
}) => {
|
||||
const providerIdentities = useRemoteQueryStep({
|
||||
entry_point: "provider_identity",
|
||||
@@ -79,6 +81,7 @@ export const generateResetPasswordTokenWorkflow = createWorkflow(
|
||||
{
|
||||
secret: input.secret,
|
||||
expiresIn: "15m",
|
||||
jwtOptions: input.jwtOptions,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -80,7 +80,21 @@ export class ConfigManager {
|
||||
http.storeCors = http.storeCors ?? ""
|
||||
http.adminCors = http.adminCors ?? ""
|
||||
|
||||
http.jwtOptions ??= {}
|
||||
http.jwtOptions.expiresIn = http.jwtExpiresIn
|
||||
|
||||
http.jwtSecret = http?.jwtSecret ?? process.env.JWT_SECRET
|
||||
http.jwtPublicKey = http?.jwtPublicKey ?? process.env.JWT_PUBLIC_KEY
|
||||
|
||||
if (
|
||||
http?.jwtPublicKey &&
|
||||
((http.jwtVerifyOptions && !http.jwtVerifyOptions.algorithms?.length) ||
|
||||
(http.jwtOptions && !http.jwtOptions.algorithm))
|
||||
) {
|
||||
this.rejectErrors(
|
||||
`JWT public key is provided but no algorithm is set in the 'jwtVerifyOptions' or 'jwtOptions' if 'jwtVerifyOptions' is not provided. It means that the algorithm will be inferred from the public key which can lead to missmatch and invalid algorithm errors.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!http.jwtSecret) {
|
||||
this.rejectErrors(
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { ApiKeyDTO, IApiKeyModuleService } from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
|
||||
import { NextFunction, RequestHandler } from "express"
|
||||
import { JwtPayload, verify } from "jsonwebtoken"
|
||||
import type {
|
||||
JwtPayload,
|
||||
Secret,
|
||||
SignOptions,
|
||||
VerifyOptions,
|
||||
} from "jsonwebtoken"
|
||||
import { verify } from "jsonwebtoken"
|
||||
import { ConfigModule } from "../../config"
|
||||
import {
|
||||
AuthContext,
|
||||
@@ -76,7 +82,9 @@ export const authenticate = (
|
||||
req.headers.authorization,
|
||||
http.jwtSecret!,
|
||||
authTypes,
|
||||
actorTypes
|
||||
actorTypes,
|
||||
http.jwtPublicKey,
|
||||
http.jwtVerifyOptions ?? http.jwtOptions
|
||||
)
|
||||
}
|
||||
|
||||
@@ -172,9 +180,11 @@ const getAuthContextFromSession = (
|
||||
|
||||
export const getAuthContextFromJwtToken = (
|
||||
authHeader: string | undefined,
|
||||
jwtSecret: string,
|
||||
jwtSecret: Secret,
|
||||
authTypes: AuthType[],
|
||||
actorTypes: string[]
|
||||
actorTypes: string[],
|
||||
jwtPublicKey?: Secret,
|
||||
jwtOptions?: VerifyOptions | SignOptions
|
||||
): AuthContext | null => {
|
||||
if (!authTypes.includes(BEARER_AUTH)) {
|
||||
return null
|
||||
@@ -195,7 +205,18 @@ export const getAuthContextFromJwtToken = (
|
||||
// get config jwt secret
|
||||
// verify token and set authUser
|
||||
try {
|
||||
const verified = verify(token, jwtSecret) as JwtPayload
|
||||
const options = { ...jwtOptions } as VerifyOptions & SignOptions
|
||||
|
||||
if (!options.algorithms && options.algorithm) {
|
||||
options.algorithms = [options.algorithm]
|
||||
delete options.algorithm
|
||||
}
|
||||
|
||||
const verified = verify(
|
||||
token,
|
||||
jwtPublicKey ?? jwtSecret!,
|
||||
options
|
||||
) as JwtPayload
|
||||
if (isActorTypePermitted(actorTypes, verified.actor_type)) {
|
||||
return verified as AuthContext
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"bignumber.js": "^9.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"awilix": "^8.0.1",
|
||||
"expect-type": "^0.20.0",
|
||||
"ioredis": "^5.4.1",
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
} from "../modules-sdk"
|
||||
import type { SignOptions, Secret, VerifyOptions } from "jsonwebtoken"
|
||||
|
||||
import type { RedisOptions } from "ioredis"
|
||||
import { ConnectionOptions } from "node:tls"
|
||||
@@ -505,7 +506,70 @@ export type ProjectConfigOptions = {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
jwtSecret?: string
|
||||
jwtSecret?: Secret
|
||||
|
||||
/**
|
||||
* The public key used to verify the JWT token in combination with the JWT secret and the JWT options.
|
||||
* Only used when the JWT secret is a secret key for asymetric validation.
|
||||
*
|
||||
* @example
|
||||
* ```js title="medusa-config.ts"
|
||||
* module.exports = defineConfig({
|
||||
* projectConfig: {
|
||||
* http: {
|
||||
* jwtPublicKey: "public-key"
|
||||
* }
|
||||
* // ...
|
||||
* },
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
jwtPublicKey?: Secret
|
||||
|
||||
/**
|
||||
* Options for the JWT token when using asymetric signing private/public key. Will be used for validation if `jwtVerifyOptions` is not provided.
|
||||
*
|
||||
* @example
|
||||
* ```js title="medusa-config.ts"
|
||||
* module.exports = defineConfig({
|
||||
* projectConfig: {
|
||||
* http: {
|
||||
* jwtOptions: {
|
||||
* algorithm: "RS256",
|
||||
* expiresIn: "1h",
|
||||
* issuer: "medusa",
|
||||
* keyid: "medusa",
|
||||
* }
|
||||
* }
|
||||
* // ...
|
||||
* },
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
jwtOptions?: SignOptions
|
||||
|
||||
/**
|
||||
* Options for the JWT token when using asymetric validation private/public key.
|
||||
*
|
||||
* @example
|
||||
* ```js title="medusa-config.ts"
|
||||
* module.exports = defineConfig({
|
||||
* projectConfig: {
|
||||
* http: {
|
||||
* jwtVerifyOptions: {
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* // ...
|
||||
* },
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
jwtVerifyOptions?: VerifyOptions
|
||||
|
||||
/**
|
||||
* The expiration time for the JWT token. Its format is based off the [ms package](https://github.com/vercel/ms).
|
||||
*
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import jwt from "jsonwebtoken"
|
||||
import jwt, { type Secret, type SignOptions } from "jsonwebtoken"
|
||||
import { MedusaError } from "../common"
|
||||
|
||||
export const generateJwtToken = (
|
||||
tokenPayload: Record<string, unknown>,
|
||||
jwtConfig: {
|
||||
secret: string | undefined
|
||||
expiresIn: string | undefined
|
||||
secret?: Secret
|
||||
expiresIn?: number | string
|
||||
jwtOptions?: SignOptions
|
||||
}
|
||||
) => {
|
||||
if (!jwtConfig.secret || !jwtConfig.expiresIn) {
|
||||
if (
|
||||
!jwtConfig.secret ||
|
||||
(!jwtConfig.expiresIn && !jwtConfig.jwtOptions?.expiresIn)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"JWT secret and expiresIn must be provided when generating a token"
|
||||
)
|
||||
}
|
||||
|
||||
const expiresIn = jwtConfig.expiresIn ?? jwtConfig.jwtOptions?.expiresIn
|
||||
return jwt.sign(tokenPayload, jwtConfig.secret, {
|
||||
expiresIn: jwtConfig.expiresIn,
|
||||
...jwtConfig.jwtOptions,
|
||||
expiresIn,
|
||||
})
|
||||
}
|
||||
@@ -117,7 +117,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -132,6 +135,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -277,7 +281,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -292,6 +299,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -445,7 +453,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -460,6 +471,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -614,7 +626,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -629,6 +644,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -771,7 +787,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -786,6 +805,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:3000",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -931,7 +951,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -946,6 +969,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:3000",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -1119,7 +1143,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -1139,6 +1166,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -1314,7 +1342,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -1334,6 +1365,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -1525,7 +1557,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -1545,6 +1580,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
@@ -1708,7 +1744,10 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"user": {
|
||||
"options": {
|
||||
"jwt_options": undefined,
|
||||
"jwt_public_key": undefined,
|
||||
"jwt_secret": "supersecret",
|
||||
"jwt_verify_options": undefined,
|
||||
},
|
||||
"resolve": "@medusajs/medusa/user",
|
||||
},
|
||||
@@ -1723,6 +1762,7 @@ describe("defineConfig", function () {
|
||||
"adminCors": "http://localhost:3000",
|
||||
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
|
||||
"cookieSecret": "supersecret",
|
||||
"jwtPublicKey": undefined,
|
||||
"jwtSecret": "supersecret",
|
||||
"restrictedFields": {
|
||||
"store": [
|
||||
|
||||
@@ -48,7 +48,7 @@ export function defineConfig(config: InputConfig = {}): ConfigModule {
|
||||
|
||||
const projectConfig = normalizeProjectConfig(config.projectConfig, options)
|
||||
const adminConfig = normalizeAdminConfig(config.admin)
|
||||
const modules = resolveModules(config.modules, options)
|
||||
const modules = resolveModules(config.modules, options, config.projectConfig)
|
||||
|
||||
return {
|
||||
projectConfig,
|
||||
@@ -132,7 +132,8 @@ export function transformModules(
|
||||
*/
|
||||
function resolveModules(
|
||||
configModules: InputConfig["modules"],
|
||||
{ isCloud }: { isCloud: boolean }
|
||||
{ isCloud }: { isCloud: boolean },
|
||||
projectConfig: InputConfig["projectConfig"]
|
||||
): Exclude<ConfigModule["modules"], undefined> {
|
||||
const sharedModules = [
|
||||
{ resolve: MODULE_PACKAGE_NAMES[Modules.STOCK_LOCATION] },
|
||||
@@ -166,7 +167,10 @@ function resolveModules(
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.USER],
|
||||
options: {
|
||||
jwt_secret: process.env.JWT_SECRET ?? DEFAULT_SECRET,
|
||||
jwt_secret: projectConfig?.http?.jwtSecret ?? DEFAULT_SECRET,
|
||||
jwt_options: projectConfig?.http?.jwtOptions,
|
||||
jwt_verify_options: projectConfig?.http?.jwtVerifyOptions,
|
||||
jwt_public_key: projectConfig?.http?.jwtPublicKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -318,7 +322,7 @@ function normalizeProjectConfig(
|
||||
* The defaults to use for the project config. They are shallow merged
|
||||
* with the user defined config.
|
||||
*/
|
||||
return {
|
||||
const config = {
|
||||
...(isCloud ? { redisUrl: process.env.REDIS_URL } : {}),
|
||||
databaseUrl: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
|
||||
http: {
|
||||
@@ -326,6 +330,7 @@ function normalizeProjectConfig(
|
||||
adminCors: process.env.ADMIN_CORS || DEFAULT_ADMIN_CORS,
|
||||
authCors: process.env.AUTH_CORS || DEFAULT_ADMIN_CORS,
|
||||
jwtSecret: process.env.JWT_SECRET || DEFAULT_SECRET,
|
||||
jwtPublicKey: process.env.JWT_PUBLIC_KEY,
|
||||
cookieSecret: process.env.COOKIE_SECRET || DEFAULT_SECRET,
|
||||
restrictedFields: {
|
||||
store: DEFAULT_STORE_RESTRICTED_FIELDS,
|
||||
@@ -375,6 +380,8 @@ function normalizeProjectConfig(
|
||||
},
|
||||
...restOfProjectConfig,
|
||||
} satisfies ConfigModule["projectConfig"]
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function normalizeAdminConfig(
|
||||
|
||||
@@ -38,8 +38,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const token = generateJwtTokenForAuthIdentity(
|
||||
{ authIdentity, actorType: actor_type },
|
||||
{
|
||||
secret: http.jwtSecret,
|
||||
secret: http.jwtSecret!,
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
options: http.jwtOptions,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -41,8 +41,9 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
actorType: actor_type,
|
||||
},
|
||||
{
|
||||
secret: http.jwtSecret,
|
||||
secret: http.jwtSecret!,
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
options: http.jwtOptions,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ export const POST = async (
|
||||
entityId: identifier,
|
||||
actorType: actor_type,
|
||||
provider: auth_provider,
|
||||
secret: http.jwtSecret as string,
|
||||
secret: http.jwtSecret!,
|
||||
jwtOptions: http.jwtOptions,
|
||||
},
|
||||
throwOnError: false, // we don't want to throw on error to avoid leaking information about non-existing identities
|
||||
})
|
||||
|
||||
@@ -45,8 +45,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
actorType: actor_type,
|
||||
},
|
||||
{
|
||||
secret: http.jwtSecret,
|
||||
secret: http.jwtSecret!,
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
options: http.jwtOptions,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ export const POST = async (
|
||||
const token = generateJwtTokenForAuthIdentity(
|
||||
{ authIdentity, actorType: req.auth_context.actor_type },
|
||||
{
|
||||
secret: http.jwtSecret,
|
||||
secret: http.jwtSecret!,
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
options: http.jwtOptions,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { AuthIdentityDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
AuthIdentityDTO,
|
||||
ProjectConfigOptions,
|
||||
} from "@medusajs/framework/types"
|
||||
import { generateJwtToken } from "@medusajs/framework/utils"
|
||||
import { type Secret } from "jsonwebtoken"
|
||||
|
||||
export function generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
@@ -9,8 +13,14 @@ export function generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
secret,
|
||||
expiresIn,
|
||||
}: { secret: string | undefined; expiresIn: string | undefined }
|
||||
options,
|
||||
}: {
|
||||
secret: Secret
|
||||
expiresIn: string | undefined
|
||||
options?: ProjectConfigOptions["http"]["jwtOptions"]
|
||||
}
|
||||
) {
|
||||
const expiresIn_ = expiresIn ?? options?.expiresIn
|
||||
const entityIdKey = `${actorType}_id`
|
||||
const entityId = authIdentity?.app_metadata?.[entityIdKey] as
|
||||
| string
|
||||
@@ -27,7 +37,8 @@ export function generateJwtTokenForAuthIdentity(
|
||||
},
|
||||
{
|
||||
secret,
|
||||
expiresIn,
|
||||
expiresIn: expiresIn_,
|
||||
jwtOptions: options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -30,16 +30,17 @@ export const validateToken = () => {
|
||||
|
||||
const req_ = req as AuthenticatedMedusaRequest
|
||||
|
||||
// @ts-ignore
|
||||
const { http } = req_.scope.resolve<ConfigModule>(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
const token = getAuthContextFromJwtToken(
|
||||
req.headers.authorization,
|
||||
http.jwtSecret as string,
|
||||
http.jwtSecret!,
|
||||
["bearer"],
|
||||
[actor_type]
|
||||
[actor_type],
|
||||
http.jwtPublicKey,
|
||||
http.jwtVerifyOptions ?? http.jwtOptions
|
||||
) as UpdateProviderJwtPayload | null
|
||||
|
||||
const errorObject = new MedusaError(
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
AbstractAuthModuleProvider,
|
||||
MedusaError,
|
||||
} from "@medusajs/framework/utils"
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
import jwt, { type JwtPayload } from "jsonwebtoken"
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
InferEntityType,
|
||||
InternalModuleDeclaration,
|
||||
ModulesSdkTypes,
|
||||
ProjectConfigOptions,
|
||||
UserTypes,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
CommonEvents,
|
||||
EmitEvents,
|
||||
generateEntityId,
|
||||
generateJwtToken,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
@@ -20,10 +22,11 @@ import {
|
||||
Modules,
|
||||
UserEvents,
|
||||
} from "@medusajs/framework/utils"
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
import jwt, { JwtPayload, SignOptions, VerifyOptions } from "jsonwebtoken"
|
||||
import crypto from "node:crypto"
|
||||
|
||||
import { Invite, User } from "@models"
|
||||
import { getExpiresAt } from "../utils/utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
@@ -51,11 +54,18 @@ export default class UserModuleService
|
||||
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<
|
||||
InferEntityType<typeof Invite>
|
||||
>
|
||||
protected readonly config: { jwtSecret: string; expiresIn: number }
|
||||
protected readonly config: {
|
||||
jwtSecret: string
|
||||
jwtPublicKey?: string
|
||||
jwt_verify_options: ProjectConfigOptions["http"]["jwtVerifyOptions"]
|
||||
jwtOptions: ProjectConfigOptions["http"]["jwtOptions"] & {
|
||||
expiresIn: number
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
{ userService, inviteService, baseRepository }: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
@@ -63,11 +73,18 @@ export default class UserModuleService
|
||||
this.baseRepository_ = baseRepository
|
||||
this.userService_ = userService
|
||||
this.inviteService_ = inviteService
|
||||
|
||||
this.config = {
|
||||
jwtSecret: moduleDeclaration["jwt_secret"],
|
||||
expiresIn:
|
||||
parseInt(moduleDeclaration["valid_duration"]) ||
|
||||
DEFAULT_VALID_INVITE_DURATION_SECONDS,
|
||||
jwtPublicKey: moduleDeclaration["jwt_public_key"],
|
||||
jwt_verify_options: moduleDeclaration["jwt_verify_options"],
|
||||
jwtOptions: {
|
||||
...moduleDeclaration["jwt_options"],
|
||||
expiresIn:
|
||||
moduleDeclaration["valid_duration"] ??
|
||||
moduleDeclaration["jwt_options"]?.expiresIn ??
|
||||
DEFAULT_VALID_INVITE_DURATION_SECONDS,
|
||||
},
|
||||
}
|
||||
|
||||
if (!this.config.jwtSecret) {
|
||||
@@ -83,8 +100,21 @@ export default class UserModuleService
|
||||
token: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<UserTypes.InviteDTO> {
|
||||
const jwtSecret = this.moduleDeclaration["jwt_secret"]
|
||||
const decoded: JwtPayload = jwt.verify(token, jwtSecret, { complete: true })
|
||||
const options = {
|
||||
...(this.config.jwt_verify_options ?? this.config.jwtOptions),
|
||||
complete: true,
|
||||
} as VerifyOptions & SignOptions
|
||||
|
||||
if (!options.algorithms && options.algorithm) {
|
||||
options.algorithms = [options.algorithm]
|
||||
delete options.algorithm
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
this.config.jwtPublicKey ?? this.config.jwtSecret,
|
||||
options
|
||||
) as JwtPayload
|
||||
|
||||
const invite = await this.inviteService_.retrieve(
|
||||
decoded.payload.id,
|
||||
@@ -155,10 +185,11 @@ export default class UserModuleService
|
||||
}
|
||||
}
|
||||
|
||||
const expiresAt = getExpiresAt(this.config.jwtOptions.expiresIn)
|
||||
const updates = invites.map((invite) => {
|
||||
return {
|
||||
id: invite.id,
|
||||
expires_at: new Date(Date.now() + this.config.expiresIn * 1000),
|
||||
expires_at: expiresAt,
|
||||
token: this.generateToken({ id: invite.id, email: invite.email }),
|
||||
}
|
||||
})
|
||||
@@ -317,12 +348,14 @@ export default class UserModuleService
|
||||
)
|
||||
}
|
||||
|
||||
const expiresAt = getExpiresAt(this.config.jwtOptions.expiresIn)
|
||||
|
||||
const toCreate = data.map((invite) => {
|
||||
const id = generateEntityId((invite as { id?: string }).id, "invite")
|
||||
return {
|
||||
...invite,
|
||||
id,
|
||||
expires_at: new Date(Date.now() + this.config.expiresIn * 1000),
|
||||
expires_at: expiresAt,
|
||||
token: this.generateToken({ id, email: invite.email }),
|
||||
}
|
||||
})
|
||||
@@ -375,10 +408,15 @@ export default class UserModuleService
|
||||
}
|
||||
|
||||
private generateToken(data: any): string {
|
||||
const jwtSecret: string = this.moduleDeclaration["jwt_secret"]
|
||||
return jwt.sign(data, jwtSecret, {
|
||||
jwtid: crypto.randomUUID(),
|
||||
expiresIn: this.config.expiresIn,
|
||||
const jwtId = this.config.jwtOptions.jwtid ?? crypto.randomUUID()
|
||||
const token = generateJwtToken(data, {
|
||||
secret: this.config.jwtSecret,
|
||||
jwtOptions: {
|
||||
...this.config.jwtOptions,
|
||||
jwtid: jwtId,
|
||||
},
|
||||
})
|
||||
|
||||
return token
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import timespan from "jsonwebtoken/lib/timespan"
|
||||
|
||||
export function getExpiresAt(expiresIn: string | number) {
|
||||
const expiresAt =
|
||||
typeof expiresIn === "number"
|
||||
? new Date(Date.now() + expiresIn * 1000)
|
||||
: new Date(Math.floor(timespan(expiresIn)) * 1000)
|
||||
|
||||
return expiresAt
|
||||
}
|
||||
Reference in new issue
Block a user