Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hono/jwk): JWK Auth Middleware #3826

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e1b65a3
Update cookie.ts
Beyondo Dec 22, 2024
3f0e470
Integrated `priority` option into setCookie serialization tests
Beyondo Dec 22, 2024
f90407c
Merge branch 'honojs:main' into main
Beyondo Jan 2, 2025
c16eb91
Add kid' to TokenHeader, fix Jwt.sign ignoring privateKey.alg with ke…
Beyondo Jan 3, 2025
82ba1da
Add ./src/middleware/jwk/jwk.ts to jsr.json
Beyondo Jan 3, 2025
298f5f0
Add hono/jwk to exports
Beyondo Jan 3, 2025
b2e5b53
feat(hono/jwk)
Beyondo Jan 3, 2025
9453a9d
(feat/Jwt.verifyFromJwks) / batteries included util
Beyondo Jan 12, 2025
df0d32a
Update index.ts
Beyondo Jan 12, 2025
e809fae
add JwtHeaderRequiresKid exception
Beyondo Jan 12, 2025
be92784
using Jwt.verifyFromJwks now
Beyondo Jan 12, 2025
69fe514
Merge branch 'honojs:main' into main
Beyondo Jan 13, 2025
5082072
improved jsdoc and formatting
Beyondo Jan 13, 2025
47f9d46
jsdoc update
Beyondo Jan 13, 2025
28a7b97
formatting
Beyondo Jan 13, 2025
abbf23c
testing jwk's `keys` receiving an async function
Beyondo Jan 13, 2025
763f1fd
removed redundancy
Beyondo Jan 13, 2025
60f10be
add 'Should authorize Keys function' test
Beyondo Jan 13, 2025
c3d054c
added jwks_uri test + improved test descriptions
Beyondo Jan 13, 2025
485b3dd
test naming consistency
Beyondo Jan 13, 2025
d6ec5ef
explicit return fix + moving global declaration merging to own interface
Beyondo Jan 13, 2025
ff2e8ac
cleaner jsdoc @example
Beyondo Jan 14, 2025
bb1cad8
removed commented-out tests unnecessarily inflating changes
Beyondo Jan 14, 2025
582334c
ExtendedJsonWebKey -> HonoJsonWebKey
Beyondo Jan 15, 2025
e26aece
Refactor test to use msw per @yusukebe's suggestion
Beyondo Jan 28, 2025
4f99220
removed stray log + added minor validation w/ @Code-Hex
Beyondo Jan 29, 2025
be0267c
Update index.ts
Beyondo Jan 29, 2025
dacce55
add more test coverage
Beyondo Jan 31, 2025
c7fcfb2
more test coverage
Beyondo Jan 31, 2025
b357e53
lint & format
Beyondo Jan 31, 2025
cea4150
typo
Beyondo Jan 31, 2025
7c4faf1
100/100 test coverage
Beyondo Feb 2, 2025
dbc6032
final touch
Beyondo Feb 2, 2025
c9919a0
added eslint-disable + type export
Beyondo Feb 2, 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
1 change: 1 addition & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"./jsx/dom/css": "./src/jsx/dom/css.ts",
"./jsx/dom/server": "./src/jsx/dom/server.ts",
"./jwt": "./src/middleware/jwt/jwt.ts",
"./jwk": "./src/middleware/jwk/jwk.ts",
"./timeout": "./src/middleware/timeout/index.ts",
"./timing": "./src/middleware/timing/timing.ts",
"./logger": "./src/middleware/logger/index.ts",
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@
"import": "./dist/middleware/jwt/index.js",
"require": "./dist/cjs/middleware/jwt/index.js"
},
"./jwk": {
"types": "./dist/types/middleware/jwk/index.d.ts",
"import": "./dist/middleware/jwk/index.js",
"require": "./dist/cjs/middleware/jwk/index.js"
},
"./timeout": {
"types": "./dist/types/middleware/timeout/index.d.ts",
"import": "./dist/middleware/timeout/index.js",
Expand Down
642 changes: 642 additions & 0 deletions src/middleware/jwk/index.test.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/middleware/jwk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { jwk } from './jwk'
153 changes: 153 additions & 0 deletions src/middleware/jwk/jwk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @module
* JWK Auth Middleware for Hono.
*/

import type { Context } from '../../context'
import { getCookie, getSignedCookie } from '../../helper/cookie'
import { HTTPException } from '../../http-exception'

Check warning on line 8 in src/middleware/jwk/jwk.ts

View check run for this annotation

Codecov / codecov/patch

src/middleware/jwk/jwk.ts#L7-L8

Added lines #L7 - L8 were not covered by tests
import type { MiddlewareHandler } from '../../types'
import type { CookiePrefixOptions } from '../../utils/cookie'
import { Jwt } from '../../utils/jwt'
import '../../context'

Check warning on line 12 in src/middleware/jwk/jwk.ts

View check run for this annotation

Codecov / codecov/patch

src/middleware/jwk/jwk.ts#L11-L12

Added lines #L11 - L12 were not covered by tests
import type { HonoJsonWebKey } from '../../utils/jwt/jws'

/**
* JWK Auth Middleware for Hono.
*
* @see {@link https://hono.dev/docs/middleware/builtin/jwk}
*
* @param {object} options - The options for the JWK middleware.
* @param {HonoJsonWebKey[] | (() => Promise<HonoJsonWebKey[]>)} [options.keys] - The values of your public keys, or a function that returns them.
* @param {string} [options.jwks_uri] - If this value is set, attempt to fetch JWKs from this URI, expecting a JSON response with `keys` which are added to the provided options.keys
* @param {string} [options.cookie] - If this value is set, then the value is retrieved from the cookie header using that value as a key, which is then validated as a token.
* @param {RequestInit} [init] - Optional initialization options for the `fetch` request when retrieving JWKS from a URI.
* @returns {MiddlewareHandler} The middleware handler function.
*
* @example
* ```ts
* const app = new Hono()
*
* app.use("/auth/*", jwk({ jwks_uri: "https://example-backend.hono.dev/.well-known/jwks.json" }))
*
* app.get('/auth/page', (c) => {
* return c.text('You are authorized')
* })
* ```
*/

export const jwk = (
options: {
keys?: HonoJsonWebKey[] | (() => Promise<HonoJsonWebKey[]>)
jwks_uri?: string
cookie?:
| string
| { key: string; secret?: string | BufferSource; prefixOptions?: CookiePrefixOptions }
},
init?: RequestInit
): MiddlewareHandler => {
if (!options || !(options.keys || options.jwks_uri)) {
throw new Error('JWK auth middleware requires options for either "keys" or "jwks_uri" or both')
}

if (!crypto.subtle || !crypto.subtle.importKey) {
throw new Error('`crypto.subtle.importKey` is undefined. JWK auth middleware requires it.')
}

return async function jwk(ctx, next) {
const credentials = ctx.req.raw.headers.get('Authorization')
let token
if (credentials) {
const parts = credentials.split(/\s+/)
if (parts.length !== 2) {
const errDescription = 'invalid credentials structure'
throw new HTTPException(401, {
message: errDescription,
res: unauthorizedResponse({
ctx,
error: 'invalid_request',
errDescription,
}),
})
} else {
token = parts[1]
}
} else if (options.cookie) {
if (typeof options.cookie == 'string') {
token = getCookie(ctx, options.cookie)
} else if (options.cookie.secret) {
if (options.cookie.prefixOptions) {
token = await getSignedCookie(
ctx,
options.cookie.secret,
options.cookie.key,
options.cookie.prefixOptions
)
} else {
token = await getSignedCookie(ctx, options.cookie.secret, options.cookie.key)
}
} else {
if (options.cookie.prefixOptions) {
token = getCookie(ctx, options.cookie.key, options.cookie.prefixOptions)
} else {
token = getCookie(ctx, options.cookie.key)
}
}
}

if (!token) {
const errDescription = 'no authorization included in request'
throw new HTTPException(401, {
message: errDescription,
res: unauthorizedResponse({
ctx,
error: 'invalid_request',
errDescription,
}),
})
}

let payload
let cause
try {
payload = await Jwt.verifyFromJwks(token, options, init)
} catch (e) {
cause = e
}

if (!payload) {
if (cause instanceof Error && cause.constructor === Error) {
throw cause
}
throw new HTTPException(401, {
message: 'Unauthorized',
res: unauthorizedResponse({
ctx,
error: 'invalid_token',
statusText: 'Unauthorized',
errDescription: 'token verification failure',
}),
cause,
})
}

ctx.set('jwtPayload', payload)

await next()
}
}

function unauthorizedResponse(opts: {
ctx: Context
error: string
errDescription: string
statusText?: string
}) {
return new Response('Unauthorized', {
status: 401,
statusText: opts.statusText,
headers: {
'WWW-Authenticate': `Bearer realm="${opts.ctx.req.url}",error="${opts.error}",error_description="${opts.errDescription}"`,
},
})
}
48 changes: 48 additions & 0 deletions src/middleware/jwk/keys.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"public_keys": [
{
"kid": "hono-test-kid-1",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"e": "AQAB",
"n": "2XGQh8VC_p8gRqfBLY0E3RycnfBl5g1mKyeiyRSPjdaR7fmNPuC3mHjVWXtyXWSvAuRYPYfL_pSi6erpxVv7NuPJbKaZ-I1MwdRPdG2qHu9mNYxniws73gvF3tUN9eSsQUIBL0sYEOnVMjniDcOxIr3Rgz_RxdLB_FxTDXYhzzG49L79wGV1udILGHq0lqlMtmUX6LRtbaoRt1fJB4rTCkYeQp9r5HYP79PKTR43vLIq0aZryI4CyBkPG_0vGEvnzasGdp-qE9Ywt_J2anQKt3nvVVR4Yhs2EIoPQkYoDnVySjeuRsUA5JQYKThrM4sFZSQsO82dHTvwKo2z2x6ZMw"
},
{
"kid": "hono-test-kid-2",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"e": "AQAB",
"n": "uRVR5DkH22a_FM4RtqvnVxd6QAjdfj8oFYPaxIux7K8oTaBy5YagxTWN0qeKI5lI3nL20cx72XxD_UF4TETCFgfD-XB48cdjnSQlOXXbRPXUX0Rdte48naAt4przAb7ydUxrfvDlbSZe02Du-ZGRzEB6RW6KLFWUvTadI4w33qb2i8hauQuTcRmaIUESt8oytUGS44dXAw3Nqt_NL-e7TRgX5o1u_31Uvet1ofsv6Mx8vxJ6zMdM_AKvzLt2iuoK_8vL4R86CjD3dpal2BwO7RkRl2Wcuf5jxjM4pruJ2RBCpzBieEvSIH8kKHIm9SfTzTDJqRhoXd7KM5jL1GNzyw"
}
],
"private_keys": [
{
"kid": "hono-test-kid-1",
"alg": "RS256",
"d": "A5CR2gGPegHwOYUbUzylZvdgUFNWMetOUK7M3TClGdVgSkWpELrTLhpTa3m50KYlG446x03baxUGU4D_MoKx7GukX0-fGCzY17FvWNOwOLACcPMYT3ZwfAQ2_jkBimJxU7CNUtH18KQ-U1B3nQ1apHZc-1Xa6CKIY5nv32yfj6uTrERRLOs7Fn9xpOE4uMHEf-l1ppIEIqK5QkEoPRMCUBABsGBSfiJP2hQVa-R-nezX3kVSxKTxAjDEOkquzb-CKlJW7xN2xQ7p40Wi7lDWZkOapBNGr59Z4gcFfo6f8XpQrqoFjDfsGsdH5q9MH_3lEEtD14wymXNnCoRHNr_mwQ",
"dp": "WMq_BNbd3At-J9VzXgE-aLvPhztS1W8K9xlghITpwAyzhEfCp9mO7IOEVtNWKoEtVFEaZrWKuNWKd-dnzjvydltCkpJ7QhTmiFNFsEzKNJdGQ1Tfsj9658csbVLUOhI4oVcN6kiCa6OdH41Z_JMyN75cTgd4z5h_FRYRRgjoUEU",
"dq": "Lz9vM7L-aEsPJOM5K2PqInLP9HNwDl943S79d_aw6w-JnHPFcu95no6-6nRcd87eSWoTvHZeFgsle4oiV0UpAocEO7xraCBa_Z9o-jGbBfynOLyXMH2l70yWBdCGCzgc_Wg2sKJwiYYXXfGJ3CzSeIRet82Rn54Q9mMlB6Ie8LE",
"e": "AQAB",
"kty": "RSA",
"n": "2XGQh8VC_p8gRqfBLY0E3RycnfBl5g1mKyeiyRSPjdaR7fmNPuC3mHjVWXtyXWSvAuRYPYfL_pSi6erpxVv7NuPJbKaZ-I1MwdRPdG2qHu9mNYxniws73gvF3tUN9eSsQUIBL0sYEOnVMjniDcOxIr3Rgz_RxdLB_FxTDXYhzzG49L79wGV1udILGHq0lqlMtmUX6LRtbaoRt1fJB4rTCkYeQp9r5HYP79PKTR43vLIq0aZryI4CyBkPG_0vGEvnzasGdp-qE9Ywt_J2anQKt3nvVVR4Yhs2EIoPQkYoDnVySjeuRsUA5JQYKThrM4sFZSQsO82dHTvwKo2z2x6ZMw",
"p": "7K-X3xMf3xxdlHTRs17x4WkbFUq4ZCU9L1al88UW2tpoF8ZDLUvaKXeF0vkosKvYUsiHsV1fbGVo6Oy75iII-op-t6-tP3R61nkjaytyJ8p32nbxBI1UWpFxZYNxG_Od07kau3LwkgDh8Ogr6zqmq8-lKoBPio-4K7PY5FiyWzs",
"q": "6y__IKt1n1pTc-S9l1WfSuC96jX8iQhEsGSxnshyNZi59mH1AigkrAw9T5b7OFX7ulHXwuithsVi8cxkq2inNmemxD3koiiU-sv6vg6lRCoZsXFHiUCP-2HoK17sR1zUb6HQpp5MEHY8qoC3Mi3IpkNC7gAbAukbMQo3WlIGqmk",
"qi": "flgM56Nw2hzHHy0Lz8ewBtOkkzfq1r_n6SmSZdU0zWlEp1lLovpHmuwyVeXpQlLJUHqcNVRw0NlwV7EN0rPd4rG3hcMdogj_Jl-r52TYzx4kVpbMEIh4xKs5rFzxbb96A3F9Ox-muRWvfOUCpXxGXCCGqHRmjRUolxDxsiPznuk"
},
{
"kid": "hono-test-kid-2",
"alg": "RS256",
"d": "JCIL50TVClnQQyUJ40JDO0b7mGXCrCNzVWP1ATsOhNkbQrBozfOPDoEqi24m81U5GyiRlBraMPboJRizfhxMUdW5RkjVa8pT4blNRR8DrD5b9C9aJir5DYLYgm1itLwNBKZjNBieicUcbSL29KUdNCWAWW6_rfEVRS1U1zxIKgDUPVd6d7jiIwAKuKvGlMc11RGRZj5eKSNMQyLU5u8Qs_VQuoBRNAyWLZZcHMlAWbh3er7m0jkmUDRdVU0y_n1UAGsr9cAxPwf2HtS5j5R2ahEodatsJynnafYtj6jbOR6jvO3N2Vf-NJ7jVY2-kfv1rJd86KAxD-tIAGx2w1VRTQ",
"dp": "wQhiWfdvVxk7ERmYj7Fn04wqjP7o7-72bn3SznGyBSkvpkg1WX4j467vpRtXVn4qxSSMXCj2UMKCrovba2RWHp1cnkvT-TFTbONkBuhOBpbx3TVwgGd-IfDJVa_i89XjiYgtEApHz173kRodEENXxcOj_mbOGyBb9Yl2M45A-tU",
"dq": "ERdP5mdziJ46OsDHTdZ4hOX2ti0EljtVqGo1B4WKXey6DMH0JGHGU_3fFiF4Gomhy3nyGUI7Qhk3kf7lixAtSsk1lWAAeQLPt1r8yZkD5odLKXLyua_yZJ041d3O3wxRYXl3OvzoVy6rPhzRPIaxevNp-Pp5ZNoKfonQPz3bDGc",
"e": "AQAB",
"kty": "RSA",
"n": "uRVR5DkH22a_FM4RtqvnVxd6QAjdfj8oFYPaxIux7K8oTaBy5YagxTWN0qeKI5lI3nL20cx72XxD_UF4TETCFgfD-XB48cdjnSQlOXXbRPXUX0Rdte48naAt4przAb7ydUxrfvDlbSZe02Du-ZGRzEB6RW6KLFWUvTadI4w33qb2i8hauQuTcRmaIUESt8oytUGS44dXAw3Nqt_NL-e7TRgX5o1u_31Uvet1ofsv6Mx8vxJ6zMdM_AKvzLt2iuoK_8vL4R86CjD3dpal2BwO7RkRl2Wcuf5jxjM4pruJ2RBCpzBieEvSIH8kKHIm9SfTzTDJqRhoXd7KM5jL1GNzyw",
"p": "7cY_nFnn4w5pVi7wq_S9FJHIGsxCwogXqSSC_d7yWopbI2rW3Ugx21IMcWT2pnpsF_VYQx5FnNFviFufNOloREOguqci4lBinAilYBf3VXaN_YrxSk4flJmykwm_HBbXpHt_L3t4HBf-uuY-klJxFkeTbBErjxMS0U0EheEpDYU",
"q": "x0UidqgkzWPqXa7vZ5noYTY5e3TDQZ_l8A26lFDKAbB62lXvnp_MhnQYDAx9VgUGYYrXv7UmaH-ZCSzuMM9Uhuw0lXRyojF-TLowNjASMlWbkJsJus3zi_AI4pAKyYnhNADxZrT1kxseI8zHiq0_bQa8qLaleXBTdkpc3Z6M1Q8",
"qi": "x5VJcfnlX9ZhH6eMKx27rOGQrPjQ4BjZgmND7rrX-CSrE0M0RG4KuC4ZOu5XpQ-YsOC_bIzolBN2cHGn4ttPXeUc3y5bnqJYo7FxMdGn4gPRbXlVjCrE54JH_cdkl8cDqcaybjme1-ilNu-vHJWgHPdpbOguhRpicARkptAkOe0"
}
]
}
1 change: 0 additions & 1 deletion src/middleware/request-id/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { RequestIdVariables } from './request-id'
export type { RequestIdVariables }
export { requestId } from './request-id'
import type {} from '../..'

declare module '../..' {
interface ContextVariableMap extends RequestIdVariables {}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/jwt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
* JWT utility.
*/

import { decode, sign, verify } from './jwt'
export const Jwt = { sign, verify, decode }
import { decode, sign, verify, verifyFromJwks } from './jwt'
export const Jwt = { sign, verify, decode, verifyFromJwks }
8 changes: 7 additions & 1 deletion src/utils/jwt/jws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ type KeyAlgorithm =
| (EcdsaParams & EcKeyImportParams)
| HmacImportParams

export type SignatureKey = string | JsonWebKey | CryptoKey
// Extending the JsonWebKey interface to include the "kid" property.
// https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
export interface HonoJsonWebKey extends JsonWebKey {
Beyondo marked this conversation as resolved.
Show resolved Hide resolved
kid?: string
}

export type SignatureKey = string | HonoJsonWebKey | CryptoKey

export async function signing(
privateKey: SignatureKey,
Expand Down
69 changes: 67 additions & 2 deletions src/utils/jwt/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import { AlgorithmTypes } from './jwa'
import type { SignatureAlgorithm } from './jwa'
import { signing, verifying } from './jws'
import type { SignatureKey } from './jws'
import type { HonoJsonWebKey, SignatureKey } from './jws'
import {
JwtHeaderInvalid,
JwtHeaderRequiresKid,
JwtTokenExpired,
JwtTokenInvalid,
JwtTokenIssuedAt,
Expand All @@ -30,6 +31,7 @@
export interface TokenHeader {
alg: SignatureAlgorithm
typ?: 'JWT'
kid?: string
}

export function isTokenHeader(obj: unknown): obj is TokenHeader {
Expand All @@ -50,7 +52,13 @@
alg: SignatureAlgorithm = 'HS256'
): Promise<string> => {
const encodedPayload = encodeJwtPart(payload)
const encodedHeader = encodeJwtPart({ alg, typ: 'JWT' } satisfies TokenHeader)
let encodedHeader
if (typeof privateKey === 'object' && 'alg' in privateKey) {
alg = privateKey.alg as SignatureAlgorithm
encodedHeader = encodeJwtPart({ alg, typ: 'JWT', kid: privateKey.kid })
} else {
encodedHeader = encodeJwtPart({ alg, typ: 'JWT' })
}

const partialToken = `${encodedHeader}.${encodedPayload}`

Expand Down Expand Up @@ -99,6 +107,54 @@
return payload
}

export const verifyFromJwks = async (
token: string,
options: {
keys?: HonoJsonWebKey[] | (() => Promise<HonoJsonWebKey[]>)
jwks_uri?: string
},

Check warning on line 115 in src/utils/jwt/jwt.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/jwt.ts#L113-L115

Added lines #L113 - L115 were not covered by tests
init?: RequestInit
): Promise<JWTPayload> => {
const header = decodeHeader(token)

Check warning on line 119 in src/utils/jwt/jwt.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/jwt.ts#L119

Added line #L119 was not covered by tests
if (!isTokenHeader(header)) {
throw new JwtHeaderInvalid(header)
}
if (!header.kid) {
throw new JwtHeaderRequiresKid(header)
}

Check warning on line 126 in src/utils/jwt/jwt.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/jwt.ts#L126

Added line #L126 was not covered by tests
let keys = typeof options.keys === 'function' ? await options.keys() : options.keys

Check warning on line 128 in src/utils/jwt/jwt.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/jwt.ts#L128

Added line #L128 was not covered by tests
if (options.jwks_uri) {
const response = await fetch(options.jwks_uri, init)
if (!response.ok) {
throw new Error(`failed to fetch JWKS from ${options.jwks_uri}`)
}
const data = (await response.json()) as { keys?: JsonWebKey[] }
if (!data.keys) {
throw new Error('invalid JWKS response. "keys" field is missing')
}
if (!Array.isArray(data.keys)) {
throw new Error('invalid JWKS response. "keys" field is not an array')
}
if (keys) {
keys.push(...data.keys)
} else {
keys = data.keys
}
} else if (!keys) {
throw new Error('verifyFromJwks requires options for either "keys" or "jwks_uri" or both')
}

Check warning on line 149 in src/utils/jwt/jwt.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/jwt.ts#L149

Added line #L149 was not covered by tests
const matchingKey = keys.find((key) => key.kid === header.kid)
if (!matchingKey) {
throw new JwtTokenInvalid(token)
}

Check warning on line 154 in src/utils/jwt/jwt.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/jwt.ts#L154

Added line #L154 was not covered by tests
return await verify(token, matchingKey, matchingKey.alg as SignatureAlgorithm)
}

export const decode = (token: string): { header: TokenHeader; payload: JWTPayload } => {
try {
const [h, p] = token.split('.')
Expand All @@ -112,3 +168,12 @@
throw new JwtTokenInvalid(token)
}
}

export const decodeHeader = (token: string): TokenHeader => {
try {
const [h] = token.split('.')
return decodeJwtPart(h) as TokenHeader
} catch {
throw new JwtTokenInvalid(token)
}
}
9 changes: 9 additions & 0 deletions src/utils/jwt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
}
}

export class JwtHeaderRequiresKid extends Error {
constructor(header: object) {
super(`required "kid" in jwt header: ${JSON.stringify(header)}`)

Check warning on line 50 in src/utils/jwt/types.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/types.ts#L49-L50

Added lines #L49 - L50 were not covered by tests
this.name = 'JwtHeaderRequiresKid'
}

Check warning on line 52 in src/utils/jwt/types.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/jwt/types.ts#L52

Added line #L52 was not covered by tests
}

export class JwtTokenSignatureMismatched extends Error {
constructor(token: string) {
super(`token(${token}) signature mismatched`)
Expand Down Expand Up @@ -81,3 +88,5 @@
*/
iat?: number
}

export type { HonoJsonWebKey } from './jws'
Loading