Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oidc-auth): optional cookie name #789

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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