feat(oidc-auth): access and set claims (#711)
* feat(oidc-auth): access and set claims Signed-off-by: Axel Meinhardt <26243798+ameinhardt@users.noreply.github.com> * chore(oidc-auth): add changeset, doc and fix types Signed-off-by: Axel Meinhardt <26243798+ameinhardt@users.noreply.github.com> * chore(oidc-auth): add tests Signed-off-by: Axel Meinhardt <26243798+ameinhardt@users.noreply.github.com> * refactored some types --------- Signed-off-by: Axel Meinhardt <26243798+ameinhardt@users.noreply.github.com> Co-authored-by: Yusuke Wada <yusuke@kamawada.com>pull/716/head
parent
cd99b40177
commit
5675a5fc32
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/oidc-auth': minor
|
||||
---
|
||||
|
||||
define custom scope, access oauth response and set custom session claims
|
|
@ -25,13 +25,13 @@ This middleware requires the following features for the IdP:
|
|||
|
||||
Here is a list of the IdPs that I have tested:
|
||||
|
||||
| IdP Name | OpenID issuer URL |
|
||||
| ---- | ---- |
|
||||
| Auth0 | `https://<tenant>.<region>.auth0.com` |
|
||||
| IdP Name | OpenID issuer URL |
|
||||
| ----------- | --------------------------------------------------------- |
|
||||
| Auth0 | `https://<tenant>.<region>.auth0.com` |
|
||||
| AWS Cognito | `https://cognito-idp.<region>.amazonaws.com/<userPoolID>` |
|
||||
| GitLab | `https://gitlab.com` |
|
||||
| Google | `https://accounts.google.com` |
|
||||
| Slack | `https://slack.com` |
|
||||
| GitLab | `https://gitlab.com` |
|
||||
| Google | `https://accounts.google.com` |
|
||||
| Slack | `https://slack.com` |
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -43,22 +43,23 @@ 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_COOKIE_PATH | The path to which the `oidc-auth` cookie is set. Restrict to not send it with every request to your domain | / |
|
||||
| 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 | / |
|
||||
|
||||
## How to Use
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
import { oidcAuthMiddleware, getAuth, revokeSession, processOAuthCallback } from '@hono/oidc-auth';
|
||||
import { oidcAuthMiddleware, getAuth, revokeSession, processOAuthCallback } from '@hono/oidc-auth'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
|
@ -82,7 +83,7 @@ export default app
|
|||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
import { oidcAuthMiddleware, getAuth } from '@hono/oidc-auth';
|
||||
import { oidcAuthMiddleware, getAuth } from '@hono/oidc-auth'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
|
@ -92,15 +93,48 @@ app.get('*', async (c) => {
|
|||
if (!auth?.email.endsWith('@example.com')) {
|
||||
return c.text('Unauthorized', 401)
|
||||
}
|
||||
const response = await c.env.ASSETS.fetch(c.req.raw);
|
||||
const response = await c.env.ASSETS.fetch(c.req.raw)
|
||||
// clone the response to return a response with modifiable headers
|
||||
const newResponse = new Response(response.body, response)
|
||||
return newResponse
|
||||
});
|
||||
})
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
## Using original response or additional claims
|
||||
|
||||
```typescript
|
||||
import type { IDToken, OidcAuth, TokenEndpointResponses } from '@hono/oidc-auth';
|
||||
import { processOAuthCallback } from '@hono/oidc-auth';
|
||||
import type { Context, OidcAuthClaims } from 'hono';
|
||||
|
||||
declare module 'hono' {
|
||||
interface OidcAuthClaims {
|
||||
name: string
|
||||
sub: string
|
||||
}
|
||||
}
|
||||
|
||||
const oidcClaimsHook = async (orig: OidcAuth | undefined, claims: IDToken | undefined, _response: TokenEndpointResponses): Promise<OidcAuthClaims> => {
|
||||
/*
|
||||
const { someOtherInfo } = await fetch(c.get('oidcAuthorizationServer').userinfo_endpoint, {
|
||||
header: _response.access_token
|
||||
}).then((res) => res.json())
|
||||
*/
|
||||
return {
|
||||
name: claims?.name as string ?? orig?.name ?? '',
|
||||
sub: claims?.sub ?? orig?.sub ?? ''
|
||||
};
|
||||
}),
|
||||
...
|
||||
app.get('/callback', async (c) => {
|
||||
c.set('oidcClaimsHook', oidcClaimsHook); // also assure to set before any getAuth(), in case the token is refreshed
|
||||
return processOAuthCallback(c);
|
||||
})
|
||||
...
|
||||
```
|
||||
|
||||
Note:
|
||||
If explicit logout is not required, the logout handler can be omitted.
|
||||
If the middleware is applied to the callback URL, the default callback handling in the middleware can be used, so the explicit callback handling is not required.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* OpenID Connect authentication middleware for hono
|
||||
*/
|
||||
|
||||
import type { Context, MiddlewareHandler } from 'hono'
|
||||
import type { Context, MiddlewareHandler, OidcAuthClaims } from 'hono'
|
||||
import { env } from 'hono/adapter'
|
||||
import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
|
||||
import { createMiddleware } from 'hono/factory'
|
||||
|
@ -10,13 +10,27 @@ import { HTTPException } from 'hono/http-exception'
|
|||
import { sign, verify } from 'hono/jwt'
|
||||
import * as oauth2 from 'oauth4webapi'
|
||||
|
||||
export type IDToken = oauth2.IDToken
|
||||
export type TokenEndpointResponses =
|
||||
| oauth2.OpenIDTokenEndpointResponse
|
||||
| oauth2.TokenEndpointResponse
|
||||
export type OidcClaimsHook = (
|
||||
orig: OidcAuth | undefined,
|
||||
claims: IDToken | undefined,
|
||||
response: TokenEndpointResponses
|
||||
) => Promise<OidcAuthClaims>
|
||||
|
||||
declare module 'hono' {
|
||||
export interface OidcAuthClaims {
|
||||
readonly [claim: string]: oauth2.JsonValue | undefined
|
||||
}
|
||||
interface ContextVariableMap {
|
||||
oidcAuthEnv: OidcAuthEnv
|
||||
oidcAuthorizationServer: oauth2.AuthorizationServer
|
||||
oidcClient: oauth2.Client
|
||||
oidcAuth: OidcAuth | null
|
||||
oidcAuthJwt: string
|
||||
oidcClaimsHook?: OidcClaimsHook
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,12 +39,10 @@ const defaultRefreshInterval = 15 * 60 // 15 minutes
|
|||
const defaultExpirationInterval = 60 * 60 * 24 // 1 day
|
||||
|
||||
export type OidcAuth = {
|
||||
sub: string
|
||||
email: string
|
||||
rtk: string // refresh token
|
||||
rtkexp: number // token expiration time ; refresh token if it's expired
|
||||
ssnexp: number // session expiration time; if it's expired, revoke session and redirect to IdP
|
||||
}
|
||||
} & OidcAuthClaims
|
||||
|
||||
type OidcAuthEnv = {
|
||||
OIDC_AUTH_SECRET: string
|
||||
|
@ -40,6 +52,7 @@ type OidcAuthEnv = {
|
|||
OIDC_CLIENT_ID: string
|
||||
OIDC_CLIENT_SECRET: string
|
||||
OIDC_REDIRECT_URI: string
|
||||
OIDC_SCOPES?: string
|
||||
OIDC_COOKIE_PATH?: string
|
||||
}
|
||||
|
||||
|
@ -114,7 +127,7 @@ export const getClient = (c: Context): oauth2.Client => {
|
|||
*/
|
||||
export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
|
||||
const env = getOidcAuthEnv(c)
|
||||
let auth = c.get('oidcAuth')
|
||||
let auth: Partial<OidcAuth> | null = c.get('oidcAuth')
|
||||
if (auth === undefined) {
|
||||
const session_jwt = getCookie(c, oidcAuthCookieName)
|
||||
if (session_jwt === undefined) {
|
||||
|
@ -150,11 +163,11 @@ export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
|
|||
deleteCookie(c, oidcAuthCookieName, { path: env.OIDC_COOKIE_PATH ?? '/' })
|
||||
return null
|
||||
}
|
||||
auth = await updateAuth(c, auth, result)
|
||||
auth = await updateAuth(c, auth as OidcAuth, result)
|
||||
}
|
||||
c.set('oidcAuth', auth)
|
||||
c.set('oidcAuth', auth as OidcAuth)
|
||||
}
|
||||
return auth
|
||||
return auth as OidcAuth
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,15 +186,22 @@ const setAuth = async (
|
|||
const updateAuth = async (
|
||||
c: Context,
|
||||
orig: OidcAuth | undefined,
|
||||
response: oauth2.OpenIDTokenEndpointResponse | oauth2.TokenEndpointResponse
|
||||
response: TokenEndpointResponses
|
||||
): 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 updated: OidcAuth = {
|
||||
sub: claims?.sub || orig?.sub || '',
|
||||
email: (claims?.email as string) || orig?.email || '',
|
||||
const claimsHook: OidcClaimsHook =
|
||||
c.get('oidcClaimsHook') ??
|
||||
(async (orig, claims) => {
|
||||
return {
|
||||
sub: claims?.sub || orig?.sub || '',
|
||||
email: (claims?.email as string) || orig?.email || '',
|
||||
}
|
||||
})
|
||||
const updated = {
|
||||
...(await claimsHook(orig, claims, response)),
|
||||
rtk: response.refresh_token || orig?.rtk || '',
|
||||
rtkexp: Math.floor(Date.now() / 1000) + authRefreshInterval,
|
||||
ssnexp: orig?.ssnexp || Math.floor(Date.now() / 1000) + authExpires,
|
||||
|
@ -249,12 +269,17 @@ const generateAuthorizationRequestUrl = async (
|
|||
throw new HTTPException(500, {
|
||||
message: 'The supported scopes information is not provided by the IdP',
|
||||
})
|
||||
} else if (as.scopes_supported.indexOf('email') === -1) {
|
||||
throw new HTTPException(500, { message: 'The "email" scope is not supported by the IdP' })
|
||||
} else if (as.scopes_supported.indexOf('offline_access') === -1) {
|
||||
authorizationRequestUrl.searchParams.set('scope', 'openid email')
|
||||
} else if (env.OIDC_SCOPES != null) {
|
||||
for (const scope of env.OIDC_SCOPES.split(' ')) {
|
||||
if (as.scopes_supported.indexOf(scope) === -1) {
|
||||
throw new HTTPException(500, {
|
||||
message: `The '${scope}' scope is not supported by the IdP`,
|
||||
})
|
||||
}
|
||||
}
|
||||
authorizationRequestUrl.searchParams.set('scope', env.OIDC_SCOPES)
|
||||
} else {
|
||||
authorizationRequestUrl.searchParams.set('scope', 'openid email offline_access')
|
||||
authorizationRequestUrl.searchParams.set('scope', as.scopes_supported.join(' '))
|
||||
}
|
||||
authorizationRequestUrl.searchParams.set('state', state)
|
||||
authorizationRequestUrl.searchParams.set('nonce', nonce)
|
||||
|
|
|
@ -10,6 +10,7 @@ const MOCK_CLIENT_SECRET = 'CLIENT_SECRET_001'
|
|||
const MOCK_REDIRECT_URI = 'http://localhost/callback'
|
||||
const MOCK_SUBJECT = 'USER_ID_001'
|
||||
const MOCK_EMAIL = 'user001@example.com'
|
||||
const MOCK_NAME = 'John Doe'
|
||||
const MOCK_STATE = crypto.randomBytes(16).toString('hex') // 32 bytes
|
||||
const MOCK_NONCE = crypto.randomBytes(16).toString('hex') // 32 bytes
|
||||
const MOCK_AUTH_SECRET = crypto.randomBytes(16).toString('hex') // 32 bytes
|
||||
|
@ -19,6 +20,8 @@ const MOCK_ID_TOKEN = jwt.sign(
|
|||
iss: MOCK_ISSUER,
|
||||
aud: MOCK_CLIENT_ID,
|
||||
sub: MOCK_SUBJECT,
|
||||
email: MOCK_EMAIL,
|
||||
name: MOCK_NAME,
|
||||
exp: Math.floor(Date.now() / 1000) + 10 * 60, // 10 minutes
|
||||
nonce: MOCK_NONCE,
|
||||
},
|
||||
|
@ -153,13 +156,21 @@ jest.unstable_mockModule('oauth4webapi', () => {
|
|||
}
|
||||
})
|
||||
|
||||
const { oidcAuthMiddleware, getAuth, revokeSession } = await import('../src')
|
||||
const { oidcAuthMiddleware, getAuth, processOAuthCallback, revokeSession } = await import('../src')
|
||||
|
||||
const app = new Hono()
|
||||
app.get('/logout', async (c) => {
|
||||
await revokeSession(c)
|
||||
return c.text('OK')
|
||||
})
|
||||
app.get('/callback-custom', async (c) => {
|
||||
c.set('oidcClaimsHook', async (orig, claims, response) => ({
|
||||
name: (claims?.name as string) ?? orig?.name ?? '',
|
||||
sub: claims?.sub ?? orig?.sub ?? '',
|
||||
token: response.access_token,
|
||||
}))
|
||||
return processOAuthCallback(c)
|
||||
})
|
||||
app.use('/*', oidcAuthMiddleware())
|
||||
app.all('/*', async (c) => {
|
||||
const auth = await getAuth(c)
|
||||
|
@ -173,6 +184,7 @@ beforeEach(() => {
|
|||
process.env.OIDC_REDIRECT_URI = MOCK_REDIRECT_URI
|
||||
process.env.OIDC_AUTH_SECRET = MOCK_AUTH_SECRET
|
||||
process.env.OIDC_AUTH_EXPIRES = MOCK_AUTH_EXPIRES
|
||||
delete process.env.OIDC_SCOPES
|
||||
delete process.env.OIDC_COOKIE_PATH
|
||||
})
|
||||
describe('oidcAuthMiddleware()', () => {
|
||||
|
@ -199,6 +211,22 @@ describe('oidcAuthMiddleware()', () => {
|
|||
)
|
||||
})
|
||||
test('Should redirect to authorization endpoint if session is expired', 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('location')).toMatch(/scope=openid(%20|\+)email(%20|\+)profile&/)
|
||||
expect(res.headers.get('location')).toMatch('access_type=offline&prompt=consent')
|
||||
expect(res.headers.get('set-cookie')).toMatch(`state=${MOCK_STATE}`)
|
||||
expect(res.headers.get('set-cookie')).toMatch(`nonce=${MOCK_NONCE}`)
|
||||
expect(res.headers.get('set-cookie')).toMatch('code_verifier=')
|
||||
expect(res.headers.get('set-cookie')).toMatch('continue=http%3A%2F%2Flocalhost%2F')
|
||||
})
|
||||
test('Should use custom scope, if defined', async () => {
|
||||
process.env.OIDC_SCOPES = 'openid email'
|
||||
const req = new Request('http://localhost/', {
|
||||
method: 'GET',
|
||||
headers: { cookie: `oidc-auth=${MOCK_JWT_EXPIRED_SESSION}` },
|
||||
|
@ -213,6 +241,16 @@ describe('oidcAuthMiddleware()', () => {
|
|||
expect(res.headers.get('set-cookie')).toMatch('code_verifier=')
|
||||
expect(res.headers.get('set-cookie')).toMatch('continue=http%3A%2F%2Flocalhost%2F')
|
||||
})
|
||||
test('Custom scope is limited to supported scopes', async () => {
|
||||
process.env.OIDC_SCOPES = 'openid email salary'
|
||||
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(500)
|
||||
})
|
||||
test('Should redirect to authorization endpoint if no session cookie is found', async () => {
|
||||
const req = new Request('http://localhost/', {
|
||||
method: 'GET',
|
||||
|
@ -251,18 +289,62 @@ describe('processOAuthCallback()', () => {
|
|||
},
|
||||
})
|
||||
const res = await app.request(req, {}, {})
|
||||
const { email, name, sub } = JSON.parse(
|
||||
atob(
|
||||
res.headers
|
||||
.get('set-cookie')
|
||||
?.match(/oidc-auth=[^;]+/)?.[0]
|
||||
?.split('.')[1] as string
|
||||
)
|
||||
)
|
||||
expect(sub).toBe(MOCK_SUBJECT)
|
||||
expect(email).toBe(MOCK_EMAIL)
|
||||
expect(name).toBeUndefined()
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('location')).toBe('http://localhost/1234')
|
||||
})
|
||||
test('Should respond with customized claims', async () => {
|
||||
const req = new Request(`${MOCK_REDIRECT_URI}-custom?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, {}, {})
|
||||
const { email, name, sub } = JSON.parse(
|
||||
atob(
|
||||
res.headers
|
||||
.get('set-cookie')
|
||||
?.match(/oidc-auth=[^;]+/)?.[0]
|
||||
?.split('.')[1] as string
|
||||
)
|
||||
)
|
||||
expect(sub).toBe(MOCK_SUBJECT)
|
||||
expect(email).toBeUndefined()
|
||||
expect(name).toBe(MOCK_NAME)
|
||||
const path = new URL(MOCK_REDIRECT_URI).pathname
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`state=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`nonce=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`code_verifier=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`continue=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp('oidc-auth=[^;]+; Path=/; HttpOnly; Secure'))
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`state=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`nonce=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`code_verifier=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`continue=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp('oidc-auth=[^;]+; Path=/; HttpOnly; Secure')
|
||||
)
|
||||
expect(res.headers.get('location')).toBe('http://localhost/1234')
|
||||
})
|
||||
test('Should restrict the auth cookie to a given path', async () => {
|
||||
const MOCK_COOKIE_PATH = process.env.OIDC_COOKIE_PATH = '/some/subpath/for/authentication'
|
||||
const MOCK_COOKIE_PATH = (process.env.OIDC_COOKIE_PATH = '/some/subpath/for/authentication')
|
||||
process.env.OIDC_REDIRECT_URI = `http://localhost${MOCK_COOKIE_PATH}/callback`
|
||||
const parentApp = new Hono().route(MOCK_COOKIE_PATH, app)
|
||||
const path = new URL(process.env.OIDC_REDIRECT_URI).pathname
|
||||
|
@ -275,11 +357,21 @@ describe('processOAuthCallback()', () => {
|
|||
const res = await parentApp.request(req, {}, {})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`state=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`nonce=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`code_verifier=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`continue=; Max-Age=0; Path=${path}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`oidc-auth=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`state=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`nonce=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`code_verifier=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`continue=; Max-Age=0; Path=${path}($|,)`)
|
||||
)
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`oidc-auth=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`)
|
||||
)
|
||||
expect(res.headers.get('location')).toBe('http://localhost/1234')
|
||||
})
|
||||
test('Should return an error if the state parameter does not match', async () => {
|
||||
|
@ -329,7 +421,7 @@ describe('RevokeSession()', () => {
|
|||
expect(res.headers.get('set-cookie')).toMatch(new RegExp('oidc-auth=; Max-Age=0; Path=/($|,)'))
|
||||
})
|
||||
test('Should revoke the session of the given path', async () => {
|
||||
const MOCK_COOKIE_PATH = process.env.OIDC_COOKIE_PATH = '/some/subpath/for/authentication'
|
||||
const MOCK_COOKIE_PATH = (process.env.OIDC_COOKIE_PATH = '/some/subpath/for/authentication')
|
||||
const parentApp = new Hono().route(MOCK_COOKIE_PATH, app)
|
||||
const req = new Request(`http://localhost${MOCK_COOKIE_PATH}/logout`, {
|
||||
method: 'GET',
|
||||
|
@ -338,6 +430,8 @@ describe('RevokeSession()', () => {
|
|||
const res = await parentApp.request(req, {}, {})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('set-cookie')).toMatch(new RegExp(`oidc-auth=; Max-Age=0; Path=${MOCK_COOKIE_PATH}($|,)`))
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
new RegExp(`oidc-auth=; Max-Age=0; Path=${MOCK_COOKIE_PATH}($|,)`)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue