Skip to content

Commit 2f716d6

Browse files
hnwmaemaemae3
andauthored
feat(oidc-auth): support absolute path for redirect URI (#926)
* feat(oidc-auth): support absolute path for redirect URI * Apply suggestions from code review Change variable names to camelCase Co-authored-by: tempepe <[email protected]> * Update .changeset/kind-cheetahs-give.md --------- Co-authored-by: tempepe <[email protected]>
1 parent c7b15e3 commit 2f716d6

File tree

4 files changed

+97
-12
lines changed

4 files changed

+97
-12
lines changed

.changeset/kind-cheetahs-give.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hono/oidc-auth': minor
3+
---
4+
5+
Add support for absolute path in OIDC_REDIRECT_URI and set its default value to '/callback'

packages/oidc-auth/README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The middleware requires the following environment variables to be set:
5151
| 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 |
5252
| 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 |
5353
| 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 |
54-
| 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 |
54+
| 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` |
5555
| OIDC_SCOPES | The scopes that should be used for the OIDC authentication | The server provided `scopes_supported` |
5656
| OIDC_COOKIE_PATH | The path to which the `oidc-auth` cookie is set. Restrict to not send it with every request to your domain | / |
5757
| 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:
6262
```typescript
6363
import { Hono } from 'hono'
6464
import { oidcAuthMiddleware, getAuth, revokeSession, processOAuthCallback } from '@hono/oidc-auth'
65-
6665
const app = new Hono()
6766

6867
app.get('/logout', async (c) => {

packages/oidc-auth/src/index.ts

+19-10
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ declare module 'hono' {
3434
}
3535
}
3636

37-
const defaultOidcAuthCookieName = 'oidc-auth'
37+
const defaultOidcRedirectUri = '/callback'
3838
const defaultOidcAuthCookiePath = '/'
39+
const defaultOidcAuthCookieName = 'oidc-auth'
3940
const defaultRefreshInterval = 15 * 60 // 15 minutes
4041
const defaultExpirationInterval = 60 * 60 * 24 // 1 day
4142

@@ -52,7 +53,7 @@ type OidcAuthEnv = {
5253
OIDC_ISSUER: string
5354
OIDC_CLIENT_ID: string
5455
OIDC_CLIENT_SECRET: string
55-
OIDC_REDIRECT_URI: string
56+
OIDC_REDIRECT_URI?: string
5657
OIDC_SCOPES?: string
5758
OIDC_COOKIE_PATH?: string
5859
OIDC_COOKIE_NAME?: string
@@ -83,8 +84,13 @@ const getOidcAuthEnv = (c: Context) => {
8384
if (oidcAuthEnv.OIDC_CLIENT_SECRET === undefined) {
8485
throw new HTTPException(500, { message: 'OIDC client secret is not provided' })
8586
}
86-
if (oidcAuthEnv.OIDC_REDIRECT_URI === undefined) {
87-
throw new HTTPException(500, { message: 'OIDC redirect URI is not provided' })
87+
oidcAuthEnv.OIDC_REDIRECT_URI = oidcAuthEnv.OIDC_REDIRECT_URI ?? defaultOidcRedirectUri
88+
if (!oidcAuthEnv.OIDC_REDIRECT_URI.startsWith('/')) {
89+
try {
90+
new URL(oidcAuthEnv.OIDC_REDIRECT_URI)
91+
} catch (e) {
92+
throw new HTTPException(500, { message: 'The OIDC redirect URI is invalid. It must be a full URL or an absolute path' })
93+
}
8894
}
8995
oidcAuthEnv.OIDC_COOKIE_PATH = oidcAuthEnv.OIDC_COOKIE_PATH ?? defaultOidcAuthCookiePath
9096
oidcAuthEnv.OIDC_COOKIE_NAME = oidcAuthEnv.OIDC_COOKIE_NAME ?? defaultOidcAuthCookieName
@@ -271,8 +277,9 @@ const generateAuthorizationRequestUrl = async (
271277
const as = await getAuthorizationServer(c)
272278
const client = getClient(c)
273279
const authorizationRequestUrl = new URL(as.authorization_endpoint!)
280+
const redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString()
274281
authorizationRequestUrl.searchParams.set('client_id', client.client_id)
275-
authorizationRequestUrl.searchParams.set('redirect_uri', env.OIDC_REDIRECT_URI)
282+
authorizationRequestUrl.searchParams.set('redirect_uri', redirectUri)
276283
authorizationRequestUrl.searchParams.set('response_type', 'code')
277284
if (as.scopes_supported === undefined || as.scopes_supported.length === 0) {
278285
throw new HTTPException(500, {
@@ -312,7 +319,7 @@ export const processOAuthCallback = async (c: Context) => {
312319

313320
// Parses the authorization response and validates the state parameter
314321
const state = getCookie(c, 'state')
315-
const path = new URL(env.OIDC_REDIRECT_URI).pathname
322+
const path = new URL(env.OIDC_REDIRECT_URI, c.req.url).pathname
316323
deleteCookie(c, 'state', { path })
317324
const currentUrl: URL = new URL(c.req.url)
318325
const params = oauth2.validateAuthResponse(as, client, currentUrl, state)
@@ -333,11 +340,12 @@ export const processOAuthCallback = async (c: Context) => {
333340
if (code === undefined || nonce === undefined || code_verifier === undefined) {
334341
throw new HTTPException(500, { message: 'Missing required parameters / cookies' })
335342
}
343+
const redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString()
336344
const result = await exchangeAuthorizationCode(
337345
as,
338346
client,
339347
params,
340-
env.OIDC_REDIRECT_URI,
348+
redirectUri,
341349
nonce,
342350
code_verifier
343351
)
@@ -385,14 +393,15 @@ const exchangeAuthorizationCode = async (
385393
export const oidcAuthMiddleware = (): MiddlewareHandler => {
386394
return createMiddleware(async (c, next) => {
387395
const env = getOidcAuthEnv(c)
388-
const uri = c.req.url.split('?')[0]
389-
if (uri === env.OIDC_REDIRECT_URI) {
396+
const uri = new URL(c.req.url)
397+
const redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url)
398+
if (uri.pathname === redirectUri.pathname && uri.origin === redirectUri.origin) {
390399
return processOAuthCallback(c)
391400
}
392401
try {
393402
const auth = await getAuth(c)
394403
if (auth === null) {
395-
const path = new URL(env.OIDC_REDIRECT_URI).pathname
404+
const path = new URL(env.OIDC_REDIRECT_URI, c.req.url).pathname
396405
const cookieDomain = env.OIDC_COOKIE_DOMAIN
397406
// Redirect to IdP for login
398407
const state = oauth2.generateRandomState()

packages/oidc-auth/test/index.test.ts

+72
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,66 @@ describe('oidcAuthMiddleware()', () => {
281281
expect(res.status).toBe(302)
282282
expect(res.headers.get('set-cookie')).toMatch(new RegExp('oidc-auth=; Max-Age=0; Path=/($|,)'))
283283
})
284+
test('Should return an error when OIDC_ISSUER is undefined', async () => {
285+
delete process.env.OIDC_ISSUER
286+
const req = new Request('http://localhost/', {
287+
method: 'GET',
288+
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
289+
})
290+
const res = await app.request(req, {}, {})
291+
expect(res).not.toBeNull()
292+
expect(res.status).toBe(500)
293+
})
294+
test('Should return an error when OIDC_CLIENT_ID is undefined', async () => {
295+
delete process.env.OIDC_CLIENT_ID
296+
const req = new Request('http://localhost/', {
297+
method: 'GET',
298+
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
299+
})
300+
const res = await app.request(req, {}, {})
301+
expect(res).not.toBeNull()
302+
expect(res.status).toBe(500)
303+
})
304+
test('Should return an error when OIDC_CLIENT_SECRET is undefined', async () => {
305+
delete process.env.OIDC_CLIENT_SECRET
306+
const req = new Request('http://localhost/', {
307+
method: 'GET',
308+
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
309+
})
310+
const res = await app.request(req, {}, {})
311+
expect(res).not.toBeNull()
312+
expect(res.status).toBe(500)
313+
})
314+
test('Should return an error when OIDC_REDIRECT_URI is a relative path', async () => {
315+
process.env.OIDC_REDIRECT_URI = '../callback'
316+
const req = new Request('http://localhost/', {
317+
method: 'GET',
318+
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
319+
})
320+
const res = await app.request(req, {}, {})
321+
expect(res).not.toBeNull()
322+
expect(res.status).toBe(500)
323+
})
324+
test('Should return an error when OIDC_AUTH_SECRET is undefined', async () => {
325+
delete process.env.OIDC_AUTH_SECRET
326+
const req = new Request('http://localhost/', {
327+
method: 'GET',
328+
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
329+
})
330+
const res = await app.request(req, {}, {})
331+
expect(res).not.toBeNull()
332+
expect(res.status).toBe(500)
333+
})
334+
test('Should return an error when OIDC_AUTH_SECRET is too short', async () => {
335+
process.env.OIDC_AUTH_SECRET = '1234567890123456789012345678901'
336+
const req = new Request('http://localhost/', {
337+
method: 'GET',
338+
headers: { cookie: `oidc-auth=${MOCK_JWT_ACTIVE_SESSION}` },
339+
})
340+
const res = await app.request(req, {}, {})
341+
expect(res).not.toBeNull()
342+
expect(res.status).toBe(500)
343+
})
284344
test('Should Domain attribute of the cookie not set if env value not defined', async () => {
285345
const req = new Request('http://localhost/', {
286346
method: 'GET',
@@ -338,6 +398,18 @@ describe('processOAuthCallback()', () => {
338398
expect(res.status).toBe(302)
339399
expect(res.headers.get('location')).toBe('http://localhost/1234')
340400
})
401+
test('Verify default callback path when OIDC_REDIRECT_URI is undefined', async () => {
402+
delete process.env.OIDC_REDIRECT_URI
403+
const req = new Request(`${MOCK_REDIRECT_URI}?code=1234&state=${MOCK_STATE}`, {
404+
method: 'GET',
405+
headers: {
406+
cookie: `state=${MOCK_STATE}; nonce=${MOCK_NONCE}; code_verifier=1234; continue=http%3A%2F%2Flocalhost%2F1234`,
407+
},
408+
})
409+
const res = await app.request(req, {}, {})
410+
expect(res).not.toBeNull()
411+
expect(res.status).toBe(302)
412+
})
341413
test('Should respond with customized claims', async () => {
342414
const req = new Request(`${MOCK_REDIRECT_URI}-custom?code=1234&state=${MOCK_STATE}`, {
343415
method: 'GET',

0 commit comments

Comments
 (0)