-
-
Notifications
You must be signed in to change notification settings - Fork 26
feat: support for remote JWKS #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…existing 'secret' implementation
WalkthroughAdds remote JWKS verification and restructures the JWT plugin to expose verify/sign via a runtime decoration. Introduces discriminated JWTOption types (secret vs jwks), expands iat typing, updates algorithm handling and verification flow, and adds JWKS-focused tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant ElysiaApp as Elysia App
participant JWTPlugin as jwtDecoration
participant JWKS as Remote/Local JWKS
participant Secret as Local Secret Key
rect rgba(200,230,201,0.3)
Note over ElysiaApp,JWTPlugin: Init: jwt({ secret?, jwks? }) decorates app with verify & sign
end
Client->>ElysiaApp: Request with Authorization: Bearer <jwt>
ElysiaApp->>JWTPlugin: verify(jwt)
JWTPlugin->>JWTPlugin: decodeProtectedHeader(jwt) -> alg
alt alg is asymmetric AND jwks provided
JWTPlugin->>JWKS: get key (JWTVerifyGetKey)
JWTPlugin->>JWTPlugin: jwtVerify(jwt, jwks, options)
else alg is symmetric (HS*)
alt secret provided
JWTPlugin->>Secret: derive HS key
JWTPlugin->>JWTPlugin: jwtVerify(jwt, key, options)
else
JWTPlugin-->>ElysiaApp: throw Error (HS requires local secret)
end
end
JWTPlugin-->>ElysiaApp: validated payload
ElysiaApp-->>Client: Response
sequenceDiagram
autonumber
participant Client
participant ElysiaApp as Elysia App
participant JWTPlugin as jwtDecoration
participant Secret as Local Secret Key
Client->>ElysiaApp: POST /sign { payload }
ElysiaApp->>JWTPlugin: sign(payload { exp/nbf/iat defaults })
alt secret provided
JWTPlugin->>Secret: derive key for SignJWT
JWTPlugin-->>ElysiaApp: signed JWT
ElysiaApp-->>Client: token
else
JWTPlugin-->>ElysiaApp: throw Error (sign requires secret)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/index.ts (2)
210-214
: Unconditional secret requirement blocks remote-only usage.This check prevents configuring the plugin purely for verification against a remote JWKS, contradicting the PR goal.
Apply this diff to allow remote-only and make
key
optional:- if (!secret) throw new Error("Secret can't be empty") - - const key = - typeof secret === 'string' ? new TextEncoder().encode(secret) : secret + if (!secret && !remoteJwks) + throw new Error("Configure either 'secret' or 'remoteJwks'") + + const key = + secret + ? (typeof secret === 'string' + ? new TextEncoder().encode(secret) + : secret) + : undefined
369-370
: Guard signing whensecret
is not configured.If the plugin is used in remote-only mode,
key
will be undefined andjwt.sign(key)
will throw.- return jwt.sign(key) + if (!key) + throw new Error('Signing is disabled: no "secret" configured') + return jwt.sign(key)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(5 hunks)
🔇 Additional comments (2)
src/index.ts (2)
16-18
: Type-only imports: good change.Using type-only imports for JWTVerifyOptions and JWTVerifyGetKey avoids runtime bloat and keeps ESM clean.
242-245
: Seeding remoteJwks: LGTM.Propagating remoteJwks via seed maintains parity with existing options.
…in security (Implemented from CodeRabbit feedback)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/index.ts (2)
402-407
:iat
handling is incorrect and contradicts the comment.Current code ignores the input flag and always sets
iat
tonew Date()
. WithJWTPayloadInput['iat']
being boolean, set to “now” only whentrue
; do nothing iffalse
/undefined
.- const setIat = 'iat' in signValue ? iat : defaultValues.iat - if (setIat !== false) { - jwt = jwt.setIssuedAt(new Date()) - } + const setIat = 'iat' in signValue ? iat : defaultValues.iat + if (setIat === true) { + jwt = jwt.setIssuedAt() + }
230-233
: Handle JWK secrets by converting them before use
src/index.ts:230-233: rawJWK
falls through as-is, butSignJWT.sign
/jwtVerify
require aKeyLike
(e.g.Uint8Array
/CryptoKey
). Either removeJWK
from the publicsecret
type or wrap it withimportJWK(secret)
(fromjose
) to produce a valid key.
♻️ Duplicate comments (2)
src/index.ts (2)
189-215
: Remote-only configuration is blocked at runtime; align guard with the union type.
JWTOption
permits remote-only, but Line 228 still throws whensecret
is absent. This breaks the PR goal. Computekey
conditionally and only require that at least one ofsecret
orremoteJwks
is provided.Apply:
- if (!secret) throw new Error("Secret can't be empty") - - const key = - typeof secret === 'string' ? new TextEncoder().encode(secret) : secret + if (!secret && !remoteJwks) + throw new Error('Either "secret" or "remoteJwks" must be provided') + + const key = secret + ? (typeof secret === 'string' ? new TextEncoder().encode(secret) : secret) + : undefinedAlso applies to: 228-233
255-285
: Tighten verification: removeany
, collapse branches, and guard HS without secret.*
- Avoid
any
by destructuringpayload
.- Single-key path reduces duplication.
- Explicitly return false for HS* when
secret
/key
is missing, instead of relying on an exception.- try { - const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let data: any - if (remoteJwks && !isSymmetric) { - data = (await jwtVerify(jwt, remoteJwks, options)).payload - } else { - data = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + try { + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + const verifyKey = remoteJwks && !isSymmetric ? remoteJwks : key + if (isSymmetric && !verifyKey) return false + const { payload } = await jwtVerify( + jwt, + verifyKey as Exclude<typeof verifyKey, undefined>, + options + ) + const data = + payload as UnwrapSchema<Schema, ClaimType> & + Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>Optional hardening: when
remoteJwks
is used andoptions?.algorithms
is unset, consider defaulting to asymmetric algs only (RS*
,PS*
,ES*
,EdDSA
) to avoid HS/RS confusion.
🧹 Nitpick comments (1)
src/index.ts (1)
378-381
: Avoid redundantalg
spread in protected header.
alg
is already part ofJWTHeader
. Pass the object directly.- let jwt = new SignJWT({ ...JWTPayload }).setProtectedHeader({ - alg: JWTHeader.alg!, - ...JWTHeader - }) + let jwt = new SignJWT({ ...JWTPayload }).setProtectedHeader(JWTHeader)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (3)
src/index.ts (3)
9-19
: Additions to jose imports — LGTM.Bringing in
decodeProtectedHeader
,JWTVerifyOptions
, andJWTVerifyGetKey
is correct for the new flow.
162-188
: Base options factoring — LGTM.Clean separation; keeps the payload/header knobs alongside
name
/schema
.
413-423
: Seed and decoration wiring — LGTM.Including
remoteJwks
in the seed and conditionally exposingsign
is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/index.ts (1)
402-407
: Bug:iat
is set by default even when not requestedCurrent logic sets
iat
unless explicitly false. Align with the comment: only set whentrue
.- const setIat = 'iat' in signValue ? iat : defaultValues.iat - if (setIat !== false) { - jwt = jwt.setIssuedAt(new Date()) - } + const setIat = 'iat' in signValue ? iat : defaultValues.iat + if (setIat === true) { + jwt = jwt.setIssuedAt() + }
♻️ Duplicate comments (2)
src/index.ts (2)
189-215
: Encode the “one-of” config at the type level (secret
|remoteJwks
)Make the remote-only branch disallow
secret
to prevent accidental dual configuration at compile time.export type JWTOption< Name extends string | undefined = 'jwt', Schema extends TSchema | undefined = undefined > = | (BaseJWTOption<Name, Schema> & { /** * JWT Secret */ secret: string | Uint8Array | CryptoKey | JWK | KeyObject /** * Remote JWKS * Use jose's `createRemoteJWKSet(new URL(...))` to create the JWKS function */ remoteJwks?: JWTVerifyGetKey }) | (BaseJWTOption<Name, Schema> & { /** * JWT Secret */ - secret?: string | Uint8Array | CryptoKey | JWK | KeyObject + secret?: never /** * Remote JWKS * Use jose's `createRemoteJWKSet(new URL(...))` to create the JWKS function */ remoteJwks: JWTVerifyGetKey })
256-284
: Unify verify path, removeany
, enforce safe algs for remote-only, and fail fast on HS without secret*Prevents HS/RS confusion, reduces duplication, and improves diagnostics.
- try { - const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let data: any - if (remoteJwks && !isSymmetric) { - data = (await jwtVerify(jwt, remoteJwks, options)).payload - } else { - data = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + try { + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + const remoteOnly = !!remoteJwks && !key + if (isSymmetric && remoteOnly) + throw new Error('HS* token presented but no local secret configured') + + const verifyKey = remoteJwks && !isSymmetric ? remoteJwks : key! + const verifyOptions = + (!isSymmetric && remoteOnly && !options?.algorithms) + ? { ...options, algorithms: ASYMMETRIC_VERIFICATION_ALGS } + : options + + const { payload: data } = await jwtVerify(jwt, verifyKey as any, verifyOptions)Add this constant once (module scope is fine):
const ASYMMETRIC_VERIFICATION_ALGS = [ 'RS256','RS384','RS512', 'PS256','PS384','PS512', 'ES256','ES384','ES512', 'EdDSA' ] as constPlease add tests:
- HS256 verifies with local secret when both
secret
andremoteJwks
are configured.- RS256 verifies via
remoteJwks
.- Remote-only config rejects HS256 with a clear error.
🧹 Nitpick comments (4)
src/index.ts (4)
227-233
: Avoidany
for the decoration objectGive
jwtDecoration
a precise shape to keep the API strongly typed.-let jwtDecoration: any = {} +type VerifyRet = + | (UnwrapSchema<Schema, ClaimType> & Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>) + | false +const jwtDecoration: { + verify: (jwt?: string, options?: JWTVerifyOptions) => Promise<VerifyRet> + sign?: ( + signValue: Omit<UnwrapSchema<Schema, ClaimType>, NormalizedClaim> & JWTPayloadInput + ) => Promise<string> +} = {} as any
409-411
: Minor: avoid verbose cast onkey
Non-null is guaranteed in this branch; simplify.
- return jwt.sign((key as Exclude<typeof key, undefined>) ) + return jwt.sign(key!)
413-421
: Avoid seeding secrets and functionsPlacing
secret
/remoteJwks
inseed
can leak through plugin introspection or logs. Seed only stable, non-sensitive config.return new Elysia({ name: '@elysiajs/jwt', seed: { name, - secret, - remoteJwks, schema, ...defaultValues } }).decorate(name as Name extends string ? Name : 'jwt', jwtDecoration)
223-225
: Runtime guard is good; consider clarifying the errorKeep the guard, but make the message actionable (e.g., list accepted alg families for each mode).
- if (!secret && !remoteJwks) throw new Error ('Either "secret" or "remoteJwks" must be provided') + if (!secret && !remoteJwks) + throw new Error('Configure either "secret" (HS*) or "remoteJwks" (RS*/PS*/ES*/EdDSA)')Also applies to: 413-423
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (1)
src/index.ts (1)
12-19
: LGTM: jose imports are correct and minimalAdding
decodeProtectedHeader
,JWTVerifyOptions
, andJWTVerifyGetKey
aligns with the new flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/index.ts (1)
419-422
: Don’t set iat by default when it’s undefined.Current logic sets
iat
whenever it’s not explicitlyfalse
, which changes behavior for callers who omit it. Only set whentrue
.- const setIat = 'iat' in signValue ? iat : defaultValues.iat - if (setIat !== false) { - jwt = jwt.setIssuedAt(new Date()) - } + const setIat = 'iat' in signValue ? iat : defaultValues.iat + if (setIat === true) { + jwt = jwt.setIssuedAt() + }
♻️ Duplicate comments (2)
src/index.ts (2)
236-239
: Avoidany
for the decoration object.Type the decoration via composition to keep inference and remove
any
.- let jwtDecoration: any = {} + const jwtDecoration: { + verify: (jwt?: string, options?: JWTVerifyOptions) => Promise< + (UnwrapSchema<Schema, ClaimType> & Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>) | false + > + sign?: ( + signValue: Omit<UnwrapSchema<Schema, ClaimType>, NormalizedClaim> & JWTPayloadInput + ) => Promise<string> + } = {} as any
263-299
: Unify verification flow; tighten options checks; add HS alg defaults.*
- Use a single
jwtVerify
call with computedverifyKey
andverifyOptions
.- Check
options?.algorithms === undefined
instead of truthiness to avoid overriding user-specified empty arrays.- Add HS* defaults on the local path for parity and defense-in-depth.
- Remove implicit
any
by destructuring{ payload }
.- try { - const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - const remoteOnly = remoteJwks && !key - if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwks && !isSymmetric) { - payload = (await jwtVerify(jwt, remoteJwks, - !options?.algorithms - ? { ...options, algorithms: ASYMMETRIC_VERIFICATION_ALGS } - : options) - ).payload - } else { - payload = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } - const data = payload as UnwrapSchema<Schema, ClaimType> & + try { + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + if (isSymmetric && remoteJwks && !key) return false // remote-only cannot verify HS* + + const verifyKey = + isSymmetric + ? (key as Exclude<typeof key, undefined>) + : (remoteJwks ?? (key as Exclude<typeof key, undefined>)) + + const verifyOptions = + options?.algorithms === undefined + ? { ...options, algorithms: isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS } + : options + + const { payload } = await jwtVerify(jwt, verifyKey, verifyOptions) + const data = payload as UnwrapSchema<Schema, ClaimType> & Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>
🧹 Nitpick comments (2)
src/index.ts (2)
189-215
: Clarify config contract: allow both or enforce one-of.Right now, the “remote-only” variant still allows
secret?: ...
, so both can be provided. If mixed local HS* + remote RS*/EC verification is intended, keep as-is and document it. If you want a strict one-of at the type level, switch tosecret?: never
in the remote variant.Apply if you want strict one-of:
| (BaseJWTOption<Name, Schema> & { - secret?: string | Uint8Array | CryptoKey | JWK | KeyObject + secret?: never /** * Remote JWKS * Use jose's `createRemoteJWKSet(new URL(...))` to create the JWKS function */ remoteJwks: JWTVerifyGetKey })
424-426
: Minor: simplify non-null assertion.Use
key!
and drop extra parentheses.- return jwt.sign((key as Exclude<typeof key, undefined>) ) + return jwt.sign(key!)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (4)
src/index.ts (4)
12-19
: Imports of jose helpers look correct.Bringing in
decodeProtectedHeader
,JWTVerifyOptions
, andJWTVerifyGetKey
is appropriate for the new verification flow.
162-188
: Solid factoring of common JWT options.
BaseJWTOption
cleanly isolates shared fields and preserves generics forname
/schema
.
229-235
: Good upfront guard for missing key material.Early fail on neither
secret
norremoteJwks
reduces footguns.
428-436
: Confirm seed semantics with function fields.Including
remoteJwks
(a function) inseed
may break if Elysia serializes or clones seeds. If seeds aren’t meant to carry non-serializable values, omit it or store a stable descriptor (e.g., URL) instead.Do we know whether Elysia ever serializes plugin
seed
? If yes, consider droppingremoteJwks
fromseed
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/index.ts (1)
189-215
: Decide: allow both secret and remoteJwks, or enforce an XOR at the type level.The current union permits configurations with both
secret
andremoteJwks
. If that’s intentional (sign locally, verify remotely), keep it. If you want “exactly one-of”, encode it explicitly to prevent accidental collisions.Example XOR helper:
type XOR<A, B> = (A | B) extends object ? (A & { [K in Exclude<keyof B, keyof A>]?: never }) | (B & { [K in Exclude<keyof A, keyof B>]?: never }) : A | B type SecretCfg<Name extends string | undefined, Schema extends TSchema | undefined> = BaseJWTOption<Name, Schema> & { secret: string | Uint8Array | CryptoKey | JWK | KeyObject } type RemoteCfg<Name extends string | undefined, Schema extends TSchema | undefined> = BaseJWTOption<Name, Schema> & { remoteJwks: JWTVerifyGetKey } export type JWTOption< Name extends string | undefined = 'jwt', Schema extends TSchema | undefined = undefined > = XOR<SecretCfg<Name, Schema>, RemoteCfg<Name, Schema>>
🧹 Nitpick comments (5)
src/index.ts (5)
216-224
: Alg lists are good; avoid later casts by passing a spread copy when used.Keeping these as
as const
is fine. When wiring intooptions.algorithms
, pass[...]
to satisfystring[]
withoutas unknown as string[]
.
238-248
: Dropas any
on jwtDecoration initialization.You’ve already declared an explicit type; avoid weakening it with
any
.- } = {} as any + } = {} as { + verify: (jwt?: string, options?: JWTVerifyOptions) => + Promise<(UnwrapSchema<Schema, ClaimType> & Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>) | false> + sign?: ( + signValue: Omit<UnwrapSchema<Schema, ClaimType>, NormalizedClaim> & JWTPayloadInput + ) => Promise<string> + }
432-434
: UsesetIssuedAt()
without an argument.Slightly cleaner; jose defaults to “now” when no value is provided.
- if (setIat !== false) { - jwt = jwt.setIssuedAt(new Date()) - } + if (setIat !== false) { + jwt = jwt.setIssuedAt() + }
440-449
: Seed contents: confirm storingremoteJwks
(a function) inseed
is intentional.If any tooling serializes or logs seeds, a function there may be surprising. If unnecessary in seed, omit it and keep it closed over in the decorator scope.
162-188
: Doc nit in example.Minor: “JWT_SECRETS” → “JWT_SECRET” to avoid suggesting multiple secrets as a single env var.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (1)
src/index.ts (1)
236-237
: Good guard for required configuration.Prevents silent misconfiguration when neither
secret
norremoteJwks
is provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/index.ts (1)
356-369
: Default alg only for symmetric keys; require explicit alg for non-symmetric secretsAvoid silently assuming HS256 when the provided key is not symmetric.
- const JWTHeader = { - alg: defaultValues.alg ?? 'HS256', + const isSymmetricSecret = + typeof secret === 'string' || secret instanceof Uint8Array + const resolvedAlg = defaultValues.alg ?? (isSymmetricSecret ? 'HS256' : undefined) + if (!resolvedAlg) + throw new Error('Missing "alg": required when using a non-symmetric signing key') + const JWTHeader = { + alg: resolvedAlg, b64: defaultValues.b64, crit: defaultValues.crit, cty: defaultValues.cty, jku: defaultValues.jku, jwk: defaultValues.jwk, kid: defaultValues.kid, typ: defaultValues.typ ?? 'JWT', x5c: defaultValues.x5c, x5t: defaultValues.x5t, x5u: defaultValues.x5u } as JWTHeaderParameters
♻️ Duplicate comments (1)
src/index.ts (1)
291-308
: Unify verification, fix boolean type, and default HS algs too
- remoteOnly is typed as string|URL; coerce to boolean.
- Collapse duplicate branches; pick key by alg.
- Default algorithms for HS* path as well.
try { - const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - const remoteOnly = remoteJwksUrl && !key - if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwksUrl && !isSymmetric) { - const remoteVerifyOptions: JWTVerifyOptions = !options - ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : (!options.algorithms - ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : options) - payload = (await jwtVerify(jwt, remoteJwks!, remoteVerifyOptions) - ).payload - } else { - payload = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + const remoteOnly = !!remoteJwks && !key + if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') + + // Prefer local secret for HS*; prefer remote for asymmetric algs when available + const verifyKey = remoteJwks && !isSymmetric + ? remoteJwks + : (key as Exclude<typeof key, undefined>) + + const mergedOptions: JWTVerifyOptions = options + ? { + ...options, + algorithms: + options.algorithms ?? + [...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)] + } + : { + algorithms: [ + ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS) + ] + } + + const { payload } = await jwtVerify(jwt, verifyKey, mergedOptions)
🧹 Nitpick comments (3)
src/index.ts (3)
189-215
: Accept a JWKS getKey function and options (not just URL) for testability and controlAllow passing a prebuilt getKey (e.g., createRemoteJWKSet(...)) and optional jose options. This avoids mandatory network in tests and lets callers tune caching/cooldowns.
Apply:
@@ -import { +import { SignJWT, jwtVerify, createRemoteJWKSet, decodeProtectedHeader, + type JWTVerifyGetKey, type CryptoKey, type JWK, type KeyObject, type JoseHeaderParameters, type JWTVerifyOptions } from 'jose' @@ export type JWTOption< @@ > = | (BaseJWTOption<Name, Schema> & { @@ - remoteJwksUrl?: string | URL + remoteJwksUrl?: string | URL + /** + * Prebuilt JWKS key resolver function (overrides remoteJwksUrl if provided) + */ + remoteJwks?: JWTVerifyGetKey + /** + * Options forwarded to createRemoteJWKSet(url, options) + */ + remoteJwksOptions?: Parameters<typeof createRemoteJWKSet>[1] }) | (BaseJWTOption<Name, Schema> & { @@ - remoteJwksUrl: string | URL + remoteJwksUrl: string | URL + remoteJwks?: never + remoteJwksOptions?: Parameters<typeof createRemoteJWKSet>[1] })
231-244
: Resolve JWKS from function or URL; support jose optionsInitialize the getKey from either a provided function or URL, and forward options.
@@ export const jwt = < @@ - secret, - remoteJwksUrl, + secret, + remoteJwksUrl, + remoteJwks: remoteJwksGetKey, + remoteJwksOptions, schema, ...defaultValues }: // End JWT Payload JWTOption<Name, Schema>) => { - if (!secret && !remoteJwksUrl) throw new Error('Either "secret" or "remoteJwksUrl" must be provided') + if (!secret && !remoteJwksUrl && !remoteJwksGetKey) + throw new Error('Either "secret", "remoteJwksUrl", or "remoteJwks" must be provided') - const remoteJwks = remoteJwksUrl - ? createRemoteJWKSet( - typeof remoteJwksUrl === 'string' - ? new URL(remoteJwksUrl) - : remoteJwksUrl) - : undefined + const remoteJwks = + remoteJwksGetKey ?? + (remoteJwksUrl + ? createRemoteJWKSet( + typeof remoteJwksUrl === 'string' ? new URL(remoteJwksUrl) : remoteJwksUrl, + remoteJwksOptions + ) + : undefined)
449-458
: Avoid exposing raw secret in plugin seedSeed is often introspected/logged; keep only capability flags.
return new Elysia({ name: '@elysiajs/jwt', seed: { name, - secret, + hasSecret: !!secret, remoteJwks, schema, ...defaultValues } }).decorate(name as Name extends string ? Name : 'jwt', jwtDecoration)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (3)
src/index.ts (3)
12-14
: Add jose imports for remote JWKs and header decode — LGTMBoth are used and appropriate.
162-188
: Type-layering with BaseJWTOption is clearGood separation of common fields and JSDoc.
216-224
: Readonly alg allowlists — LGTMMarked as const; neat separation of symmetric/asymmetric.
…or testing and internal implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/index.ts (3)
162-215
: Clarify config intent; enforce remote-only at the type level (or document allowing both).The second variant still permits
secret
alongsideremoteJwks
, so the union does not encode remote-only. If remote-only is intended, makesecret
impossible in that branch.Apply this diff:
- | (BaseJWTOption<Name, Schema> & { + | (BaseJWTOption<Name, Schema> & { /** * JWT Secret */ - secret?: string | Uint8Array | CryptoKey | JWK | KeyObject + secret?: never /** * Remote JWKS * Use jose's `createRemoteJWKSet(new URL(...))` to create the JWKS function */ remoteJwks: JWTVerifyGetKey })If supporting both simultaneously is desired, keep the first variant (secret-required,
remoteJwks?
) and explicitly document that “both” is allowed, while the second branch enforces remote-only.
216-224
: Good readonly alg sets; also use the symmetric list in verification.
SYMMETRIC_VERIFICATION_ALGS
is defined but not applied; default to it for HS* to prevent HS/RS confusion.
284-301
: Unify verification path and set safe default algs for both HS and asymmetric.*Removes duplicated branches and ensures HS* defaults when verifying with a local secret.
- const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - const remoteOnly = remoteJwks && !key - if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwks && !isSymmetric) { - const remoteVerifyOptions: JWTVerifyOptions = !options - ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : (!options.algorithms - ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : options) - payload = (await jwtVerify(jwt, remoteJwks!, remoteVerifyOptions) - ).payload - } else { - payload = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + const remoteOnly = !!remoteJwks && !key + if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') + + // Prefer local secret for HS*; prefer remote for asymmetric algs when available + const verifyKey = (remoteJwks && !isSymmetric) + ? remoteJwks + : (key as Exclude<typeof key, undefined>) + + const mergedOptions: JWTVerifyOptions = options + ? { + ...options, + algorithms: options.algorithms ?? [ + ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS) + ] + } + : { + algorithms: [ + ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS) + ] + } + + const { payload } = await jwtVerify(jwt, verifyKey, mergedOptions)
🧹 Nitpick comments (2)
src/index.ts (2)
309-311
: Consider exposing error reasons for observability.Returning
false
for all failures hides root causes (expired, bad audience, unknown kid, network). Optionally support a hook (e.g.,onVerifyError(err)
) or a debug flag to surface reasons.
431-436
: Nit: use jose’s implicit “now” for iat.Slightly simpler and equivalent.
- if (setIat !== false) { - jwt = jwt.setIssuedAt(new Date()) - } + if (setIat !== false) { + jwt = jwt.setIssuedAt() + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (6)
src/index.ts (6)
12-12
: Imports look correct and minimal.Type-only imports used where appropriate. No action needed.
Also applies to: 17-19
236-236
: Strong guard for required keys.Early fail when neither
secret
norremoteJwks
is provided is correct.
238-240
: Key normalization LGTM.UTF-8 encoding for string secrets is appropriate.
264-273
: Decorator shape/types are clear.Return typing
Promise<Claims | false>
is precise and helpful for consumers.
305-307
: Schema validation on verified payload is correct.Throws
ValidationError
pre-catch; behavior is consistent with existing plugin semantics.
438-440
: Signing with normalized key LGTM.Covers string, raw bytes, and KeyLike cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/index.ts (3)
284-301
: Unify verification flow and default algs for HS path as well.Collapse the duplicate branches, choose key by alg, and set default
algorithms
for both HS and non-HS. This also removes theremoteVerifyOptions
fork.- // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwks && !isSymmetric) { - const remoteVerifyOptions: JWTVerifyOptions = !options - ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : (!options.algorithms - ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : options) - payload = (await jwtVerify(jwt, remoteJwks, remoteVerifyOptions) - ).payload - } else { - payload = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + // Prefer local secret for HS*; prefer remote for asymmetric algs when available + const verifyKey = (remoteJwks && !isSymmetric) + ? remoteJwks + : (key as Exclude<typeof key, undefined>) + const mergedOptions: JWTVerifyOptions = options + ? { ...options, algorithms: options.algorithms ?? [...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)] } + : { algorithms: [...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)] } + const { payload } = await jwtVerify(jwt, verifyKey, mergedOptions)
442-451
: Do not persist raw secrets in the plugin seed.Seeds can be inspected/logged. Remove
secret
and expose a non-sensitive flag instead.seed: { name, - secret, - remoteJwks, + hasSecret: !!secret, + remoteJwks, schema, ...defaultValues }
189-215
: Make the union disjoint to avoid ambiguous option shapes.When both arms accept
secret
, TS can’t reliably narrow. Disallowsecret
in the remote-only arm.| (BaseJWTOption<Name, Schema> & { /** * JWT Secret */ - secret?: string | Uint8Array | CryptoKey | JWK | KeyObject + secret?: never /** * Remote JWKS * Use jose's `createRemoteJWKSet(new URL(...))` to create the JWKS function */ remoteJwks: JWTVerifyGetKey })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(4 hunks)
🔇 Additional comments (6)
src/index.ts (6)
12-19
: LGTM: correct imports for the new flow.Adding decodeProtectedHeader, JWTVerifyOptions, and JWTVerifyGetKey is appropriate.
162-188
: Solid base option shape.Merging JWTHeaderParameters and JWTPayloadInput into BaseJWTOption is clean and keeps defaults typed.
216-223
: Alg allow-lists look good.Readonly tuples for asymmetric and symmetric algs are a good hardening step.
231-241
: Good guardrails and key normalization.Throwing when neither
secret
norremoteJwks
is provided and normalizing the secret to Uint8Array are correct.
264-273
: Typed verify contract is clear.Returning
false
on failure and a typed payload on success is consistent with the plugin’s prior API.
438-440
: LGTM: sign uses normalized key.Signing with the normalized
key
(when present) is correct given the gating above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/index.ts (4)
218-226
: Locking alg sets is solid; add symmetric defaults usage in verify.Readonly tuples are great. Next, ensure HS* verification always defaults to SYMMETRIC_VERIFICATION_ALGS when caller doesn’t provide algorithms.
Apply together with the verification refactor below.
286-303
: Unify verify path, set safe default alg lists, and avoid duplicate branches.Single flow reduces bugs and fixes missing HS* default algorithms; also avoids spreading possibly-undefined options in one branch.
Apply:
- try { - const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - const remoteOnly = remoteJwks && !key - if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwks && !isSymmetric) { - const remoteVerifyOptions: JWTVerifyOptions = !options - ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : (!options.algorithms - ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : options) - payload = (await jwtVerify(jwt, remoteJwks, remoteVerifyOptions) - ).payload - } else { - payload = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + try { + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + const remoteOnly = !!remoteJwks && !secret + if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') + + const verifyKey = (remoteJwks && !isSymmetric) + ? remoteJwks + : await getLocalKeyForAlg(typeof alg === 'string' ? alg : undefined) + + const mergedOptions: JWTVerifyOptions = options ? { ...options } : {} + if (!mergedOptions.algorithms) { + mergedOptions.algorithms = [ + ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS) + ] as unknown as string[] + } + const { payload } = await jwtVerify(jwt, verifyKey as any, mergedOptions)
433-440
: Fix iat handling: don’t coerce numeric/string iat through Date; use jose’s overloads.Current code converts numbers to milliseconds Date and always sets a value; respect “true” for now and pass through provided values.
Apply:
- const setIat = 'iat' in signValue ? iat : (defaultValues.iat ?? true) - if (setIat === true) { - jwt = jwt.setIssuedAt(new Date()) - } else { - jwt = jwt.setIssuedAt(new Date(setIat as string | number | Date)) - } + const setIat = 'iat' in signValue ? iat : defaultValues.iat + if (setIat === true) { + jwt = jwt.setIssuedAt() + } else if (setIat !== undefined) { + jwt = jwt.setIssuedAt(setIat as number | string | Date) + }
446-455
: Remove raw secret from plugin seed.Secrets can leak via introspection/logs; expose only non-sensitive metadata.
Apply:
return new Elysia({ name: '@elysiajs/jwt', seed: { name, - secret, remoteJwks, + hasSecret: !!secret, schema, ...defaultValues } }).decorate(name as Name extends string ? Name : 'jwt', jwtDecoration)
🧹 Nitpick comments (1)
src/index.ts (1)
351-363
: Nit: avoid duplicate alg in protected header.alg is already present in JWTHeader; passing it twice is redundant.
Apply:
- let jwt = new SignJWT({ ...JWTPayload }).setProtectedHeader({ - alg: JWTHeader.alg!, - ...JWTHeader - }) + let jwt = new SignJWT({ ...JWTPayload }).setProtectedHeader(JWTHeader)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(5 hunks)
🔇 Additional comments (4)
src/index.ts (4)
131-132
: Type-level win: iat accepts true | number | string | Date.This unlocks correct “pass-through” handling for non-boolean values.
189-216
: Good: discriminated union cleanly encodes secret vs remoteJwks.Enables remote-only and avoids config collisions. Nice.
238-238
: Good guard: require at least one of secret or remoteJwks.Clear and actionable error.
266-275
: API shape of jwtDecoration looks good.verify returns payload-or-false; sign gated behind presence of secret. Solid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/index.test.ts (1)
85-99
: Fix user-facing messages (“setted” → “set”).Improve professionalism of error text.
Apply:
- message: 'exp was not setted on jwt' + message: 'exp was not set on JWT' @@ - message: 'iat was not setted on jwt' + message: 'iat was not set on JWT'
♻️ Duplicate comments (3)
src/index.ts (3)
296-313
: Unify verification into a single call and default algs for HS path.Removes duplication and ensures HS* defaults to SYMMETRIC_VERIFICATION_ALGS when caller doesn’t supply options.
Apply:
- // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwks && !isSymmetric) { - const remoteVerifyOptions: JWTVerifyOptions = !options - ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : (!options.algorithms - ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : options) - payload = (await jwtVerify(jwt, remoteJwks, remoteVerifyOptions) - ).payload - } else { - payload = (await jwtVerify(jwt, (key as Exclude<typeof key, undefined>), options)).payload - } + // Prefer local secret for HS*, prefer remoteJwks for asymmetric when available + const verifyKey = (remoteJwks && !isSymmetric) + ? remoteJwks + : (key as Exclude<typeof key, undefined>) + + const mergedOptions: JWTVerifyOptions = options + ? { + ...options, + algorithms: options.algorithms ?? [ + ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS) + ] + } + : { + algorithms: [ + ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS) + ] + } + + const { payload } = await jwtVerify(jwt, verifyKey, mergedOptions)
456-465
: Do not persist secrets in plugin seed.Seed can be inspected/logged; storing
secret
is unsafe. ExposehasSecret
instead.Apply:
return new Elysia({ name: '@elysiajs/jwt', seed: { name, - secret, remoteJwks, + hasSecret: !!secret, schema, ...defaultValues } }).decorate(name as Name extends string ? Name : 'jwt', jwtDecoration)Run to ensure no other seed leaks:
#!/bin/bash rg -n -C2 'seed:\s*\{' | sed -n '1,200p' rg -nP "seed\s*:\s*\{[^}]*secret" -C1
445-450
: Bug: truthy check drops valid iat=0 and empty-string timestamps.Use explicit type checks; only
true
should set “now”.Apply:
- const setIat = 'iat' in signValue ? iat : (defaultValues.iat ?? true) - if (setIat === true) { - jwt = jwt.setIssuedAt() - } else if (setIat) { - jwt = jwt.setIssuedAt(setIat as string | number | Date) - } + const setIat = 'iat' in signValue ? iat : (defaultValues.iat ?? true) + if (setIat === true) { + jwt = jwt.setIssuedAt() + } else if ( + typeof setIat === 'number' || + typeof setIat === 'string' || + setIat instanceof Date + ) { + jwt = jwt.setIssuedAt(setIat as number | string | Date) + }
🧹 Nitpick comments (2)
src/index.ts (1)
239-253
: Key normalization and JWK import path are mostly right; consider caching.
key
handles strings/KeyLike and defers JWK togetKeyForAlg
. To avoid repeatedimportJWK
work on hot paths, cache imported keys per alg.Apply this minimal cache:
- const getKeyForAlg = (alg: string) => { - return importJWK(secret as JWK, alg) - } + const importedKeyCache = new Map<string, Promise<CryptoKey>>() + const getKeyForAlg = (alg: string) => { + if (!importedKeyCache.has(alg)) { + importedKeyCache.set(alg, importJWK(secret as JWK, alg)) + } + return importedKeyCache.get(alg)! + }test/index.test.ts (1)
198-198
: Add coverage for remote JWKS (RS256 verify) and HS256-with-remote coexistence.Current tests don’t exercise the new
remoteJwks
path.Proposed additions (appended to this file):
+import { createLocalJWKSet, exportJWK, generateKeyPair } from 'jose' + +it('verifies RS256 via remoteJwks and HS256 via local secret when both configured', async () => { + // RS256 key pair + local JWKS resolver + const { publicKey, privateKey } = await generateKeyPair('RS256') + const pubJwk = await exportJWK(publicKey) + Object.assign(pubJwk, { alg: 'RS256', kid: 'test' }) + const getKey = createLocalJWKSet({ keys: [pubJwk] }) + + const appDual = new Elysia() + .use(jwt({ name: 'jwt', secret: TEST_SECRET, remoteJwks: getKey })) + .post('/verify', async ({ jwt, body }) => ({ ok: !!(await jwt.verify(body.token)) }), { + body: t.Object({ token: t.String() }) + }) + + // RS256 token -> remoteJwks + const rsToken = await new SignJWT({ role: 'remote' }) + .setProtectedHeader({ alg: 'RS256', kid: 'test' }) + .setExpirationTime('5m') + .sign(privateKey) + const rsResp = await appDual.handle(post('/verify', { token: rsToken })) + expect((await rsResp.json()).ok).toBe(true) + + // HS256 token -> local secret (should not go remote) + const hsToken = await new SignJWT({ role: 'local' }) + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(TEST_SECRET)) + const hsResp = await appDual.handle(post('/verify', { token: hsToken })) + expect((await hsResp.json()).ok).toBe(true) +})
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/index.ts
(6 hunks)test/index.test.ts
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/index.test.ts (1)
src/index.ts (1)
jwt
(228-466)
🔇 Additional comments (6)
src/index.ts (4)
12-20
: New jose imports look correct.
importJWK
,decodeProtectedHeader
,JWTVerifyOptions
, andJWTVerifyGetKey
are appropriate for the new flows.
132-132
: Extending iat input typing is appropriate.Allows boolean | number | string | Date as intended.
163-218
: Discriminated union for JWTOption is solid.Accurately encodes the “one-of” secret | remoteJwks contract and disables sign for remote-only.
219-227
: Algorithm lists defined as readonly are good.Clear split between asymmetric and symmetric algs.
test/index.test.ts (2)
31-34
: Non-null assertion on sign is fine.Matches the remote-only shape where
sign
may be undefined.
44-49
: Correctly disables exp and iat defaults.
exp: undefined
andiat: false
cover both claims.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/index.ts (2)
296-314
: Fix HS + JWK verify and wrong remoteOnly; unify key selection and defaults*HS* tokens with a JWK secret fail because
key
isundefined
here. Also,remoteOnly
tests!key
instead of!secret
. Select the verify key once (local for HS*, remote for asymmetric) and set safe defaultalgorithms
for both paths.Apply:
- const { alg } = decodeProtectedHeader(jwt) - const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') - const remoteOnly = remoteJwks && !key - if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret') - // Prefer local secret for HS*; prefer remote for asymmetric algs when available - let payload - if (remoteJwks && !isSymmetric) { - const remoteVerifyOptions: JWTVerifyOptions = !options - ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : (!options.algorithms - ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } - : options) - payload = (await jwtVerify(jwt, remoteJwks, remoteVerifyOptions) - ).payload - } else { - payload = (await jwtVerify(jwt, key!, options)).payload - } + const { alg } = decodeProtectedHeader(jwt) + const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') + const hasLocalSecret = !!secret + if (isSymmetric && !hasLocalSecret) + throw new Error('HS* algorithm requires a local secret') + + // Prefer local secret for HS*; prefer remote for asymmetric algs when available + const verifyKey = remoteJwks && !isSymmetric + ? remoteJwks + : await getLocalKeyForAlg(alg) + + const mergedOptions: JWTVerifyOptions = options + ? { + ...options, + algorithms: + options.algorithms ?? + [...(verifyKey === remoteJwks ? ASYMMETRIC_VERIFICATION_ALGS : SYMMETRIC_VERIFICATION_ALGS)] + } + : { + algorithms: [ + ...(verifyKey === remoteJwks ? ASYMMETRIC_VERIFICATION_ALGS : SYMMETRIC_VERIFICATION_ALGS) + ] + } + + const { payload } = await jwtVerify( + jwt, + verifyKey as Parameters<typeof jwtVerify>[1], + mergedOptions + )
241-252
: Generalize local key resolver; reuse across sign/verify (handles JWK).Create a single resolver that imports JWKs and falls back to the pre-derived
key
. This unblocks HS* verify with JWK and de-duplicates logic.Apply:
- const getKeyForAlg = (alg: string) => { - return importJWK(secret as JWK, alg) - } + const isJwkSecret = + typeof secret === 'object' && + secret !== null && + 'kty' in (secret as Record<string, unknown>) + + const getLocalKeyForAlg = async (alg?: string) => { + if (!secret) throw new Error('Missing local secret') + return isJwkSecret + ? importJWK(secret as JWK, alg) + : (key as Exclude<typeof key, undefined>) + } @@ - const key = secret - ? (typeof secret === 'object' - && ('kty' in (secret as Record<string, unknown>)) - ? undefined - : typeof secret === 'string' - ? new TextEncoder().encode(secret) - : secret) - : undefined + const key = secret + ? (isJwkSecret + ? undefined + : (typeof secret === 'string' + ? new TextEncoder().encode(secret) + : secret)) + : undefined
🧹 Nitpick comments (3)
src/index.ts (3)
455-456
: Use the unified key resolver in sign; minor cleanup.Leverage
getLocalKeyForAlg
and drop extra parens.Apply:
- return jwt.sign((key ?? await getKeyForAlg(JWTHeader.alg!)) ) + return jwt.sign(await getLocalKeyForAlg(JWTHeader.alg))
420-422
: Remove redundant alg override in protected header.
JWTHeader.alg
is already set; spreading then re-setting is unnecessary.Apply:
- let jwt = new SignJWT({ ...JWTPayload }).setProtectedHeader({ - ...JWTHeader, - alg: JWTHeader.alg! - }) + let jwt = new SignJWT({ ...JWTPayload }).setProtectedHeader(JWTHeader)
459-466
: Seed hygiene: expose non-sensitive metadata only.Good job omitting
secret
. Consider adding a boolean flag for observability without secret leakage.Apply:
seed: { name, schema, + hasSecret: !!secret, ...defaultValues }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(6 hunks)
🔇 Additional comments (5)
src/index.ts (5)
12-20
: Imports look correct.
importJWK
,decodeProtectedHeader
, and types are appropriate for the new API surface.
132-133
: Input iat typing expanded appropriately.Matches the new sign logic that distinguishes
true
vs explicit values.
163-189
: Base option typing is clear and well-documented.Sets a solid foundation for discriminated unions.
190-218
: Discriminated union forJWTOption
is spot-on.Remote-only disables signing via
secret?: never
; no collision with local secret.
276-285
: Typed decoration surface looks good.Accurate return types and async
sign
.
const ASYMMETRIC_VERIFICATION_ALGS = [ | ||
'RS256','RS384','RS512', | ||
'PS256','PS384','PS512', | ||
'ES256','ES384','ES512', | ||
'EdDSA' | ||
] as const | ||
|
||
const SYMMETRIC_VERIFICATION_ALGS = ['HS256', 'HS384', 'HS512'] as const | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Enforce symmetric defaults on local HS verification.*
SYMMETRIC_VERIFICATION_ALGS
is defined but unused. Use it as the default when verifying with a local secret. Covered by the verify diff above.
Consider adding tests to assert:
- HS256 verifies locally even with
remoteJwks
set. - RS256 verifies via
remoteJwks
. - HS256 with only
remoteJwks
throws the intended error.
🏁 Script executed:
#!/usr/bin/env bash
# Locate the constant definitions
rg -n "const SYMMETRIC_VERIFICATION_ALGS" -C2 .
rg -n "const ASYMMETRIC_VERIFICATION_ALGS" -C2 .
# Search for any references to SYMMETRIC_VERIFICATION_ALGS in verification logic
rg -n "SYMMETRIC_VERIFICATION_ALGS" -C2 src
# Inspect the verify function signature and defaults
rg -nP "\bfunction\s+verify\b" -C3 src/index.ts
Length of output: 1435
🏁 Script executed:
#!/usr/bin/env bash
# Extract verify implementation from src/index.ts
sed -n '250,350p' src/index.ts
# Search for jwtVerify import and usage
rg -n "import .*jwtVerify" -C2 src/index.ts
rg -n "jwtVerify" -C5 src/index.ts
# Search for local (secret-based) verification usage (e.g., jws or jwt.verify)
rg -nP "(jws|jwt)\s*\.verify" -C5 src/index.ts
Length of output: 4072
Enforce symmetric defaults for local HS verification*
Use SYMMETRIC_VERIFICATION_ALGS
as the default algorithms
when calling jwtVerify
with a local secret (i.e., in the else
branch before payload = …
) so HS* tokens can’t be verified with unintended algs. Add tests to confirm:
- HS256 verifies locally even if
remoteJwks
is provided. - RS256 uses
remoteJwks
. - HS256 with only
remoteJwks
throws the expected error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (3)
src/index.ts (3)
224-231
: Readonly alg lists + symmetric set.Nice hardening; tuple types prevent mutation and prep for defaults.
450-458
: Good: iat handling preserves explicit values.
true
=> now; number/string/Date => pass-through; otherwise unset.
464-471
: Good: seed no longer persists secrets.Avoids leaking sensitive material via plugin metadata.
🧹 Nitpick comments (3)
src/index.ts (3)
12-20
: Type the key resolver (import KeyLike).
getKeyForAlg
returns a KeyLike; import the type for accuracy.import { SignJWT, jwtVerify, importJWK, decodeProtectedHeader, + type KeyLike, type CryptoKey, type JWK, type KeyObject, type JoseHeaderParameters, type JWTVerifyOptions, type JWTVerifyGetKey } from 'jose'
244-248
: Type the local JWK import helper.Return a
Promise<KeyLike>
for better inference.- const getKeyForAlg = (alg: string) => { - return importJWK(secret as JWK, alg) - } + const getKeyForAlg = (alg: string): Promise<KeyLike> => importJWK(secret as JWK, alg)
366-378
: Infer sensible default alg from secret type (optional).Defaulting to HS256 breaks when
secret
is RSA/EC/OKP JWK. Prefer the secret’salg
or kty-based default.- const JWTHeader = { - alg: defaultValues.alg ?? 'HS256', + const JWTHeader = { + alg: defaultValues.alg ?? inferDefaultAlgFromSecret(secret),Place this helper near
getKeyForAlg
:function inferDefaultAlgFromSecret( secret: string | Uint8Array | CryptoKey | JWK | KeyObject | undefined ): string { if (!secret) return 'HS256' if (typeof secret === 'string' || secret instanceof Uint8Array) return 'HS256' if (typeof secret === 'object' && secret && 'kty' in (secret as Record<string, unknown>)) { const jwk = secret as JWK if (typeof jwk.alg === 'string') return jwk.alg switch (jwk.kty) { case 'RSA': return 'RS256' case 'EC': return 'ES256' case 'OKP': return 'EdDSA' case 'oct': return 'HS256' } } return 'HS256' }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/index.ts
(6 hunks)test/index.test.ts
(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/index.test.ts (1)
src/index.ts (1)
jwt
(233-472)
🔇 Additional comments (6)
src/index.ts (3)
132-133
: Widened iat input is correct.Allowing
boolean | number | string | Date
aligns input vs. normalized payload.
163-189
: Good: extracted BaseJWTOption with name/schema.This clarifies shared fields and improves ergonomics.
190-223
: Good: discriminated union for secret | jwks.Encodes the “one-of” at the type level and documents behavior when both are present.
test/index.test.ts (3)
33-37
: Non-null assertion on sign is appropriate here.
sign
is optional only whensecret
is absent; tests configure a secret.
46-52
: Same here: safe use ofjwt.sign!
.
91-99
: Message copy fix LGTM.“set on jwt” reads correctly.
try { | ||
const { alg } = decodeProtectedHeader(jwt) | ||
const isSymmetric = typeof alg === 'string' && alg.startsWith('HS') | ||
const asymmetricOnly = jwks && !key | ||
if (isSymmetric && asymmetricOnly) throw new Error('HS* algorithm requires a local secret') | ||
// Prefer local secret for HS*; prefer remote for asymmetric algs when available | ||
let payload | ||
if (jwks && !isSymmetric) { | ||
const jwksVerifyOptions: JWTVerifyOptions = !options | ||
? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } | ||
: (!options.algorithms | ||
? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] } | ||
: options) | ||
payload = (await jwtVerify(jwt, jwks, jwksVerifyOptions) | ||
).payload | ||
} else { | ||
payload = (await jwtVerify(jwt, key!, options)).payload | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix: HS verify fails when secret is a JWK; also restrict default algs and remove duplication.*
- Bug:
key
isundefined
for JWK secrets, so HS* falls intokey!
and throws; also remote-only detection uses!key
which misclassifies JWK. - Hardening: default allowed
algorithms
should be symmetric for HS*, asymmetric for JWKS.
Apply:
- const { alg } = decodeProtectedHeader(jwt)
- const isSymmetric = typeof alg === 'string' && alg.startsWith('HS')
- const asymmetricOnly = jwks && !key
- if (isSymmetric && asymmetricOnly) throw new Error('HS* algorithm requires a local secret')
- // Prefer local secret for HS*; prefer remote for asymmetric algs when available
- let payload
- if (jwks && !isSymmetric) {
- const jwksVerifyOptions: JWTVerifyOptions = !options
- ? { algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] }
- : (!options.algorithms
- ? { ...options, algorithms: [...ASYMMETRIC_VERIFICATION_ALGS] }
- : options)
- payload = (await jwtVerify(jwt, jwks, jwksVerifyOptions)
- ).payload
- } else {
- payload = (await jwtVerify(jwt, key!, options)).payload
- }
+ const { alg } = decodeProtectedHeader(jwt)
+ const isSymmetric = typeof alg === 'string' && alg.startsWith('HS')
+ const remoteOnly = !!jwks && secret === undefined
+ if (isSymmetric && remoteOnly) throw new Error('HS* algorithm requires a local secret')
+
+ // Prefer local secret for HS*; prefer JWKS for asymmetric algs when available.
+ const verifyKey =
+ isSymmetric
+ ? (key ?? await getKeyForAlg(alg as string))
+ : (jwks ?? (key ?? await getKeyForAlg(alg as string)))
+
+ const mergedOptions: JWTVerifyOptions = options
+ ? {
+ ...options,
+ algorithms: options.algorithms ?? [
+ ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)
+ ]
+ }
+ : {
+ algorithms: [
+ ...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)
+ ]
+ }
+
+ const { payload } = await jwtVerify(jwt, verifyKey as any, mergedOptions)
Please add a test proving HS256 verify works when secret
is an oct JWK (see comment in tests).
Committable suggestion skipped: line range outside the PR's diff.
// Basic JWKS test | ||
it('Should verify RS256 via jwks and HS256 via local secret when both are configured', | ||
async () => { | ||
// RS256 key pair + jwks | ||
const { publicKey, privateKey } = await generateKeyPair('RS256') | ||
const pubJwk = await exportJWK(publicKey) | ||
Object.assign(pubJwk, { alg: 'RS256', kid: 'test' }) | ||
const getKey = createLocalJWKSet({ keys: [pubJwk] }) | ||
|
||
const jwksApp = new Elysia() | ||
.use(jwt({ name: 'jwt', secret: TEST_SECRET, jwks: getKey })) | ||
.post('/verify', async ({ jwt, body }) => { | ||
const token = await jwt.verify(body.token) | ||
return { | ||
token, | ||
ok: !!token | ||
} | ||
}, { | ||
body: t.Object({ token: t.String() }) | ||
}) | ||
.post('/sign', async ({ body, jwt }) => await jwt.sign!({ | ||
name: body.name, | ||
exp: undefined, | ||
iat: false, | ||
}), { | ||
body: t.Object({ name: t.String() }) | ||
}) | ||
|
||
// RS256 token -> jwks | ||
const rsToken = await new SignJWT({ role: 'local' }) | ||
.setProtectedHeader({ alg: 'RS256', kid: 'test' }) | ||
.setExpirationTime('5m') | ||
.sign(privateKey) | ||
const rsResp = await jwksApp.handle(post('/verify', { token: rsToken })) | ||
const rsRespJson = await rsResp.json() | ||
expect((rsRespJson.ok)).toBe(true) | ||
|
||
// HS256 token -> local secret | ||
const hsSignResp = await jwksApp.handle(post('/sign', { name: 'test' })) | ||
const hsToken = await hsSignResp.text() | ||
expect(decodeProtectedHeader(hsToken).alg).toBe('HS256') | ||
const hsResp = await jwksApp.handle(post('/verify', { token: hsToken })) | ||
const hsRespJson = await hsResp.json() | ||
expect(hsRespJson.ok).toBe(true) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Great coverage for JWKS + local secret routing; add a JWK-oct HS256 test.
Add a test to prove HS256 verify works when secret
is an oct JWK (and that HS* is rejected in jwks-only mode).
Example to append:
it('Should sign/verify HS256 using an oct JWK as local secret', async () => {
// Generate an HMAC key and export as JWK (oct)
const hmacKey = await crypto.subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' },
true,
['sign', 'verify']
)
const octJwk = await exportJWK(hmacKey)
octJwk.alg = 'HS256'
const appJwk = new Elysia()
.use(jwt({ name: 'jwt', secret: octJwk }))
.post('/sign', async ({ jwt }) => jwt.sign!({ user: 'oct', exp: '5m', iat: true }))
.post('/verify', async ({ jwt, body }) => ({ ok: !!(await jwt.verify(body.token)) }), {
body: t.Object({ token: t.String() })
})
const signed = await appJwk.handle(post('/sign', {})).then(r => r.text())
const verified = await appJwk.handle(post('/verify', { token: signed })).then(r => r.json())
expect(verified.ok).toBe(true)
})
it('Should reject HS256 when configured jwks-only', async () => {
const { publicKey } = await generateKeyPair('RS256')
const pubJwk = await exportJWK(publicKey); Object.assign(pubJwk, { alg: 'RS256' })
const getKey = createLocalJWKSet({ keys: [pubJwk] })
const appJwksOnly = new Elysia()
.use(jwt({ name: 'jwt', jwks: getKey })) // no secret
.post('/verify', async ({ jwt, body }) => ({ ok: !!(await jwt.verify(body.token)) }), {
body: t.Object({ token: t.String() })
})
const hsToken = await new SignJWT({ n: 1 })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('5m')
.sign(new TextEncoder().encode('local-only'))
const resp = await appJwksOnly.handle(post('/verify', { token: hsToken }))
const json = await resp.json()
expect(json.ok).toBe(false)
})
🤖 Prompt for AI Agents
In test/index.test.ts around lines 201 to 245, the reviewer asked to add two
tests: one proving HS256 verify works when the configured secret is an oct JWK,
and another proving HS* tokens are rejected when only jwks is configured. Add a
test that uses crypto.subtle.generateKey to create an HMAC key, exportJWK to
produce an oct JWK with alg='HS256', then mount jwt({ name:'jwt', secret: octJwk
}) with /sign and /verify endpoints and assert a signed HS256 token verifies
successfully. Add a second test that creates an RS256 jwks-only app
(createLocalJWKSet from exported public JWK), craft an HS256 token signed with a
raw TextEncoder secret, call the jwks-only /verify and assert verification fails
(ok === false). Ensure body schemas and async handling match existing tests and
reuse helpers (SignJWT, exportJWK, createLocalJWKSet, generateKeyPair).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/index.ts (1)
299-316
: Default local-verify algs aren’t restricted; enforce per HS/asymmetric.*JWKS branch defaults to asymmetric algs, but the local branch passes
options
as-is. Set safe defaults for the local path too (HS* whenisSymmetric
, asymmetric otherwise).- } else { - payload = (await jwtVerify(jwt, key ?? await getKeyForAlg(alg!), options)).payload - } + } else { + const localVerifyOptions: JWTVerifyOptions = !options + ? { algorithms: [...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)] } + : (!options.algorithms + ? { ...options, algorithms: [...(isSymmetric ? SYMMETRIC_VERIFICATION_ALGS : ASYMMETRIC_VERIFICATION_ALGS)] } + : options) + payload = (await jwtVerify(jwt, key ?? await getKeyForAlg(alg!), localVerifyOptions)).payload + }
🧹 Nitpick comments (2)
src/index.ts (2)
244-257
: Minor: guardgetKeyForAlg
for non-JWK secrets (defensive).Today it’s only called when
secret
is a JWK (sincekey
is undefined), but a small runtime assert prevents accidental misuse later.Apply:
-const getKeyForAlg = (alg: string) => { - return importJWK(secret as JWK, alg) -} +const getKeyForAlg = async (alg: string) => { + if (!(typeof secret === 'object' && secret && 'kty' in (secret as Record<string, unknown>))) { + throw new Error(`Expected a JWK "secret" to import for alg ${alg}`) + } + return importJWK(secret as JWK, alg) +}
299-316
: Optional: de-duplicate verify branches.Compute
verifyKey
andmergedOptions
once based onisSymmetric
and avoid twin await blocks.I can provide a unified diff if you’d like to reduce this to a single
jwtVerify
call.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/index.ts
(6 hunks)
🔇 Additional comments (6)
src/index.ts (6)
12-20
: Imports look correct and scoped.New jose imports (
importJWK
,decodeProtectedHeader
,JWTVerifyOptions
,JWTVerifyGetKey
) align with the usages below. No concerns.
128-133
: Allowing string/Date for iat is correct per jose API.
SignJWT#setIssuedAt
accepts number, Date, or a timespan string; your handling matches this. (github.com)
163-189
: Strong “base” option type is clean.Good separation of decoration name/schema from header/payload inputs.
190-223
: Discriminated union for secret | jwks reads well.This encodes the one-of contract clearly and avoids collisions.
224-232
: Alg allow-lists are solid.Readonly tuples for asymmetric and symmetric algs are a good hardening step.
444-454
: iat pass-through fix is correct.You now set “now” only when
iat === true
and pass number/string/Date through, which matches jose’s contract. (github.com)
{ | ||
jwt = jwt.setIssuedAt(setIat as string | number | Date) | ||
} | ||
|
||
return jwt.sign(key) | ||
}, | ||
async verify( | ||
jwt?: string, | ||
options?: JWTVerifyOptions | ||
): Promise< | ||
| (UnwrapSchema<Schema, ClaimType> & | ||
Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>) | ||
| false | ||
> { | ||
if (!jwt) return false | ||
|
||
try { | ||
const data: any = ( | ||
await (options | ||
? jwtVerify(jwt, key, options) | ||
: jwtVerify(jwt, key)) | ||
).payload | ||
|
||
if (validator && !validator.Check(data)) | ||
throw new ValidationError('JWT', validator, data) | ||
|
||
return data | ||
} catch (_) { | ||
return false | ||
} | ||
return jwt.sign((key ?? await getKeyForAlg(JWTHeader.alg!)) ) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fail fast on signing with asymmetric alg + symmetric secret.
If someone sets alg='RS256'
but provides a string/Uint8Array secret, jose
will throw deep in signing. Add a concise upfront error.
- return jwt.sign((key ?? await getKeyForAlg(JWTHeader.alg!)) )
+ const algIsSymmetric = typeof JWTHeader.alg === 'string' && JWTHeader.alg.startsWith('HS')
+ if (!algIsSymmetric && (typeof secret === 'string' || secret instanceof Uint8Array)) {
+ throw new Error(`Signing with ${JWTHeader.alg} requires a KeyObject/CryptoKey or JWK "secret"`)
+ }
+ return jwt.sign((key ?? await getKeyForAlg(JWTHeader.alg!)))
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{ | |
jwt = jwt.setIssuedAt(setIat as string | number | Date) | |
} | |
return jwt.sign(key) | |
}, | |
async verify( | |
jwt?: string, | |
options?: JWTVerifyOptions | |
): Promise< | |
| (UnwrapSchema<Schema, ClaimType> & | |
Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>) | |
| false | |
> { | |
if (!jwt) return false | |
try { | |
const data: any = ( | |
await (options | |
? jwtVerify(jwt, key, options) | |
: jwtVerify(jwt, key)) | |
).payload | |
if (validator && !validator.Check(data)) | |
throw new ValidationError('JWT', validator, data) | |
return data | |
} catch (_) { | |
return false | |
} | |
return jwt.sign((key ?? await getKeyForAlg(JWTHeader.alg!)) ) | |
} | |
{ | |
jwt = jwt.setIssuedAt(setIat as string | number | Date) | |
} | |
const algIsSymmetric = typeof JWTHeader.alg === 'string' && JWTHeader.alg.startsWith('HS') | |
if (!algIsSymmetric && (typeof secret === 'string' || secret instanceof Uint8Array)) { | |
throw new Error(`Signing with ${JWTHeader.alg} requires a KeyObject/CryptoKey or JWK "secret"`) | |
} | |
return jwt.sign((key ?? await getKeyForAlg(JWTHeader.alg!))) | |
} |
🤖 Prompt for AI Agents
In src/index.ts around lines 452 to 457, the signer allows an asymmetric
algorithm (e.g. RS256/ES*/PS*) while accepting a symmetric secret
(string/Uint8Array) which causes a deep error in jose; add an explicit early
validation before signing: detect when JWTHeader.alg indicates an asymmetric
algorithm and the resolved key (the provided key param or the result of await
getKeyForAlg(...)) is a raw symmetric secret (typeof key === 'string' || key
instanceof Uint8Array), and if so throw a clear TypeError (e.g. "Asymmetric
algorithm <alg> requires a private key, not a symmetric secret") so callers fail
fast; place this check immediately before calling jwt.sign and ensure it runs
for both direct key param and keys returned by getKeyForAlg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this with Kinde, and it works great! Thanks
Support for strongly-typed remote JWKS that doesn't collide with existing 'secret' implementation
Summary by CodeRabbit
New Features
Breaking Changes
Tests