Skip to content

Conversation

jmlow
Copy link
Contributor

@jmlow jmlow commented Aug 29, 2025

Support for strongly-typed remote JWKS that doesn't collide with existing 'secret' implementation

Summary by CodeRabbit

  • New Features

    • Added remote JWKS support for verification and prefer JWKS for asymmetric algs.
    • Exposed sign and verify via a jwt decoration on the app instance.
    • iat option now accepts boolean, number, string, or Date.
  • Breaking Changes

    • JWT options are a discriminated union: either secret (optional jwks) or jwks-only; HS algorithms require a local secret.
    • Initialization now returns a decorated instance instead of the previous seed.
  • Tests

    • Added JWKS integration tests covering RS256 (JWKS) and HS256 (secret).

Copy link

coderabbitai bot commented Aug 29, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary of Edits
JWT core & API
src/index.ts
Added remote JWKS support and prioritized asymmetric verification via jwks. Introduced jwtDecoration exposing verify and sign. Reworked public types: BaseJWTOption, discriminated JWTOption (secret vs jwks), extended iat typing. Added imports (importJWK, decodeProtectedHeader, JWTVerifyGetKey), algorithm detection, asymmetric/symmetric alg lists, signing key derivation, and stricter error paths (require secret or jwks; HS with jwks-only errors).
Tests
test/index.test.ts
Updated imports (JOSE helpers, inferBodyReference), changed route calls to jwt.sign!, updated error messages (“set on jwt”), and added a JWKS integration test (generates RSA key pair, creates local JWKS, verifies RS256 via JWKS and HS256 via local secret; uses decodeProtectedHeader, createLocalJWKSet, SignJWT, exportJWK, generateKeyPair).

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I nibbled at keys both near and far,
JWKS like constellations, each a bright star.
HS keeps snug with a secret tight,
RS waves its JWKs in moonlit flight.
I sign and verify, soft-footed, spry—🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jmlow jmlow changed the title feat: Support for remote JWKs feat: support for remote JWKs Aug 29, 2025
@jmlow jmlow changed the title feat: support for remote JWKs feat: support for remote JWKS Aug 29, 2025
@jmlow jmlow marked this pull request as ready for review August 29, 2025 00:33
Copy link

@coderabbitai coderabbitai bot left a 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 when secret is not configured.

If the plugin is used in remote-only mode, key will be undefined and jwt.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.

📥 Commits

Reviewing files that changed from the base of the PR and between c6e97f0 and 71628be.

📒 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.

Copy link

@coderabbitai coderabbitai bot left a 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 to new Date(). With JWTPayloadInput['iat'] being boolean, set to “now” only when true; do nothing if false/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: raw JWK falls through as-is, but SignJWT.sign/jwtVerify require a KeyLike (e.g. Uint8Array/CryptoKey). Either remove JWK from the public secret type or wrap it with importJWK(secret) (from jose) 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 when secret is absent. This breaks the PR goal. Compute key conditionally and only require that at least one of secret or remoteJwks 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)
+		: undefined

Also applies to: 228-233


255-285: Tighten verification: remove any, collapse branches, and guard HS without secret.*

  • Avoid any by destructuring payload.
  • 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 and options?.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 redundant alg spread in protected header.

alg is already part of JWTHeader. 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f1bebfe and 5c54235.

📒 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, and JWTVerifyGetKey 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 exposing sign is correct.

Copy link

@coderabbitai coderabbitai bot left a 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 requested

Current logic sets iat unless explicitly false. Align with the comment: only set when true.

- 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, remove any, 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 const

Please add tests:

  • HS256 verifies with local secret when both secret and remoteJwks are configured.
  • RS256 verifies via remoteJwks.
  • Remote-only config rejects HS256 with a clear error.
🧹 Nitpick comments (4)
src/index.ts (4)

227-233: Avoid any for the decoration object

Give 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 on key

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 functions

Placing secret/remoteJwks in seed 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 error

Keep 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c54235 and 956e3d3.

