Skip to content

Commit

Permalink
feat(oidc-auth): optional cookie name (#789)
Browse files Browse the repository at this point in the history
  • Loading branch information
maemaemae3 authored Oct 25, 2024
1 parent ee5d7e0 commit 68eec9e
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-teachers-knock.md
Original file line number Diff line number Diff line change
@@ -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')
1 change: 1 addition & 0 deletions packages/oidc-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 25 additions & 17 deletions packages/oidc-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -54,6 +55,7 @@ type OidcAuthEnv = {
OIDC_REDIRECT_URI: string
OIDC_SCOPES?: string
OIDC_COOKIE_PATH?: string
OIDC_COOKIE_NAME?: string
}

/**
Expand Down Expand Up @@ -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>
}

/**
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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) => {
Expand All @@ -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,
})
Expand All @@ -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
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -397,16 +405,16 @@ 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()
c.res.headers.set('Cache-Control', 'private, no-cache')
// 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,
})
Expand Down
16 changes: 16 additions & 0 deletions packages/oidc-auth/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 68eec9e

Please sign in to comment.