Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/large-worlds-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hono/oidc-auth': minor
---

- Optionally specify a custom algorithm for signing and verifying the session JWT using the `OIDC_JWT_ALG` environment variable.
- Fixes issues related to https://github.com/honojs/hono/issues/4625 .
1 change: 1 addition & 0 deletions packages/oidc-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ The middleware requires the following variables to be set as either environment
| OIDC_COOKIE_DOMAIN | The custom domain of the cookie. For example, set this like `example.com` to enable authentication across subdomains (e.g., `a.example.com` and `b.example.com`). | Domain of the request |
| OIDC_AUDIENCE | The audience for the access token | No default, optional. Primarily intended for use with Auth0. [`audience`](https://community.auth0.com/t/what-is-the-audience/71414) is required by Auth0 to receive a non-opaque access token, for other providers you may not need this. |
| OIDC_AUTH_EXTERNAL_URL | The full, public-facing base URL of the application, including the protocol and any path prefixes (e.g., `https://app.example.com` or `https://example.com/myapp`). This is used to construct the correct redirect URL after login when running behind a reverse proxy. | None. The middleware will use the URL from the incoming request. |
| OIDC_JWT_ALG | The algorithm used for signing and verifying the session JWT. | `HS256` |

## How to Use

Expand Down
33 changes: 33 additions & 0 deletions packages/oidc-auth/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ const MOCK_JWT_INVALID_ALGORITHM = jwt.sign(
null,
{ algorithm: 'none', expiresIn: '1h' }
)
const MOCK_JWT_HS384_SESSION = jwt.sign(
{
sub: MOCK_SUBJECT,
email: MOCK_EMAIL,
rtk: 'DUMMY_REFRESH_TOKEN',
rtkexp: Math.floor(Date.now() / 1000) + 10 * 60, // 10 minutes
ssnexp: Math.floor(Date.now() / 1000) + 10 * 60, // 10 minutes
},
MOCK_AUTH_SECRET,
{ algorithm: 'HS384', expiresIn: '1h' }
)
vi.mock(import('oauth4webapi'), async (importOriginal) => {
const original = await importOriginal()

Expand Down Expand Up @@ -198,6 +209,7 @@ beforeEach(() => {
delete process.env.OIDC_COOKIE_DOMAIN
delete process.env.OIDC_AUDIENCE
delete process.env.OIDC_AUTH_EXTERNAL_URL
delete process.env.OIDC_JWT_ALG
})
describe('oidcAuthMiddleware()', () => {
test('Should respond with 200 OK if session is active', async () => {
Expand Down Expand Up @@ -428,6 +440,27 @@ describe('oidcAuthMiddleware()', () => {
expect(res).not.toBeNull()
expect(res.status).toBe(500)
})
test('Should use HS256 as default algorithm when OIDC_JWT_ALG is not set', async () => {
const req = new Request('http://localhost/', {
method: 'GET',
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
})
const res = await app.request(req, {}, {})
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.text()).toBe(`Hello ${MOCK_EMAIL}! Refresh token: DUMMY_REFRESH_TOKEN`)
})
test('Should respond with 200 OK when OIDC_JWT_ALG is HS384 and session uses HS384', async () => {
process.env.OIDC_JWT_ALG = 'HS384'
const req = new Request('http://localhost/', {
method: 'GET',
headers: { cookie: `oidc-auth=${MOCK_JWT_HS384_SESSION}` },
})
const res = await app.request(req, {}, {})
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.text()).toBe(`Hello ${MOCK_EMAIL}! Refresh token: DUMMY_REFRESH_TOKEN`)
})
})
describe('processOAuthCallback()', () => {
test('Should successfully process the OAuth2.0 callback and redirect to the continue URL', async () => {
Expand Down
10 changes: 7 additions & 3 deletions packages/oidc-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'
import { sign, verify } from 'hono/jwt'
import type { SignatureAlgorithm } from 'hono/utils/jwt/jwa'
import * as oauth2 from 'oauth4webapi'

export type IDToken = oauth2.IDToken
Expand Down Expand Up @@ -60,6 +61,7 @@ export type OidcAuthEnv = {
OIDC_COOKIE_DOMAIN?: string
OIDC_AUDIENCE?: string
OIDC_AUTH_EXTERNAL_URL?: string
OIDC_JWT_ALG?: SignatureAlgorithm
}

/**
Expand Down Expand Up @@ -97,6 +99,7 @@ const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
OIDC_COOKIE_DOMAIN: config?.OIDC_COOKIE_DOMAIN ?? ev.OIDC_COOKIE_DOMAIN,
OIDC_AUDIENCE: config?.OIDC_AUDIENCE ?? ev.OIDC_AUDIENCE,
OIDC_AUTH_EXTERNAL_URL: config?.OIDC_AUTH_EXTERNAL_URL ?? ev.OIDC_AUTH_EXTERNAL_URL,
OIDC_JWT_ALG: config?.OIDC_JWT_ALG ?? ev.OIDC_JWT_ALG,
}
if (oidcAuthEnv.OIDC_AUTH_SECRET === undefined) {
throw new HTTPException(500, { message: 'Session secret is not provided' })
Expand Down Expand Up @@ -140,6 +143,7 @@ const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL ?? `${defaultRefreshInterval}`
oidcAuthEnv.OIDC_AUTH_EXPIRES = oidcAuthEnv.OIDC_AUTH_EXPIRES ?? `${defaultExpirationInterval}`
oidcAuthEnv.OIDC_SCOPES = oidcAuthEnv.OIDC_SCOPES ?? ''
oidcAuthEnv.OIDC_JWT_ALG = oidcAuthEnv.OIDC_JWT_ALG ?? 'HS256'
c.set('oidcAuthEnv', oidcAuthEnv)
}

Expand Down Expand Up @@ -201,7 +205,7 @@ export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
return null
}
try {
auth = (await verify(session_jwt, env.OIDC_AUTH_SECRET)) as OidcAuth
auth = (await verify(session_jwt, env.OIDC_AUTH_SECRET, env.OIDC_JWT_ALG)) as OidcAuth
} catch {
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
return null
Expand Down Expand Up @@ -273,7 +277,7 @@ const updateAuth = async (
rtkexp: Math.floor(Date.now() / 1000) + authRefreshInterval,
ssnexp: orig?.ssnexp || Math.floor(Date.now() / 1000) + authExpires,
}
const session_jwt = await sign(updated, env.OIDC_AUTH_SECRET)
const session_jwt = await sign(updated, env.OIDC_AUTH_SECRET, env.OIDC_JWT_ALG)
const cookieOptions =
env.OIDC_COOKIE_DOMAIN == null
? { path: env.OIDC_COOKIE_PATH, httpOnly: true, secure: true }
Expand All @@ -299,7 +303,7 @@ export const revokeSession = async (c: Context): Promise<void> => {
domain: env.OIDC_COOKIE_DOMAIN,
})
}
const auth = (await verify(session_jwt, env.OIDC_AUTH_SECRET)) as OidcAuth
const auth = (await verify(session_jwt, env.OIDC_AUTH_SECRET, env.OIDC_JWT_ALG)) as OidcAuth
if (auth.rtk !== undefined && auth.rtk !== '') {
// revoke refresh token
const as = await getAuthorizationServer(c)
Expand Down
Loading