feat(oidc-auth) Add initOidcAuthMiddleware and avoid mutating environment variables (#980)
* Add setOidcAuthEnv * Avoid test relying on mutated global * Test and docs * Changeset * style * Update type import * Switch to setOidcAuthEnvMiddleware * Update changeset description * nit remove unneeded optional param on getOidcAuthEnv * Rename to initOidcAuthMiddlewarepull/1018/head
parent
e394e64ea5
commit
87be440009
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/oidc-auth': minor
|
||||
---
|
||||
|
||||
Add initOidcAuthMiddleware() and avoid mutating environment variables
|
|
@ -41,9 +41,9 @@ npm i hono @hono/oidc-auth
|
|||
|
||||
## Configuration
|
||||
|
||||
The middleware requires the following environment variables to be set:
|
||||
The middleware requires the following variables to be set as either environment variables or by calling `initOidcAuthMiddleware`:
|
||||
|
||||
| Environment Variable | Description | Default Value |
|
||||
| Variable | Description | Default Value |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| OIDC_AUTH_SECRET | The secret key used for signing the session JWT. It is used to verify the JWT in the cookie and prevent tampering. (Must be at least 32 characters long) | None, must be provided |
|
||||
| OIDC_AUTH_REFRESH_INTERVAL | The interval (in seconds) at which the session should be implicitly refreshed. | 15 \* 60 (15 minutes) |
|
||||
|
@ -140,6 +140,26 @@ Note:
|
|||
If explicit logout is not required, the logout handler can be omitted.
|
||||
If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
|
||||
|
||||
## Programmatically configure auth variables
|
||||
|
||||
```typescript
|
||||
// Before other oidc-auth APIs are used
|
||||
app.use(initOidcAuthMiddleware(config));
|
||||
```
|
||||
|
||||
Or to leverage context, use the [`Context access inside Middleware arguments`](https://hono.dev/docs/guides/middleware#context-access-inside-middleware-arguments) pattern.
|
||||
|
||||
```typescript
|
||||
// Before other oidc-auth APIs are used
|
||||
app.use(async (c, next) => {
|
||||
const config = {
|
||||
// Create config using context
|
||||
}
|
||||
const middleware = initOidcAuthMiddleware(config)
|
||||
return middleware(c, next)
|
||||
})
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Yoshio HANAWA <https://github.com/hnw>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Hono } from 'hono'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import type * as oauth2 from 'oauth4webapi'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const MOCK_ISSUER = 'https://accounts.google.com'
|
||||
|
@ -156,7 +157,7 @@ vi.mock(import('oauth4webapi'), async (importOriginal) => {
|
|||
}
|
||||
})
|
||||
|
||||
const { oidcAuthMiddleware, getAuth, processOAuthCallback, revokeSession } = await import('../src')
|
||||
const { oidcAuthMiddleware, getAuth, processOAuthCallback, revokeSession, initOidcAuthMiddleware, getClient } = await import('../src')
|
||||
|
||||
const app = new Hono()
|
||||
app.get('/logout', async (c) => {
|
||||
|
@ -482,6 +483,7 @@ describe('processOAuthCallback()', () => {
|
|||
})
|
||||
test('Should respond with custom cookie name', async () => {
|
||||
const MOCK_COOKIE_NAME = (process.env.OIDC_COOKIE_NAME = 'custom-auth-cookie')
|
||||
const defaultOidcAuthCookiePath = '/'
|
||||
const req = new Request(`${MOCK_REDIRECT_URI}?code=1234&state=${MOCK_STATE}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
@ -493,7 +495,7 @@ describe('processOAuthCallback()', () => {
|
|||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(
|
||||
`${MOCK_COOKIE_NAME}=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`
|
||||
`${MOCK_COOKIE_NAME}=[^;]+; Path=${defaultOidcAuthCookiePath}; HttpOnly; Secure`
|
||||
)
|
||||
)
|
||||
})
|
||||
|
@ -558,3 +560,39 @@ describe('RevokeSession()', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
describe('initOidcAuthMiddleware()', () => {
|
||||
test('Should error if not called first in context', async () => {
|
||||
const app = new Hono()
|
||||
app.use('/*', oidcAuthMiddleware())
|
||||
app.use(initOidcAuthMiddleware({}))
|
||||
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(500)
|
||||
})
|
||||
test('Should prefer programmatically configured varaiables', async () => {
|
||||
let client: oauth2.Client | undefined
|
||||
const CUSTOM_OIDC_CLIENT_ID = 'custom-client-id'
|
||||
const CUSTOM_OIDC_CLIENT_SECRET = 'custom-client-secret'
|
||||
const app = new Hono()
|
||||
app.use(initOidcAuthMiddleware({
|
||||
OIDC_CLIENT_ID: CUSTOM_OIDC_CLIENT_ID,
|
||||
OIDC_CLIENT_SECRET: CUSTOM_OIDC_CLIENT_SECRET
|
||||
}))
|
||||
app.use(async (c) => {
|
||||
client = getClient(c)
|
||||
return c.text('finished')
|
||||
})
|
||||
const req = new Request('http://localhost/', {
|
||||
method: 'GET'
|
||||
})
|
||||
const res = await app.request(req, {}, {})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(client?.client_id).toBe(CUSTOM_OIDC_CLIENT_ID)
|
||||
expect(client?.client_secret).toBe(CUSTOM_OIDC_CLIENT_SECRET)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -46,7 +46,7 @@ export type OidcAuth = {
|
|||
ssnexp: number // session expiration time; if it's expired, revoke session and redirect to IdP
|
||||
} & OidcAuthClaims
|
||||
|
||||
type OidcAuthEnv = {
|
||||
export type OidcAuthEnv = {
|
||||
OIDC_AUTH_SECRET: string
|
||||
OIDC_AUTH_REFRESH_INTERVAL?: string
|
||||
OIDC_AUTH_EXPIRES?: string
|
||||
|
@ -61,12 +61,39 @@ type OidcAuthEnv = {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variables for OIDC-auth middleware.
|
||||
* Configure the OIDC variables programmatically.
|
||||
* If used, should be called before any other OIDC middleware or functions for the Hono context.
|
||||
* Unconfigured values will fallback to environment variables.
|
||||
*/
|
||||
const getOidcAuthEnv = (c: Context) => {
|
||||
let oidcAuthEnv = c.get('oidcAuthEnv')
|
||||
if (oidcAuthEnv === undefined) {
|
||||
oidcAuthEnv = env<OidcAuthEnv>(c)
|
||||
export const initOidcAuthMiddleware = (config: Partial<OidcAuthEnv>) => {
|
||||
return createMiddleware(async (c, next) => {
|
||||
setOidcAuthEnv(c, config)
|
||||
await next()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the OIDC variables.
|
||||
*/
|
||||
const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
|
||||
const currentOidcAuthEnv = c.get('oidcAuthEnv')
|
||||
if (currentOidcAuthEnv !== undefined) {
|
||||
throw new HTTPException(500, { message: 'OIDC Auth env is already configured' })
|
||||
}
|
||||
const ev = env<Readonly<OidcAuthEnv>>(c)
|
||||
const oidcAuthEnv = {
|
||||
OIDC_AUTH_SECRET: config?.OIDC_AUTH_SECRET ?? ev.OIDC_AUTH_SECRET,
|
||||
OIDC_AUTH_REFRESH_INTERVAL: config?.OIDC_AUTH_REFRESH_INTERVAL ?? ev.OIDC_AUTH_REFRESH_INTERVAL,
|
||||
OIDC_AUTH_EXPIRES: config?.OIDC_AUTH_EXPIRES ?? ev.OIDC_AUTH_EXPIRES,
|
||||
OIDC_ISSUER: config?.OIDC_ISSUER ?? ev.OIDC_ISSUER,
|
||||
OIDC_CLIENT_ID: config?.OIDC_CLIENT_ID ?? ev.OIDC_CLIENT_ID,
|
||||
OIDC_CLIENT_SECRET: config?.OIDC_CLIENT_SECRET ?? ev.OIDC_CLIENT_SECRET,
|
||||
OIDC_REDIRECT_URI: config?.OIDC_REDIRECT_URI ?? ev.OIDC_REDIRECT_URI,
|
||||
OIDC_SCOPES: config?.OIDC_SCOPES ?? ev.OIDC_SCOPES,
|
||||
OIDC_COOKIE_PATH: config?.OIDC_COOKIE_PATH ?? ev.OIDC_COOKIE_PATH,
|
||||
OIDC_COOKIE_NAME: config?.OIDC_COOKIE_NAME ?? ev.OIDC_COOKIE_NAME,
|
||||
OIDC_COOKIE_DOMAIN: config?.OIDC_COOKIE_DOMAIN ?? ev.OIDC_COOKIE_DOMAIN,
|
||||
}
|
||||
if (oidcAuthEnv.OIDC_AUTH_SECRET === undefined) {
|
||||
throw new HTTPException(500, { message: 'Session secret is not provided' })
|
||||
}
|
||||
|
@ -102,6 +129,16 @@ const getOidcAuthEnv = (c: Context) => {
|
|||
oidcAuthEnv.OIDC_SCOPES = oidcAuthEnv.OIDC_SCOPES ?? ''
|
||||
c.set('oidcAuthEnv', oidcAuthEnv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variables for OIDC-auth middleware.
|
||||
*/
|
||||
const getOidcAuthEnv = (c: Context) => {
|
||||
let oidcAuthEnv = c.get('oidcAuthEnv')
|
||||
if (oidcAuthEnv === undefined) {
|
||||
setOidcAuthEnv(c)
|
||||
oidcAuthEnv = c.get('oidcAuthEnv')
|
||||
}
|
||||
return oidcAuthEnv as Required<OidcAuthEnv>
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue