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 <maekawa@tempepe.com> * Update .changeset/kind-cheetahs-give.md --------- Co-authored-by: tempepe <maekawa@tempepe.com>pull/929/head
parent
c7b15e365d
commit
2f716d619d
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/oidc-auth': minor
|
||||
---
|
||||
|
||||
Add support for absolute path in OIDC_REDIRECT_URI and set its default value to '/callback'
|
|
@ -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) => {
|
||||
|
|
|
@ -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 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, {
|
||||
|
@ -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 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
|
||||
)
|
||||
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue