Skip to content

Commit

Permalink
feat(config): ✨ Proxy support for fetching and refreshing tokens
Browse files Browse the repository at this point in the history
closes #44
  • Loading branch information
itpropro committed Jan 1, 2025
1 parent 2926948 commit 9dd0e6a
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 15 deletions.
4 changes: 3 additions & 1 deletion src/runtime/providers/auth0.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ofetch } from 'ofetch'
import { normalizeURL, withHttps, withoutTrailingSlash } from 'ufo'
import { createProviderFetch } from '../server/utils/oidc'
import { defineOidcProvider } from '../server/utils/provider'

interface Auth0ProviderConfig {
Expand Down Expand Up @@ -53,7 +54,8 @@ export const auth0 = defineOidcProvider<Auth0ProviderConfig, Auth0RequiredFields
],
async openIdConfiguration(config: any) {
const baseUrl = normalizeURL(withoutTrailingSlash(withHttps(config.baseUrl as string)))
return await ofetch(`${baseUrl}/.well-known/openid-configuration`)
const customFetch = createProviderFetch(config)
return await customFetch(`${baseUrl}/.well-known/openid-configuration`)
},
validateAccessToken: true,
validateIdToken: false,
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/providers/entra.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ofetch } from 'ofetch'
import { parseURL } from 'ufo'
import { createProviderFetch } from '../server/utils/oidc'
import { defineOidcProvider } from '../server/utils/provider'

type EntraIdRequiredFields = 'clientId' | 'clientSecret' | 'authorizationUrl' | 'tokenUrl' | 'redirectUri'
Expand Down Expand Up @@ -55,7 +56,8 @@ export const entra = defineOidcProvider<EntraProviderConfig, EntraIdRequiredFiel
async openIdConfiguration(config: any) {
const parsedUrl = parseURL(config.authorizationUrl)
const tenantId = parsedUrl.pathname.split('/')[1]
const openIdConfig = await ofetch(`https://${parsedUrl.host}/${tenantId}/.well-known/openid-configuration${config.audience && `?appid=${config.audience}`}`)
const customFetch = createProviderFetch(config)
const openIdConfig = await customFetch(`https://${parsedUrl.host}/${tenantId}/.well-known/openid-configuration${config.audience && `?appid=${config.audience}`}`)
openIdConfig.issuer = [`https://${parsedUrl.host}/${tenantId}/v2.0`, openIdConfig.issuer]
return openIdConfig
},
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/providers/keycloak.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ofetch } from 'ofetch'
import { generateProviderUrl } from '../server/utils/config'
import { createProviderFetch } from '../server/utils/oidc'
import { defineOidcProvider } from '../server/utils/provider'

type KeycloakRequiredFields = 'baseUrl' | 'clientId' | 'clientSecret' | 'redirectUri'
Expand Down Expand Up @@ -59,6 +60,7 @@ export const keycloak = defineOidcProvider<KeycloakProviderConfig, KeycloakRequi
logoutRedirectParameterName: 'post_logout_redirect_uri',
async openIdConfiguration(config: any) {
const configUrl = generateProviderUrl(config.baseUrl, '.well-known/openid-configuration')
return await ofetch(configUrl)
const customFetch = createProviderFetch(config)
return await customFetch(configUrl)
},
})
4 changes: 3 additions & 1 deletion src/runtime/providers/zitadel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ofetch } from 'ofetch'
import { normalizeURL, withHttps, withoutTrailingSlash } from 'ufo'
import { createProviderFetch } from '../server/utils/oidc'
import { defineOidcProvider, type OidcProviderConfig } from '../server/utils/provider'

