Skip to content
Open
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
71628be
Add strongly-typed support for remote JWKs that doesn't collide with …
jmlow Aug 29, 2025
f1bebfe
Clean up formatting
jmlow Aug 29, 2025
5c54235
Improve collision handling between 'secret' and 'remoteJwks' for plug…
jmlow Aug 29, 2025
74f44f2
Fix error handling and 'key' assignment with optional 'secret'
jmlow Aug 29, 2025
956e3d3
Add explicit any for 'data' (causes build failure when absent)
jmlow Aug 29, 2025
480dfd9
Implement security railguards for asymmetric encryption; Clean up typing
jmlow Aug 29, 2025
fb9fa87
Improve encryption algorithm handling; Strongly type jwtDecoration
jmlow Aug 29, 2025
6ac5421
Refactor remoteJwks configuration to remoteJwksUrl for cleaner seed; …
jmlow Aug 29, 2025
3df6f48
Revert remoteJwksUrl -> remoteJwks to keep plugin interface simpler f…
jmlow Aug 29, 2025
82333fb
Remove unnecessary 'remoteJwks!' assertion
jmlow Aug 29, 2025
0f69510
Clarify documentation/types for remote verify-only config
jmlow Aug 29, 2025
38b8d28
Fix setIat logic issue
jmlow Aug 29, 2025
53d00d6
Set `iat=true` when missing default or specific config to pass tests …
jmlow Aug 29, 2025
c69cae3
Update test to account for conditional 'sign()' decoration
jmlow Aug 29, 2025
7303181
Allow disabling 'iat' when set to 'false'
jmlow Aug 29, 2025
13849fd
Handle JWK with async 'sign()' and alg-aware key
jmlow Aug 29, 2025
a761d46
Improve 'setIat' checking; Remove sensitive data from checksum
jmlow Aug 29, 2025
ceee3dd
Generalize jwks to support local or remote (but still only asymmetric…
jmlow Aug 29, 2025
3c31c1e
Add test for jwks and secret implementations co-existing in plugin
jmlow Aug 29, 2025
1dff1ec
Revert conditional decorations & throw error in 'sign()' if missing '…
jmlow Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 133 additions & 71 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
import {
SignJWT,
jwtVerify,
decodeProtectedHeader,
type CryptoKey,
type JWK,
type KeyObject,
type JoseHeaderParameters,
type JWTVerifyOptions
type JWTVerifyOptions,
type JWTVerifyGetKey
} from 'jose'

import { Type as t } from '@sinclair/typebox'
Expand Down Expand Up @@ -157,54 +159,85 @@ export interface JWTHeaderParameters extends JoseHeaderParameters {
crit?: string[]
}

export interface JWTOption<
type BaseJWTOption<Name extends string | undefined, Schema extends TSchema | undefined> =
JWTHeaderParameters & JWTPayloadInput & {
/**
* Name to decorate method as
*
* ---
* @example
* For example, `jwt` will decorate Context with `Context.jwt`
*
* ```typescript
* app
* .decorate({
* name: 'myJWTNamespace',
* secret: process.env.JWT_SECRET
* })
* .get('/sign/:name', ({ myJWTNamespace, params }) => {
* return myJWTNamespace.sign(params)
* })
* ```
*/
name?: Name
/**
* Type strict validation for JWT payload
*/
schema?: Schema
}

export type JWTOption<
Name extends string | undefined = 'jwt',
Schema extends TSchema | undefined = undefined
> extends JWTHeaderParameters,
JWTPayloadInput {
/**
* Name to decorate method as
*
* ---
* @example
* For example, `jwt` will decorate Context with `Context.jwt`
*
* ```typescript
* app
* .decorate({
* name: 'myJWTNamespace',
* secret: process.env.JWT_SECRETS
* })
* .get('/sign/:name', ({ myJWTNamespace, params }) => {
* return myJWTNamespace.sign(params)
* })
* ```
*/
name?: Name
/**
* JWT Secret
*/
secret: string | Uint8Array | CryptoKey | JWK | KeyObject
/**
* Type strict validation for JWT payload
*/
schema?: Schema
}
> =
| (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
/**
* Remote JWKS
* Use jose's `createRemoteJWKSet(new URL(...))` to create the JWKS function
*/
remoteJwks: JWTVerifyGetKey
})

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.

This is unused. Should be removed


Comment on lines +224 to 232
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.

export const jwt = <
const Name extends string = 'jwt',
const Schema extends TSchema | undefined = undefined
>({
name = 'jwt' as Name,
secret,
remoteJwks,
schema,
...defaultValues
}: // End JWT Payload
JWTOption<Name, Schema>) => {
if (!secret) throw new Error("Secret can't be empty")
if (!secret && !remoteJwks) throw new Error('Either "secret" or "remoteJwks" must be provided')

const key =
typeof secret === 'string' ? new TextEncoder().encode(secret) : secret
const key = secret
? (typeof secret === 'string' ? new TextEncoder().encode(secret) : secret)
: undefined

const validator = schema
? getSchemaValidator(
Expand All @@ -228,19 +261,62 @@ JWTOption<Name, Schema>) => {
)
: undefined

return new Elysia({
name: '@elysiajs/jwt',
seed: {
name,
secret,
schema,
...defaultValues
let 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>
} = {
verify: async (
jwt?: string,
options?: JWTVerifyOptions
): Promise<
| (UnwrapSchema<Schema, ClaimType> &
Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>)
| false
> => {
if (!jwt) return false

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
}
const data = payload as UnwrapSchema<Schema, ClaimType> &
Omit<JWTPayloadSpec, keyof UnwrapSchema<Schema, {}>>

if (validator && !validator.Check(data))
throw new ValidationError('JWT', validator, data)

return data
} catch (_) {
return false
}
}
}).decorate(name as Name extends string ? Name : 'jwt', {
sign(
}

if (secret) {
jwtDecoration.sign = (
signValue: Omit<UnwrapSchema<Schema, ClaimType>, NormalizedClaim> &
JWTPayloadInput
) {
) => {
const { nbf, exp, iat, ...data } = signValue

/**
Expand Down Expand Up @@ -359,34 +435,20 @@ JWTOption<Name, Schema>) => {
jwt = jwt.setIssuedAt(new 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 jwt.sign((key as Exclude<typeof key, undefined>) )
}
}

return data
} catch (_) {
return false
}
return new Elysia({
name: '@elysiajs/jwt',
seed: {
name,
secret,
remoteJwks,
schema,
...defaultValues
}
})
}).decorate(name as Name extends string ? Name : 'jwt', jwtDecoration)
}

export default jwt