feat(oidc-auth): optional cookie domain (#919)
parent
8a2d4651b0
commit
4a0606f774
|
@ -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)
|
|
@ -44,7 +44,7 @@ npm i hono @hono/oidc-auth
|
||||||
The middleware requires the following environment variables to be set:
|
The middleware requires the following environment variables to be set:
|
||||||
|
|
||||||
| Environment Variable | Description | Default Value |
|
| 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_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_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_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) |
|
||||||
|
@ -55,6 +55,7 @@ The middleware requires the following environment variables to be set:
|
||||||
| OIDC_SCOPES | The scopes that should be used for the OIDC authentication | The server provided `scopes_supported` |
|
| 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_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_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
|
## How to Use
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ type OidcAuthEnv = {
|
||||||
OIDC_SCOPES?: string
|
OIDC_SCOPES?: string
|
||||||
OIDC_COOKIE_PATH?: string
|
OIDC_COOKIE_PATH?: string
|
||||||
OIDC_COOKIE_NAME?: 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,
|
ssnexp: orig?.ssnexp || Math.floor(Date.now() / 1000) + authExpires,
|
||||||
}
|
}
|
||||||
const session_jwt = await sign(updated, env.OIDC_AUTH_SECRET)
|
const session_jwt = await sign(updated, env.OIDC_AUTH_SECRET)
|
||||||
setCookie(c, env.OIDC_COOKIE_NAME, session_jwt, {
|
const cookieOptions =
|
||||||
path: env.OIDC_COOKIE_PATH,
|
env.OIDC_COOKIE_DOMAIN == null
|
||||||
httpOnly: true,
|
? { path: env.OIDC_COOKIE_PATH, httpOnly: true, secure: 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)
|
c.set('oidcAuthJwt', session_jwt)
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
@ -392,16 +393,21 @@ export const oidcAuthMiddleware = (): MiddlewareHandler => {
|
||||||
const auth = await getAuth(c)
|
const auth = await getAuth(c)
|
||||||
if (auth === null) {
|
if (auth === null) {
|
||||||
const path = new URL(env.OIDC_REDIRECT_URI).pathname
|
const path = new URL(env.OIDC_REDIRECT_URI).pathname
|
||||||
|
const cookieDomain = env.OIDC_COOKIE_DOMAIN
|
||||||
// Redirect to IdP for login
|
// Redirect to IdP for login
|
||||||
const state = oauth2.generateRandomState()
|
const state = oauth2.generateRandomState()
|
||||||
const nonce = oauth2.generateRandomNonce()
|
const nonce = oauth2.generateRandomNonce()
|
||||||
const code_verifier = oauth2.generateRandomCodeVerifier()
|
const code_verifier = oauth2.generateRandomCodeVerifier()
|
||||||
const code_challenge = await oauth2.calculatePKCECodeChallenge(code_verifier)
|
const code_challenge = await oauth2.calculatePKCECodeChallenge(code_verifier)
|
||||||
const url = await generateAuthorizationRequestUrl(c, state, nonce, code_challenge)
|
const url = await generateAuthorizationRequestUrl(c, state, nonce, code_challenge)
|
||||||
setCookie(c, 'state', state, { path, httpOnly: true, secure: true })
|
const cookieOptions =
|
||||||
setCookie(c, 'nonce', nonce, { path, httpOnly: true, secure: true })
|
cookieDomain == null
|
||||||
setCookie(c, 'code_verifier', code_verifier, { path, httpOnly: true, secure: true })
|
? { path, httpOnly: true, secure: true }
|
||||||
setCookie(c, 'continue', c.req.url, { 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)
|
return c.redirect(url)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -187,6 +187,7 @@ beforeEach(() => {
|
||||||
delete process.env.OIDC_SCOPES
|
delete process.env.OIDC_SCOPES
|
||||||
delete process.env.OIDC_COOKIE_PATH
|
delete process.env.OIDC_COOKIE_PATH
|
||||||
delete process.env.OIDC_COOKIE_NAME
|
delete process.env.OIDC_COOKIE_NAME
|
||||||
|
delete process.env.OIDC_COOKIE_DOMAIN
|
||||||
})
|
})
|
||||||
describe('oidcAuthMiddleware()', () => {
|
describe('oidcAuthMiddleware()', () => {
|
||||||
test('Should respond with 200 OK if session is active', async () => {
|
test('Should respond with 200 OK if session is active', async () => {
|
||||||
|
@ -280,6 +281,38 @@ describe('oidcAuthMiddleware()', () => {
|
||||||
expect(res.status).toBe(302)
|
expect(res.status).toBe(302)
|
||||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp('oidc-auth=; Max-Age=0; Path=/($|,)'))
|
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()', () => {
|
describe('processOAuthCallback()', () => {
|
||||||
test('Should successfully process the OAuth2.0 callback and redirect to the continue URL', async () => {
|
test('Should successfully process the OAuth2.0 callback and redirect to the continue URL', async () => {
|
||||||
|
|
Loading…
Reference in New Issue