feat(oidc-auth): optional cookie domain (#919)

pull/925/head
tempepe 2025-01-05 18:23:44 +09:00 committed by GitHub
parent 8a2d4651b0
commit 4a0606f774
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 21 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/oidc-auth': minor
---
Optionally specify a custom cookie domain using the OIDC_COOKIE_DOMAIN environment variable (default is domain of the request)

View File

@ -43,18 +43,19 @@ npm i hono @hono/oidc-auth
The middleware requires the following environment variables to be set:
| Environment 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) |
| OIDC_AUTH_EXPIRES | The interval (in seconds) after which the session should be considered expired. Once expired, the user will be redirected to the IdP for re-authentication. | 60 _ 60 _ 24 (1 day) |
| OIDC_ISSUER | The issuer URL of the OpenID Connect (OIDC) discovery. This URL is used to retrieve the OIDC provider's configuration. | None, must be provided |
| OIDC_CLIENT_ID | The OAuth 2.0 client ID assigned to your application. This ID is used to identify your application to the OIDC provider. | None, must be provided |
| OIDC_CLIENT_SECRET | The OAuth 2.0 client secret assigned to your application. This secret is used to authenticate your application to the OIDC provider. | None, must be provided |
| OIDC_REDIRECT_URI | The URL to which the OIDC provider should redirect the user after authentication. This URL must be registered as a redirect URI in the OIDC provider. | None, must be provided |
| OIDC_SCOPES | The scopes that should be used for the OIDC authentication | The server provided `scopes_supported` |
| OIDC_COOKIE_PATH | The path to which the `oidc-auth` cookie is set. Restrict to not send it with every request to your domain | / |
| OIDC_COOKIE_NAME | The name of the cookie to be set | `oidc-auth` |
| Environment 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) |
| OIDC_AUTH_EXPIRES | The interval (in seconds) after which the session should be considered expired. Once expired, the user will be redirected to the IdP for re-authentication. | 60 _ 60 _ 24 (1 day) |
| OIDC_ISSUER | The issuer URL of the OpenID Connect (OIDC) discovery. This URL is used to retrieve the OIDC provider's configuration. | None, must be provided |
| OIDC_CLIENT_ID | The OAuth 2.0 client ID assigned to your application. This ID is used to identify your application to the OIDC provider. | None, must be provided |
| OIDC_CLIENT_SECRET | The OAuth 2.0 client secret assigned to your application. This secret is used to authenticate your application to the OIDC provider. | None, must be provided |
| OIDC_REDIRECT_URI | The URL to which the OIDC provider should redirect the user after authentication. This URL must be registered as a redirect URI in the OIDC provider. | None, must be provided |
| OIDC_SCOPES | The scopes that should be used for the OIDC authentication | The server provided `scopes_supported` |
| OIDC_COOKIE_PATH | The path to which the `oidc-auth` cookie is set. Restrict to not send it with every request to your domain | / |
| OIDC_COOKIE_NAME | The name of the cookie to be set | `oidc-auth` |
| 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 |
## How to Use

View File

