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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in New Issue