feat(oidc-auth): optional cookie name (#789)
parent
ee5d7e0e74
commit
68eec9e2bc
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/oidc-auth': minor
|
||||
---
|
||||
|
||||
Optionally specify a custom cookie name using the OIDC_COOKIE_NAME environment variable (default is 'oidc-auth')
|
|
@ -54,6 +54,7 @@ The middleware requires the following environment variables to be set:
|
|||
| 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` |
|
||||
|
||||
## How to Use
|
||||
|
||||
|
|
|
@ -34,7 +34,8 @@ declare module 'hono' {
|
|||
}
|
||||
}
|
||||
|
||||
const oidcAuthCookieName = 'oidc-auth'
|
||||
const defaultOidcAuthCookieName = 'oidc-auth'
|
||||
const defaultOidcAuthCookiePath = '/'
|
||||
const defaultRefreshInterval = 15 * 60 // 15 minutes
|
||||
const defaultExpirationInterval = 60 * 60 * 24 // 1 day
|
||||
|
||||
|
@ -54,6 +55,7 @@ type OidcAuthEnv = {
|
|||
OIDC_REDIRECT_URI: string
|
||||
OIDC_SCOPES?: string
|
||||
OIDC_COOKIE_PATH?: string
|
||||
OIDC_COOKIE_NAME?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,9 +85,15 @@ const getOidcAuthEnv = (c: Context) => {
|
|||
if (oidcAuthEnv.OIDC_REDIRECT_URI === undefined) {
|
||||
throw new HTTPException(500, { message: 'OIDC redirect URI is not provided' })
|
||||
}
|
||||
oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath
|
||||
oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName
|
||||
oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL =
|
||||
oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL ?? `${defaultRefreshInterval}`
|
||||
oidcAuthEnv.OIDC_AUTH_EXPIRES = oidcAuthEnv.OIDC_AUTH_EXPIRES ?? `${defaultExpirationInterval}`
|
||||
oidcAuthEnv.OIDC_SCOPES = oidcAuthEnv.OIDC_SCOPES ?? ''
|
||||
c.set('oidcAuthEnv', oidcAuthEnv)
|
||||
}
|
||||
return oidcAuthEnv
|
||||
return oidcAuthEnv as Required<OidcAuthEnv>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,14 +137,14 @@ export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
|
|||
const env = getOidcAuthEnv(c)
|
||||
let auth: Partial<OidcAuth> | null = c.get('oidcAuth')
|
||||
if (auth === undefined) {
|
||||
const session_jwt = getCookie(c, oidcAuthCookieName)
|
||||
const session_jwt = getCookie(c, env.OIDC_COOKIE_NAME)
|
||||
if (session_jwt === undefined) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
auth = await verify(session_jwt, env.OIDC_AUTH_SECRET)
|
||||
} catch (e) {
|
||||
deleteCookie(c, oidcAuthCookieName, { path: env.OIDC_COOKIE_PATH ?? '/' })
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
return null
|
||||
}
|
||||
if (auth === null || auth.rtkexp === undefined || auth.ssnexp === undefined) {
|
||||
|
@ -151,7 +159,7 @@ export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
|
|||
if (auth.rtkexp < now) {
|
||||
// Refresh the token if it has expired
|
||||
if (auth.rtk === undefined || auth.rtk === '') {
|
||||
deleteCookie(c, oidcAuthCookieName, { path: env.OIDC_COOKIE_PATH ?? '/' })
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
return null
|
||||
}
|
||||
const as = await getAuthorizationServer(c)
|
||||
|
@ -160,7 +168,7 @@ export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
|
|||
const result = await oauth2.processRefreshTokenResponse(as, client, response)
|
||||
if (oauth2.isOAuth2Error(result)) {
|
||||
// The refresh_token might be expired or revoked
|
||||
deleteCookie(c, oidcAuthCookieName, { path: env.OIDC_COOKIE_PATH ?? '/' })
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
return null
|
||||
}
|
||||
auth = await updateAuth(c, auth as OidcAuth, result)
|
||||
|
@ -190,8 +198,8 @@ const updateAuth = async (
|
|||
): Promise<OidcAuth> => {
|
||||
const env = getOidcAuthEnv(c)
|
||||
const claims = oauth2.getValidatedIdTokenClaims(response)
|
||||
const authRefreshInterval = Number(env.OIDC_AUTH_REFRESH_INTERVAL!) || defaultRefreshInterval
|
||||
const authExpires = Number(env.OIDC_AUTH_EXPIRES!) || defaultExpirationInterval
|
||||
const authRefreshInterval = Number(env.OIDC_AUTH_REFRESH_INTERVAL)
|
||||
const authExpires = Number(env.OIDC_AUTH_EXPIRES)
|
||||
const claimsHook: OidcClaimsHook =
|
||||
c.get('oidcClaimsHook') ??
|
||||
(async (orig, claims) => {
|
||||
|
@ -207,8 +215,8 @@ const updateAuth = async (
|
|||
ssnexp: orig?.ssnexp || Math.floor(Date.now() / 1000) + authExpires,
|
||||
}
|
||||
const session_jwt = await sign(updated, env.OIDC_AUTH_SECRET)
|
||||
setCookie(c, oidcAuthCookieName, session_jwt, {
|
||||
path: env.OIDC_COOKIE_PATH ?? '/',
|
||||
setCookie(c, env.OIDC_COOKIE_NAME, session_jwt, {
|
||||
path: env.OIDC_COOKIE_PATH,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
})
|
||||
|
@ -220,10 +228,10 @@ const updateAuth = async (
|
|||
* Revokes the refresh token of the current session and deletes the session cookie
|
||||
*/
|
||||
export const revokeSession = async (c: Context): Promise<void> => {
|
||||
const session_jwt = getCookie(c, oidcAuthCookieName)
|
||||
const env = getOidcAuthEnv(c)
|
||||
const session_jwt = getCookie(c, env.OIDC_COOKIE_NAME)
|
||||
if (session_jwt !== undefined) {
|
||||
const env = getOidcAuthEnv(c)
|
||||
deleteCookie(c, oidcAuthCookieName, { path: env.OIDC_COOKIE_PATH ?? '/' })
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
const auth = await verify(session_jwt, env.OIDC_AUTH_SECRET)
|
||||
if (auth.rtk !== undefined && auth.rtk !== '') {
|
||||
// revoke refresh token
|
||||
|
@ -269,7 +277,7 @@ const generateAuthorizationRequestUrl = async (
|
|||
throw new HTTPException(500, {
|
||||
message: 'The supported scopes information is not provided by the IdP',
|
||||
})
|
||||
} else if (env.OIDC_SCOPES != null) {
|
||||
} else if (env.OIDC_SCOPES !== '') {
|
||||
for (const scope of env.OIDC_SCOPES.split(' ')) {
|
||||
if (as.scopes_supported.indexOf(scope) === -1) {
|
||||
throw new HTTPException(500, {
|
||||
|
@ -397,7 +405,7 @@ export const oidcAuthMiddleware = (): MiddlewareHandler => {
|
|||
return c.redirect(url)
|
||||
}
|
||||
} catch (e) {
|
||||
deleteCookie(c, oidcAuthCookieName, { path: env.OIDC_COOKIE_PATH ?? '/' })
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
throw new HTTPException(500, { message: 'Invalid session' })
|
||||
}
|
||||
await next()
|
||||
|
@ -405,8 +413,8 @@ export const oidcAuthMiddleware = (): MiddlewareHandler => {
|
|||
// Workaround to set the session cookie when the response is returned by the origin server
|
||||
const session_jwt = c.get('oidcAuthJwt')
|
||||
if (session_jwt !== undefined) {
|
||||
setCookie(c, oidcAuthCookieName, session_jwt, {
|
||||
path: env.OIDC_COOKIE_PATH ?? '/',
|
||||
setCookie(c, env.OIDC_COOKIE_NAME, session_jwt, {
|
||||
path: env.OIDC_COOKIE_PATH,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
})
|
||||
|
|
|
@ -186,6 +186,7 @@ beforeEach(() => {
|
|||
process.env.OIDC_AUTH_EXPIRES = MOCK_AUTH_EXPIRES
|
||||
delete process.env.OIDC_SCOPES
|
||||
delete process.env.OIDC_COOKIE_PATH
|
||||
delete process.env.OIDC_COOKIE_NAME
|
||||
})
|
||||
describe('oidcAuthMiddleware()', () => {
|
||||
test('Should respond with 200 OK if session is active', async () => {
|
||||
|
@ -374,6 +375,21 @@ describe('processOAuthCallback()', () => {
|
|||
)
|
||||
expect(res.headers.get('location')).toBe('http://localhost/1234')
|
||||
})
|
||||
test('Should respond with custom cookie name', async () => {
|
||||
const MOCK_COOKIE_NAME = (process.env.OIDC_COOKIE_NAME = 'custom-auth-cookie')
|
||||
const req = new Request(`${MOCK_REDIRECT_URI}?code=1234&state=${MOCK_STATE}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
cookie: `state=${MOCK_STATE}; nonce=${MOCK_NONCE}; code_verifier=1234; continue=http%3A%2F%2Flocalhost%2F1234`,
|
||||
},
|
||||
})
|
||||
const res = await app.request(req, {}, {})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`${MOCK_COOKIE_NAME}=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`)
|
||||
)
|
||||
})
|
||||
test('Should return an error if the state parameter does not match', async () => {
|
||||
const req = new Request(`${MOCK_REDIRECT_URI}?code=1234&state=${MOCK_STATE}`, {
|
||||
method: 'GET',
|
||||
|
|
Loading…
Reference in New Issue