From 5387a1679603f0ab4cbe14a1df3a2cc90ab35a3a Mon Sep 17 00:00:00 2001 From: Yoshio HANAWA Date: Wed, 8 Jan 2025 02:27:23 +0900 Subject: [PATCH 1/3] feat(oidc-auth): support absolute path for redirect URI --- .changeset/kind-cheetahs-give.md | 5 ++ packages/oidc-auth/README.md | 3 +- packages/oidc-auth/src/index.ts | 29 +++++++---- packages/oidc-auth/test/index.test.ts | 72 +++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 .changeset/kind-cheetahs-give.md diff --git a/.changeset/kind-cheetahs-give.md b/.changeset/kind-cheetahs-give.md new file mode 100644 index 000000000..da6683a68 --- /dev/null +++ b/.changeset/kind-cheetahs-give.md @@ -0,0 +1,5 @@ +--- +'@hono/oidc-auth': patch +--- + +Add support for absolute path in OIDC_REDIRECT_URI and set its default value to '/callback' diff --git a/packages/oidc-auth/README.md b/packages/oidc-auth/README.md index 79676951c..369cf4892 100644 --- a/packages/oidc-auth/README.md +++ b/packages/oidc-auth/README.md @@ -51,7 +51,7 @@ The middleware requires the following environment variables to be set: | 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_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` | @@ -62,7 +62,6 @@ The middleware requires the following environment variables to be set: ```typescript import { Hono } from 'hono' import { oidcAuthMiddleware, getAuth, revokeSession, processOAuthCallback } from '@hono/oidc-auth' - const app = new Hono() app.get('/logout', async (c) => { diff --git a/packages/oidc-auth/src/index.ts b/packages/oidc-auth/src/index.ts index 790cf97d4..5bc9906fd 100644 --- a/packages/oidc-auth/src/index.ts +++ b/packages/oidc-auth/src/index.ts @@ -34,8 +34,9 @@ declare module 'hono' { } } -const defaultOidcAuthCookieName = 'oidc-auth' +const defaultOidcRedirectUri = '/callback' const defaultOidcAuthCookiePath = '/' +const defaultOidcAuthCookieName = 'oidc-auth' const defaultRefreshInterval = 15 * 60 // 15 minutes const defaultExpirationInterval = 60 * 60 * 24 // 1 day @@ -52,7 +53,7 @@ type OidcAuthEnv = { OIDC_ISSUER: string OIDC_CLIENT_ID: string OIDC_CLIENT_SECRET: string - OIDC_REDIRECT_URI: string + OIDC_REDIRECT_URI?: string OIDC_SCOPES?: string OIDC_COOKIE_PATH?: string OIDC_COOKIE_NAME?: string @@ -83,8 +84,13 @@ const getOidcAuthEnv = (c: Context) => { if (oidcAuthEnv.OIDC_CLIENT_SECRET === undefined) { throw new HTTPException(500, { message: 'OIDC client secret is not provided' }) } - if (oidcAuthEnv.OIDC_REDIRECT_URI === undefined) { - throw new HTTPException(500, { message: 'OIDC redirect URI is not provided' }) + oidcAuthEnv.OIDC_REDIRECT_URI = oidcAuthEnv.OIDC_REDIRECT_URI ?? defaultOidcRedirectUri + if (!oidcAuthEnv.OIDC_REDIRECT_URI.startsWith('/')) { + try { + new URL(oidcAuthEnv.OIDC_REDIRECT_URI) + } catch (e) { + throw new HTTPException(500, { message: 'The OIDC redirect URI is invalid. It must be a full URL or an absolute path' }) + } } oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName @@ -271,8 +277,9 @@ const generateAuthorizationRequestUrl = async ( const as = await getAuthorizationServer(c) const client = getClient(c) const authorizationRequestUrl = new URL(as.authorization_endpoint!) + const redirect_uri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString() authorizationRequestUrl.searchParams.set('client_id', client.client_id) - authorizationRequestUrl.searchParams.set('redirect_uri', env.OIDC_REDIRECT_URI) + authorizationRequestUrl.searchParams.set('redirect_uri', redirect_uri) authorizationRequestUrl.searchParams.set('response_type', 'code') if (as.scopes_supported === undefined || as.scopes_supported.length === 0) { throw new HTTPException(500, { @@ -312,7 +319,7 @@ export const processOAuthCallback = async (c: Context) => { // Parses the authorization response and validates the state parameter const state = getCookie(c, 'state') - const path = new URL(env.OIDC_REDIRECT_URI).pathname + const path = new URL(env.OIDC_REDIRECT_URI, c.req.url).pathname deleteCookie(c, 'state', { path }) const currentUrl: URL = new URL(c.req.url) const params = oauth2.validateAuthResponse(as, client, currentUrl, state) @@ -333,11 +340,12 @@ export const processOAuthCallback = async (c: Context) => { if (code === undefined || nonce === undefined || code_verifier === undefined) { throw new HTTPException(500, { message: 'Missing required parameters / cookies' }) } + const redirect_uri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString() const result = await exchangeAuthorizationCode( as, client, params, - env.OIDC_REDIRECT_URI, + redirect_uri, nonce, code_verifier ) @@ -385,14 +393,15 @@ const exchangeAuthorizationCode = async ( export const oidcAuthMiddleware = (): MiddlewareHandler => { return createMiddleware(async (c, next) => { const env = getOidcAuthEnv(c) - const uri = c.req.url.split('?')[0] - if (uri === env.OIDC_REDIRECT_URI) { + const uri = new URL(c.req.url) + const redirect_uri = new URL(env.OIDC_REDIRECT_URI, c.req.url) + if (uri.pathname === redirect_uri.pathname && uri.origin === redirect_uri.origin) { return processOAuthCallback(c) } try { const auth = await getAuth(c) if (auth === null) { - const path = new URL(env.OIDC_REDIRECT_URI).pathname + const path = new URL(env.OIDC_REDIRECT_URI, c.req.url).pathname const cookieDomain = env.OIDC_COOKIE_DOMAIN // Redirect to IdP for login const state = oauth2.generateRandomState() diff --git a/packages/oidc-auth/test/index.test.ts b/packages/oidc-auth/test/index.test.ts index 9a304f8c7..3e8aaf663 100644 --- a/packages/oidc-auth/test/index.test.ts +++ b/packages/oidc-auth/test/index.test.ts @@ -281,6 +281,66 @@ describe('oidcAuthMiddleware()', () => { expect(res.status).toBe(302) expect(res.headers.get('set-cookie')).toMatch(new RegExp('oidc-auth=; Max-Age=0; Path=/($|,)')) }) + test('Should return an error when OIDC_ISSUER is undefined', async () => { + delete process.env.OIDC_ISSUER + const req = new Request('http://localhost/', { + method: 'GET', + headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` }, + }) + const res = await app.request(req, {}, {}) + expect(res).not.toBeNull() + expect(res.status).toBe(500) + }) + test('Should return an error when OIDC_CLIENT_ID is undefined', async () => { + delete process.env.OIDC_CLIENT_ID + const req = new Request('http://localhost/', { + method: 'GET', + headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` }, + }) + const res = await app.request(req, {}, {}) + expect(res).not.toBeNull() + expect(res.status).toBe(500) + }) + test('Should return an error when OIDC_CLIENT_SECRET is undefined', async () => { + delete process.env.OIDC_CLIENT_SECRET + const req = new Request('http://localhost/', { + method: 'GET', + headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` }, + }) + const res = await app.request(req, {}, {}) + expect(res).not.toBeNull() + expect(res.status).toBe(500) + }) + test('Should return an error when OIDC_REDIRECT_URI is a relative path', async () => { + process.env.OIDC_REDIRECT_URI = '../callback' + const req = new Request('http://localhost/', { + method: 'GET', + headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` }, + }) + const res = await app.request(req, {}, {}) + expect(res).not.toBeNull() + expect(res.status).toBe(500) + }) + test('Should return an error when OIDC_AUTH_SECRET is undefined', async () => { + delete process.env.OIDC_AUTH_SECRET + const req = new Request('http://localhost/', { + method: 'GET', + headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` }, + }) + const res = await app.request(req, {}, {}) + expect(res).not.toBeNull() + expect(res.status).toBe(500) + }) + test('Should return an error when OIDC_AUTH_SECRET is too short', async () => { + process.env.OIDC_AUTH_SECRET = '1234567890123456789012345678901' + const req = new Request('http://localhost/', { + method: 'GET', + headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` }, + }) + const res = await app.request(req, {}, {}) + expect(res).not.toBeNull() + expect(res.status).toBe(500) + }) test('Should Domain attribute of the cookie not set if env value not defined', async () => { const req = new Request('http://localhost/', { method: 'GET', @@ -338,6 +398,18 @@ describe('processOAuthCallback()', () => { expect(res.status).toBe(302) expect(res.headers.get('location')).toBe('http://localhost/1234') }) + test('Verify default callback path when OIDC_REDIRECT_URI is undefined', async () => { + delete process.env.OIDC_REDIRECT_URI + 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) + }) test('Should respond with customized claims', async () => { const req = new Request(`${MOCK_REDIRECT_URI}-custom?code=1234&state=${MOCK_STATE}`, { method: 'GET', From 7f28455de5c707a6fed320f25375006dc3315c26 Mon Sep 17 00:00:00 2001 From: Yoshio HANAWA Date: Thu, 9 Jan 2025 00:19:11 +0900 Subject: [PATCH 2/3] Apply suggestions from code review Change variable names to camelCase Co-authored-by: tempepe --- packages/oidc-auth/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/oidc-auth/src/index.ts b/packages/oidc-auth/src/index.ts index 5bc9906fd..f1079e050 100644 --- a/packages/oidc-auth/src/index.ts +++ b/packages/oidc-auth/src/index.ts @@ -277,9 +277,9 @@ const generateAuthorizationRequestUrl = async ( const as = await getAuthorizationServer(c) const client = getClient(c) const authorizationRequestUrl = new URL(as.authorization_endpoint!) - const redirect_uri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString() + const redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString() authorizationRequestUrl.searchParams.set('client_id', client.client_id) - authorizationRequestUrl.searchParams.set('redirect_uri', redirect_uri) + authorizationRequestUrl.searchParams.set('redirect_uri', redirectUri) authorizationRequestUrl.searchParams.set('response_type', 'code') if (as.scopes_supported === undefined || as.scopes_supported.length === 0) { throw new HTTPException(500, { @@ -340,12 +340,12 @@ export const processOAuthCallback = async (c: Context) => { if (code === undefined || nonce === undefined || code_verifier === undefined) { throw new HTTPException(500, { message: 'Missing required parameters / cookies' }) } - const redirect_uri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString() + const redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString() const result = await exchangeAuthorizationCode( as, client, params, - redirect_uri, + redirectUri, nonce, code_verifier ) @@ -394,8 +394,8 @@ export const oidcAuthMiddleware = (): MiddlewareHandler => { return createMiddleware(async (c, next) => { const env = getOidcAuthEnv(c) const uri = new URL(c.req.url) - const redirect_uri = new URL(env.OIDC_REDIRECT_URI, c.req.url) - if (uri.pathname === redirect_uri.pathname && uri.origin === redirect_uri.origin) { + const redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url) + if (uri.pathname === redirectUri.pathname && uri.origin === redirectUri.origin) { return processOAuthCallback(c) } try { From 9163976d0144f8cb66e5b4f7aa9ad4e9c2ceadc9 Mon Sep 17 00:00:00 2001 From: Yoshio HANAWA Date: Thu, 9 Jan 2025 00:30:23 +0900 Subject: [PATCH 3/3] Update .changeset/kind-cheetahs-give.md --- .changeset/kind-cheetahs-give.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/kind-cheetahs-give.md b/.changeset/kind-cheetahs-give.md index da6683a68..3a546b1ec 100644 --- a/.changeset/kind-cheetahs-give.md +++ b/.changeset/kind-cheetahs-give.md @@ -1,5 +1,5 @@ --- -'@hono/oidc-auth': patch +'@hono/oidc-auth': minor --- Add support for absolute path in OIDC_REDIRECT_URI and set its default value to '/callback'