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(oidc-auth): optional cookie domain #919

Merged
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
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
Loading