@ -56,6 +56,7 @@ type OidcAuthEnv = {
OIDC_SCOPES?: string
OIDC_COOKIE_PATH?: string
OIDC_COOKIE_NAME?: string
OIDC_COOKIE_DOMAIN?: string
}
/**
@ -215,11 +216,11 @@ const updateAuth = async (
ssnexp: orig?.ssnexp || Math.floor(Date.now() / 1000) + authExpires,
}
const session_jwt = await sign(updated, env.OIDC_AUTH_SECRET)
setCookie(c, env.OIDC_COOKIE_NAME, session_jwt, {
path: env.OIDC_COOKIE_PATH,
httpOnly: true,
secure: true,
})
const cookieOptions =
env.OIDC_COOKIE_DOMAIN == null
? { path: env.OIDC_COOKIE_PATH, httpOnly: true, secure: true }
: { path: env.OIDC_COOKIE_PATH, domain: env.OIDC_COOKIE_DOMAIN, httpOnly: true, secure: true }
setCookie(c, env.OIDC_COOKIE_NAME, session_jwt, cookieOptions)
c.set('oidcAuthJwt', session_jwt)
return updated
}
@ -392,16 +393,21 @@ export const oidcAuthMiddleware = (): MiddlewareHandler => {
const auth = await getAuth(c)
if (auth === null) {
const path = new URL(env.OIDC_REDIRECT_URI).pathname
const cookieDomain = env.OIDC_COOKIE_DOMAIN
// Redirect to IdP for login
const state = oauth2.generateRandomState()
const nonce = oauth2.generateRandomNonce()
const code_verifier = oauth2.generateRandomCodeVerifier()
const code_challenge = await oauth2.calculatePKCECodeChallenge(code_verifier)
const url = await generateAuthorizationRequestUrl(c, state, nonce, code_challenge)
setCookie(c, 'state', state, { path, httpOnly: true, secure: true })
setCookie(c, 'nonce', nonce, { path, httpOnly: true, secure: true })
setCookie(c, 'code_verifier', code_verifier, { path, httpOnly: true, secure: true })
setCookie(c, 'continue', c.req.url, { path, httpOnly: true, secure: true })
const cookieOptions =
cookieDomain == null
? { path, httpOnly: true, secure: true }
: { path, domain: cookieDomain, httpOnly: true, secure: true }
setCookie(c, 'state', state, cookieOptions)
setCookie(c, 'nonce', nonce, cookieOptions)
setCookie(c, 'code_verifier', code_verifier, cookieOptions)
setCookie(c, 'continue', c.req.url, cookieOptions)
return c.redirect(url)
}
} catch (e) {

View File

@ -187,6 +187,7 @@ beforeEach(() => {
delete process.env.OIDC_SCOPES
delete process.env.OIDC_COOKIE_PATH
delete process.env.OIDC_COOKIE_NAME
delete process.env.OIDC_COOKIE_DOMAIN
})
describe('oidcAuthMiddleware()', () => {
test('Should respond with 200 OK if session is active', async () => {
@ -280,6 +281,38 @@ describe('oidcAuthMiddleware()', () => {
expect(res.status).toBe(302)
expect(res.headers.get('set-cookie')).toMatch(new RegExp('oidc-auth=; Max-Age=0; Path=/($|,)'))
})
test('Should Domain attribute of the cookie not set if env value not defined', async () => {
const req = new Request('http://localhost/', {
method: 'GET',
headers: { cookie: `oidc-auth=${MOCK_JWT_EXPIRED_SESSION}` },
})
const res = await app.request(req, {}, {})
expect(res).not.toBeNull()
expect(res.status).toBe(302)
expect(res.headers.get('set-cookie')).not.toMatch('Domain=')
})
test('Should Domain attribute of the cookie set if env value defined (with renewed refresh token)', async () => {
const MOCK_COOKIE_DOMAIN = (process.env.OIDC_COOKIE_DOMAIN = 'example.com')
const req = new Request('http://localhost/', {
method: 'GET',
headers: { cookie: `oidc-auth=${MOCK_JWT_TOKEN_EXPIRED_SESSION}` },
})
const res = await app.request(req, {}, {})
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(res.headers.get('set-cookie')).toMatch(`Domain=${MOCK_COOKIE_DOMAIN}`)
})
test('Should Domain attribute of the cookie set if env value defined (if session is expired)', async () => {
const MOCK_COOKIE_DOMAIN = (process.env.OIDC_COOKIE_DOMAIN = 'example.com')
const req = new Request('http://localhost/', {
method: 'GET',
headers: { cookie: `oidc-auth=${MOCK_JWT_EXPIRED_SESSION}` },
})
const res = await app.request(req, {}, {})
expect(res).not.toBeNull()
expect(res.status).toBe(302)
expect(res.headers.get('set-cookie')).toMatch(`Domain=${MOCK_COOKIE_DOMAIN}`)
})
})
describe('processOAuthCallback()', () => {
test('Should successfully process the OAuth2.0 callback and redirect to the continue URL', async () => {