type ZitadelRequiredFields = 'baseUrl' | 'clientId' | 'clientSecret'
Expand Down Expand Up @@ -37,7 +38,8 @@ export const zitadel = defineOidcProvider<OidcProviderConfig, ZitadelRequiredFie
logoutRedirectParameterName: 'post_logout_redirect_uri',
async openIdConfiguration(config: any) {
const baseUrl = normalizeURL(withoutTrailingSlash(withHttps(config.baseUrl as string)))
return await ofetch(`${baseUrl}/.well-known/openid-configuration`)
const customFetch = createProviderFetch(config)
return await customFetch(`${baseUrl}/.well-known/openid-configuration`)
},
excludeOfflineScopeFromTokenRequest: true,
})
25 changes: 17 additions & 8 deletions src/runtime/server/handler/callback.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import type { H3Event } from 'h3'
import type { OAuthConfig, PersistentSession, ProviderKeys, TokenRequest, TokenRespose, Tokens, UserSession } from '../../types'
import type { OidcProviderConfig } from '../utils/provider'
// @ts-expect-error - Missing Nitro type exports in Nuxt
import { useRuntimeConfig, useStorage } from '#imports'
import { deleteCookie, eventHandler, getQuery, getRequestURL, readBody, sendRedirect } from 'h3'
import { ofetch } from 'ofetch'
import { normalizeURL, parseURL } from 'ufo'
import { textToBase64 } from 'undio'
import * as providerPresets from '../../providers'
import { validateConfig } from '../utils/config'
import { configMerger, convertObjectToSnakeCase, convertTokenRequestToType, oidcErrorHandler, useOidcLogger } from '../utils/oidc'
import { configMerger, convertObjectToSnakeCase, convertTokenRequestToType, createProviderFetch, oidcErrorHandler, useOidcLogger } from '../utils/oidc'
import { encryptToken, type JwtPayload, parseJwtToken, validateToken } from '../utils/security'
import { getUserSessionId, setUserSession, useAuthSession } from '../utils/session'
// @ts-expect-error - Missing Nitro type exports in Nuxt
import { useRuntimeConfig, useStorage } from '#imports'
import { textToBase64 } from 'undio'

function callbackEventHandler({ onSuccess }: OAuthConfig<UserSession>) {
const logger = useOidcLogger()
return eventHandler(async (event: H3Event) => {
const provider = event.path.split('/')[2] as ProviderKeys
const config = configMerger(useRuntimeConfig().oidc.providers[provider] as OidcProviderConfig, providerPresets[provider])

// Create custom fetch instance for this provider
const customFetch = createProviderFetch(config)

const validationResult = validateConfig(config, config.requiredProperties)

if (!validationResult.valid) {
Expand Down Expand Up @@ -84,7 +87,7 @@ function callbackEventHandler({ onSuccess }: OAuthConfig<UserSession>) {
// Make token request
let tokenResponse: TokenRespose
try {
tokenResponse = await ofetch(
tokenResponse = await customFetch(
config.tokenUrl,
{
method: 'POST',
Expand Down Expand Up @@ -126,7 +129,11 @@ function callbackEventHandler({ onSuccess }: OAuthConfig<UserSession>) {
}
if ([config.audience as string, config.clientId].some(audience => accessToken.aud?.includes(audience) || idToken?.aud?.includes(audience)) && (config.validateAccessToken || config.validateIdToken)) {
// Get OIDC configuration
const openIdConfiguration = (config.openIdConfiguration && typeof config.openIdConfiguration === 'object') ? config.openIdConfiguration : typeof config.openIdConfiguration === 'string' ? await ofetch(config.openIdConfiguration) : await (config.openIdConfiguration!)(config)
const openIdConfiguration = (config.openIdConfiguration && typeof config.openIdConfiguration === 'object')
? config.openIdConfiguration
: typeof config.openIdConfiguration === 'string'
? await customFetch(config.openIdConfiguration)
: await (config.openIdConfiguration!)(config)
const validationOptions = { jwksUri: openIdConfiguration.jwks_uri as string, ...openIdConfiguration.issuer && { issuer: openIdConfiguration.issuer as string }, ...config.audience && { audience: [config.audience, config.clientId] } }
try {
tokens = {
Expand Down Expand Up @@ -161,12 +168,14 @@ function callbackEventHandler({ onSuccess }: OAuthConfig<UserSession>) {
// Request userinfo
try {
if (config.userInfoUrl) {
const userInfoResult = await ofetch(config.userInfoUrl, {
const userInfoResult = await customFetch(config.userInfoUrl, {
headers: {
Authorization: `${tokenResponse.token_type} ${tokenResponse.access_token}`,
},
})
user.userInfo = config.filterUserInfo ? Object.fromEntries(Object.entries(userInfoResult).filter(([key]) => config.filterUserInfo?.includes(key))) : userInfoResult
user.userInfo = config.filterUserInfo
? Object.fromEntries(Object.entries(userInfoResult).filter(([key]) => config.filterUserInfo?.includes(key)))
: userInfoResult
}
}
catch (error) {
Expand Down
12 changes: 11 additions & 1 deletion src/runtime/server/utils/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { sendRedirect } from 'h3'
import { ofetch } from 'ofetch'
import { snakeCase } from 'scule'
import { normalizeURL } from 'ufo'
import { ProxyAgent } from 'undici'
import { textToBase64 } from 'undio'
import { parseJwtToken } from './security'
import { clearUserSession } from './session'
Expand All @@ -23,8 +24,17 @@ export const configMerger = createDefu((obj, key, value) => {
}
})

export function createProviderFetch(config: OidcProviderConfig) {
if (config.proxy) {
const proxyAgent = config.ignoreProxyCertificateErrors ? new ProxyAgent({ uri: config.proxy, requestTls: { rejectUnauthorized: false } }) : new ProxyAgent({ uri: config.proxy })
return ofetch.create({ dispatcher: proxyAgent })
}
return ofetch
}

export async function refreshAccessToken(refreshToken: string, config: OidcProviderConfig) {
const logger = useOidcLogger()
const customFetch = createProviderFetch(config)
// Construct request header object
const headers: HeadersInit = {}

Expand All @@ -45,7 +55,7 @@ export async function refreshAccessToken(refreshToken: string, config: OidcProvi
// Make refresh token request
let tokenResponse: TokenRespose
try {
tokenResponse = await ofetch(
tokenResponse = await customFetch(
config.tokenUrl,
{
method: 'POST',
Expand Down
14 changes: 14 additions & 0 deletions src/runtime/server/utils/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ export interface OidcProviderConfig {
* @default undefined
*/
sessionConfiguration?: ProviderSessionConfig
/**
* Proxy URL
* @default undefined
*/
proxy?: string
/**
* WARNING: Only enable this in development/testing environments!
* Enabling this option in production is a serious security risk as it bypasses SSL/TLS certificate validation
* when using a proxy, making your application vulnerable to man-in-the-middle attacks.
*
* Ignore certificate errors when using a proxy
* @default false
*/
ignoreProxyCertificateErrors?: boolean
}

// Cannot import from utils here, otherwise Nuxt will throw '[worker reload] [worker init] Cannot access 'configMerger' before initialization'
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/server/utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { H3Event, SessionConfig } from 'h3'
import type { AuthSession, AuthSessionConfig, PersistentSession, ProviderKeys, ProviderSessionConfig, UserSession } from '../../types'
import type { OidcProviderConfig } from './provider'
// @ts-expect-error - Missing Nitro type exports in Nuxt
import { useRuntimeConfig, useStorage } from '#imports'
import { defu } from 'defu'
import { createError, deleteCookie, sendRedirect, useSession } from 'h3'
import { createHooks } from 'hookable'
import * as providerPresets from '../../providers'
import { configMerger, refreshAccessToken, useOidcLogger } from './oidc'
import { decryptToken, encryptToken } from './security'
// @ts-expect-error - Missing Nitro type exports in Nuxt
import { useRuntimeConfig, useStorage } from '#imports'

const sessionName = 'nuxt-oidc-auth'
let sessionConfig: Pick<SessionConfig, 'name' | 'password'> & AuthSessionConfig
Expand Down

0 comments on commit 9dd0e6a

Please sign in to comment.