Skip to content

Commit

Permalink
feat(oidc-auth): optional cookie domain (#919)
Browse files Browse the repository at this point in the history
  • Loading branch information
maemaemae3 authored Jan 5, 2025
1 parent 8a2d465 commit 4a0606f
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-trainers-jam.md
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 13 additions & 12 deletions packages/oidc-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 15 additions & 9 deletions packages/oidc-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type OidcAuthEnv = {
OIDC_SCOPES?: string
OIDC_COOKIE_PATH?: string
OIDC_COOKIE_NAME?: string
OIDC_COOKIE_DOMAIN?: string
}

/**
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down
33 changes: 33 additions & 0 deletions packages/oidc-auth/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down

0 comments on commit 4a0606f

Please sign in to comment.