From 4a0606f774022097bf7de69077fe366280bf4f49 Mon Sep 17 00:00:00 2001 From: tempepe Date: Sun, 5 Jan 2025 18:23:44 +0900 Subject: [PATCH] feat(oidc-auth): optional cookie domain (#919) --- .changeset/ten-trainers-jam.md | 5 ++++ packages/oidc-auth/README.md | 25 ++++++++++---------- packages/oidc-auth/src/index.ts | 24 +++++++++++-------- packages/oidc-auth/test/index.test.ts | 33 +++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 .changeset/ten-trainers-jam.md diff --git a/.changeset/ten-trainers-jam.md b/.changeset/ten-trainers-jam.md new file mode 100644 index 00000000..f45bc33e --- /dev/null +++ b/.changeset/ten-trainers-jam.md @@ -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) diff --git a/packages/oidc-auth/README.md b/packages/oidc-auth/README.md index 16f80783..79676951 100644 --- a/packages/oidc-auth/README.md +++ b/packages/oidc-auth/README.md @@ -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 diff --git a/packages/oidc-auth/src/index.ts b/packages/oidc-auth/src/index.ts index 01401fa0..790cf97d 100644 --- a/packages/oidc-auth/src/index.ts +++ b/packages/oidc-auth/src/index.ts @@ -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) { diff --git a/packages/oidc-auth/test/index.test.ts b/packages/oidc-auth/test/index.test.ts index 7c1e7fa4..9a304f8c 100644 --- a/packages/oidc-auth/test/index.test.ts +++ b/packages/oidc-auth/test/index.test.ts @@ -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 () => {