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:
Adrien de Peretti
2025-06-25 10:29:32 +02:00
committed by GitHub
parent a833c3c98c
commit d517dbd66a
23 changed files with 813 additions and 43 deletions

View File

@@ -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
}
}