feat(oidc-auth): Add OIDC_AUTH_EXTERNAL_URL to support reverse proxies (#1248)
* feat(oidc-auth): Add OIDC_AUTH_EXTERNAL_URL to support reverse proxies * chore(oidc-auth): Omit unused exception variables in catch blockspull/1260/head
parent
a11e1edc7a
commit
6e4ee78e95
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/oidc-auth': minor
|
||||
---
|
||||
|
||||
Add OIDC_AUTH_EXTERNAL_URL to support reverse proxies
|
|
@ -45,20 +45,21 @@ npm i hono @hono/oidc-auth
|
|||
|
||||
The middleware requires the following variables to be set as either environment variables or by calling `initOidcAuthMiddleware`:
|
||||
|
||||
| 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. | `/callback` |
|
||||
| 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 |
|
||||
| OIDC_AUDIENCE | The audience for the access token | No default, optional. Primarily intended for use with Auth0. [`audience`](https://community.auth0.com/t/what-is-the-audience/71414) is required by Auth0 to receive a non-opaque access token, for other providers you may not need this. |
|
||||
| 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. | `/callback` |
|
||||
| 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 |
|
||||
| OIDC_AUDIENCE | The audience for the access token | No default, optional. Primarily intended for use with Auth0. [`audience`](https://community.auth0.com/t/what-is-the-audience/71414) is required by Auth0 to receive a non-opaque access token, for other providers you may not need this. |
|
||||
| OIDC_AUTH_EXTERNAL_URL | The full, public-facing base URL of the application, including the protocol and any path prefixes (e.g., `https://app.example.com` or `https://example.com/myapp`). This is used to construct the correct redirect URL after login when running behind a reverse proxy. | None. The middleware will use the URL from the incoming request. |
|
||||
|
||||
## How to Use
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@ beforeEach(() => {
|
|||
delete process.env.OIDC_COOKIE_NAME
|
||||
delete process.env.OIDC_COOKIE_DOMAIN
|
||||
delete process.env.OIDC_AUDIENCE
|
||||
delete process.env.OIDC_AUTH_EXTERNAL_URL
|
||||
})
|
||||
describe('oidcAuthMiddleware()', () => {
|
||||
test('Should respond with 200 OK if session is active', async () => {
|
||||
|
@ -403,6 +404,30 @@ describe('oidcAuthMiddleware()', () => {
|
|||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('location')).not.toMatch(/audience=/)
|
||||
})
|
||||
test('Should use external URL for continue cookie when OIDC_AUTH_EXTERNAL_URL is set', async () => {
|
||||
process.env.OIDC_AUTH_EXTERNAL_URL = 'https://public.example.com/app'
|
||||
const req = new Request('http://internal.host/sub/path?q=1#hash', {
|
||||
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)
|
||||
const expectedContinueUrl = 'https://public.example.com/app/sub/path?q=1#hash'
|
||||
expect(res.headers.get('set-cookie')).toMatch(
|
||||
`continue=${encodeURIComponent(expectedContinueUrl)}`
|
||||
)
|
||||
})
|
||||
test('Should return an error when OIDC_AUTH_EXTERNAL_URL is an invalid URL', async () => {
|
||||
process.env.OIDC_AUTH_EXTERNAL_URL = 'invalid-url'
|
||||
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)
|
||||
})
|
||||
})
|
||||
describe('processOAuthCallback()', () => {
|
||||
test('Should successfully process the OAuth2.0 callback and redirect to the continue URL', async () => {
|
||||
|
|
|
@ -59,6 +59,7 @@ export type OidcAuthEnv = {
|
|||
OIDC_COOKIE_NAME?: string
|
||||
OIDC_COOKIE_DOMAIN?: string
|
||||
OIDC_AUDIENCE?: string
|
||||
OIDC_AUTH_EXTERNAL_URL?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,6 +96,7 @@ const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
|
|||
OIDC_COOKIE_NAME: config?.OIDC_COOKIE_NAME ?? ev.OIDC_COOKIE_NAME,
|
||||
OIDC_COOKIE_DOMAIN: config?.OIDC_COOKIE_DOMAIN ?? ev.OIDC_COOKIE_DOMAIN,
|
||||
OIDC_AUDIENCE: config?.OIDC_AUDIENCE ?? ev.OIDC_AUDIENCE,
|
||||
OIDC_AUTH_EXTERNAL_URL: config?.OIDC_AUTH_EXTERNAL_URL ?? ev.OIDC_AUTH_EXTERNAL_URL,
|
||||
}
|
||||
if (oidcAuthEnv.OIDC_AUTH_SECRET === undefined) {
|
||||
throw new HTTPException(500, { message: 'Session secret is not provided' })
|
||||
|
@ -117,12 +119,21 @@ const setOidcAuthEnv = (c: Context, config?: Partial<OidcAuthEnv>) => {
|
|||
if (!oidcAuthEnv.OIDC_REDIRECT_URI.startsWith('/')) {
|
||||
try {
|
||||
new URL(oidcAuthEnv.OIDC_REDIRECT_URI)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new HTTPException(500, {
|
||||
message: 'The OIDC redirect URI is invalid. It must be a full URL or an absolute path',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (oidcAuthEnv.OIDC_AUTH_EXTERNAL_URL) {
|
||||
try {
|
||||
new URL(oidcAuthEnv.OIDC_AUTH_EXTERNAL_URL)
|
||||
} catch {
|
||||
throw new HTTPException(500, {
|
||||
message: 'The OIDC external URL is invalid. It must be a full URL.',
|
||||
})
|
||||
}
|
||||
}
|
||||
oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath
|
||||
oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName
|
||||
oidcAuthEnv.OIDC_AUTH_REFRESH_INTERVAL =
|
||||
|
@ -191,7 +202,7 @@ export const getAuth = async (c: Context): Promise<OidcAuth | null> => {
|
|||
}
|
||||
try {
|
||||
auth = (await verify(session_jwt, env.OIDC_AUTH_SECRET)) as OidcAuth
|
||||
} catch (e) {
|
||||
} catch {
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
return null
|
||||
}
|
||||
|
@ -462,10 +473,22 @@ export const oidcAuthMiddleware = (): MiddlewareHandler => {
|
|||
setCookie(c, 'state', state, cookieOptions)
|
||||
setCookie(c, 'nonce', nonce, cookieOptions)
|
||||
setCookie(c, 'code_verifier', code_verifier, cookieOptions)
|
||||
setCookie(c, 'continue', c.req.url, cookieOptions)
|
||||
const continueUrl = env.OIDC_AUTH_EXTERNAL_URL
|
||||
? (() => {
|
||||
const externalUrl = new URL(env.OIDC_AUTH_EXTERNAL_URL)
|
||||
const originalUrl = new URL(c.req.url)
|
||||
externalUrl.pathname = `${externalUrl.pathname.replace(/\/$/, '')}${
|
||||
originalUrl.pathname
|
||||
}`
|
||||
externalUrl.search = originalUrl.search
|
||||
externalUrl.hash = originalUrl.hash
|
||||
return externalUrl.toString()
|
||||
})()
|
||||
: c.req.url
|
||||
setCookie(c, 'continue', continueUrl, cookieOptions)
|
||||
return c.redirect(url)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
deleteCookie(c, env.OIDC_COOKIE_NAME, { path: env.OIDC_COOKIE_PATH })
|
||||
throw new HTTPException(500, { message: 'Invalid session' })
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue