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): support absolute path for redirect URI #926

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/kind-cheetahs-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/oidc-auth': minor
---

Add support for absolute path in OIDC_REDIRECT_URI and set its default value to '/callback'
3 changes: 1 addition & 2 deletions packages/oidc-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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) => {
Expand Down
29 changes: 19 additions & 10 deletions packages/oidc-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -271,8 +277,9 @@ const generateAuthorizationRequestUrl = async (
const as = await getAuthorizationServer(c)
const client = getClient(c)
const authorizationRequestUrl = new URL(as.authorization_endpoint!)
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', env.OIDC_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, {
Expand Down Expand Up @@ -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)
Expand All @@ -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 redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url).toString()
const result = await exchangeAuthorizationCode(
as,
client,
params,
env.OIDC_REDIRECT_URI,
redirectUri,
nonce,
code_verifier
)
Expand Down Expand Up @@ -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 redirectUri = new URL(env.OIDC_REDIRECT_URI, c.req.url)
if (uri.pathname === redirectUri.pathname && uri.origin === redirectUri.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()
Expand Down
72 changes: 72 additions & 0 deletions packages/oidc-auth/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Loading