📒 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 minimal

Adding decodeProtectedHeader, JWTVerifyOptions, and JWTVerifyGetKey aligns with the new flow.

Copy link

@coderabbitai coderabbitai bot left a 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 explicitly false, which changes behavior for callers who omit it. Only set when true.

-            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: Avoid any 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 computed verifyKey and verifyOptions.
  • 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 to secret?: 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 956e3d3 and 480dfd9.

📒 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, and JWTVerifyGetKey is appropriate for the new verification flow.


162-188: Solid factoring of common JWT options.

BaseJWTOption cleanly isolates shared fields and preserves generics for name/schema.


229-235: Good upfront guard for missing key material.

Early fail on neither secret nor remoteJwks reduces footguns.


428-436: Confirm seed semantics with function fields.

Including remoteJwks (a function) in seed 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 dropping remoteJwks from seed.

Copy link

@coderabbitai coderabbitai bot left a 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 and remoteJwks. 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 into options.algorithms, pass [...] to satisfy string[] without as unknown as string[].


238-248: Drop as 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: Use setIssuedAt() 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 storing remoteJwks (a function) in seed 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 480dfd9 and fb9fa87.

📒 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 nor remoteJwks is provided.

Copy link

@coderabbitai coderabbitai bot left a 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 secrets

Avoid 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 control

Allow 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 options

Initialize 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 seed

Seed 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.

📥 Commits

Reviewing files that changed from the base of the PR and between fb9fa87 and 6ac5421.

📒 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 — LGTM

Both are used and appropriate.


162-188: Type-layering with BaseJWTOption is clear

Good separation of common fields and JSDoc.


216-224: Readonly alg allowlists — LGTM

Marked as const; neat separation of symmetric/asymmetric.

Copy link

@coderabbitai coderabbitai bot left a 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 alongside remoteJwks, so the union does not encode remote-only. If remote-only is intended, make secret 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac5421 and 3df6f48.

📒 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 nor remoteJwks 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.

Copy link

@coderabbitai coderabbitai bot left a 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 the remoteVerifyOptions 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. Disallow secret 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3df6f48 and 82333fb.

📒 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 nor remoteJwks 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.

Copy link

@coderabbitai coderabbitai bot left a 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 0f69510 and 53d00d6.

📒 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.

Copy link

@coderabbitai coderabbitai bot left a 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. Expose hasSecret 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 to getKeyForAlg. To avoid repeated importJWK 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 53d00d6 and 13849fd.

📒 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, and JWTVerifyGetKey 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 and iat: false cover both claims.

Copy link

@coderabbitai coderabbitai bot left a 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 is undefined here. Also, remoteOnly tests !key instead of !secret. Select the verify key once (local for HS*, remote for asymmetric) and set safe default algorithms 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 13849fd and a761d46.

📒 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 for JWTOption 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.

Comment on lines +219 to 227
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

Copy link

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.

Copy link

@coderabbitai coderabbitai bot left a 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’s alg 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.

📥 Commits

Reviewing files that changed from the base of the PR and between a761d46 and 3c31c1e.

📒 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 when secret is absent; tests configure a secret.


46-52: Same here: safe use of jwt.sign!.


91-99: Message copy fix LGTM.

“set on jwt” reads correctly.

Comment on lines 301 to 318
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
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix: HS verify fails when secret is a JWK; also restrict default algs and remove duplication.*

  • Bug: key is undefined for JWK secrets, so HS* falls into key! 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.

Comment on lines +201 to +245
// 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)
})
Copy link

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).

Copy link

@coderabbitai coderabbitai bot left a 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* when isSymmetric, 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: guard getKeyForAlg for non-JWK secrets (defensive).

Today it’s only called when secret is a JWK (since key 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 and mergedOptions once based on isSymmetric 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3c31c1e and 1dff1ec.

📒 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)

Comment on lines +452 to 457
{
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!)) )
}
Copy link

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.

Suggested change
{
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.

Copy link

@aminya aminya left a 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants