Skip to content

Commit 20c92ef

Browse files
committed
feat(oidc): ✨ Added per provider session configuration
1 parent 55230a2 commit 20c92ef

File tree

7 files changed

+171
-32
lines changed

7 files changed

+171
-32
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,18 +434,28 @@ export default defineNuxtConfig({
434434
| exposeIdToken | `boolean` (optional) | `false` | Expose raw id token to the client within session object
435435
| callbackRedirectUrl | `string` (optional) | `/` | Set a custom redirect url to redirect to after a successful callback
436436
| allowedClientAuthParameters | `string[]` (optional) | `[]` | List of allowed client-side user-added query parameters for the auth request
437+
| sessionConfiguration | `ProviderSessionConfig` (optional) | `{}` | Session configuration overrides, see [session](#session)
437438

438439
#### `session`
439440

440-
The following options are available for the session configuration.
441+
The following options are available for the global session configuration.
441442

442443
| Option | Type | Default | Description |
443444
| --- | --- | --- | --- |
444-
| expirationCheck | `boolean` | `true` | Check if session is expired based on access token exp |
445445
| automaticRefresh | `boolean` | `true` | Automatically refresh access token and session if refresh token is available (indicated by `canRefresh` property on user object) |
446+
| expirationCheck | `boolean` | `true` | Check if session is expired based on access token exp |
447+
| expirationThreshold | `number` | `0` | Amount of seconds before access token expiration to trigger automatic refresh |
446448
| maxAge | `number` | `60 * 60 * 24` (1 day) | Maximum auth session duration in seconds |
447449
| cookie | `` | `` | Additional cookie setting overrides for `sameSite` and `secure` |
448450

451+
The following options are available on every provider as overrides for the global session configuration.
452+
453+
| Option | Type | Default | Description |
454+
| --- | --- | --- | --- |
455+
| automaticRefresh | `boolean` | `true` | Check if session is expired based on access token exp |
456+
| expirationCheck | `boolean` | `true` | Automatically refresh access token and session if refresh token is available (indicated by `canRefresh` property on user object) |
457+
| expirationThreshold | `number` | `0` | Amount of seconds before access token expiration to trigger automatic refresh |
458+
449459
#### `middleware`
450460

451461
| Option | Type | Default | Description |

pnpm-lock.yaml

Lines changed: 109 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/module.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ export default defineNuxtModule<ModuleOptions>({
125125
})
126126
}
127127
else {
128-
if (options.defaultProvider && !options.middleware.customLoginPage) {
129-
extendRouteRules('/auth/login', {
130-
redirect: {
131-
to: `/auth/${options.defaultProvider}/login`,
132-
statusCode: 302,
133-
},
134-
})
128+
if (options.defaultProvider) {
129+
if (!options.middleware.customLoginPage) {
130+
extendRouteRules('/auth/login', {
131+
redirect: {
132+
to: `/auth/${options.defaultProvider}/login`,
133+
statusCode: 302,
134+
},
135+
})
136+
}
135137
extendRouteRules('/auth/logout', {
136138
redirect: {
137139
to: `/auth/${options.defaultProvider}/logout`,

src/runtime/server/api/refresh.post.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getUserSession, refreshUserSession, sessionHooks } from '../utils/sessi
44
export default eventHandler(async (event) => {
55
await getUserSession(event)
66
const session = await refreshUserSession(event)
7-
await sessionHooks.callHookParallel('refresh', session, event)
7+
if (session)
8+
await sessionHooks.callHookParallel('refresh', session, event)
89
return session
910
})

src/runtime/server/utils/provider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ProviderSessionConfig } from '../../types'
12
import { createDefu } from 'defu'
23

34
type MakePropertiesRequired<T, K extends keyof T> = T & Required<Pick<T, K>>
@@ -171,6 +172,11 @@ export interface OidcProviderConfig {
171172
* @default []
172173
*/
173174
allowedClientAuthParameters?: string[]
175+
/**
176+
* Session configuration overrides
177+
* @default {}
178+
*/
179+
sessionConfiguration: ProviderSessionConfig
174180
}
175181

176182
// Cannot import from utils here, otherwise Nuxt will throw '[worker reload] [worker init] Cannot access 'configMerger' before initialization'
@@ -211,6 +217,7 @@ export function defineOidcProvider<TConfig, TRequired extends keyof OidcProvider
211217
callbackRedirectUrl: '/',
212218
allowedClientAuthParameters: [],
213219
logoutUrl: '',
220+
sessionConfiguration: {},
214221
}
215222
const mergedConfig = configMerger(config, defaults)
216223
return mergedConfig as MakePropertiesRequired<Partial<typeof mergedConfig>, TRequired>

src/runtime/server/utils/session.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { H3Event, SessionConfig } from 'h3'
2-
import type { AuthSessionConfig, PersistentSession, ProviderKeys, UserSession } from '../../types'
2+
import type { AuthSessionConfig, PersistentSession, ProviderKeys, ProviderSessionConfig, UserSession } from '../../types'
33
import type { OidcProviderConfig } from './provider'
44
import { defu } from 'defu'
5-
import { createError, deleteCookie, useSession } from 'h3'
5+
import { createError, deleteCookie, sendRedirect, useSession } from 'h3'
66
import { createHooks } from 'hookable'
77
import * as providerPresets from '../../providers'
88
import { configMerger, refreshAccessToken, useOidcLogger } from './oidc'
@@ -11,7 +11,8 @@ import { decryptToken, encryptToken, parseJwtToken } from './security'
1111
import { useRuntimeConfig, useStorage } from '#imports'
1212

1313
const sessionName = 'nuxt-oidc-auth'
14-
let sessionConfig: SessionConfig & AuthSessionConfig
14+
let sessionConfig: Pick<SessionConfig, 'name' | 'password'> & AuthSessionConfig
15+
const providerSessionConfigs: Record<ProviderKeys, ProviderSessionConfig> = {} as any
1516

1617
export interface SessionHooks {
1718
/**
@@ -61,14 +62,10 @@ export async function refreshUserSession(event: H3Event) {
6162
const persistentSession = await useStorage('oidc').getItem<PersistentSession>(session.id as string) as PersistentSession | null
6263

6364
if (!session.data.canRefresh || !persistentSession?.refreshToken) {
64-
throw createError({
65-
statusCode: 500,
66-
message: 'No refresh token',
67-
})
65+
logger.warn('No refresh token')
66+
return
6867
}
6968

70-
await sessionHooks.callHookParallel('refresh', session.data, event)
71-
7269
// Refresh the access token
7370
const tokenKey = process.env.NUXT_OIDC_TOKEN_KEY as string
7471
const refreshToken = await decryptToken(persistentSession.refreshToken, tokenKey)
@@ -82,7 +79,7 @@ export async function refreshUserSession(event: H3Event) {
8279
}
8380
catch (error) {
8481
logger.error(error)
85-
await clearUserSession(event)
82+
return sendRedirect(event, '/auth/logout')
8683
}
8784

8885
const { user, tokens, expiresIn } = tokenRefreshResponse!
@@ -110,7 +107,8 @@ export async function requireUserSession(event: H3Event) {
110107

111108
export async function getUserSession(event: H3Event) {
112109
const logger = useOidcLogger()
113-
const userSession = (await _useSession(event)).data
110+
const session = await _useSession(event)
111+
const userSession = session.data
114112

115113
if (Object.keys(userSession).length === 0) {
116114
throw createError({
@@ -119,19 +117,21 @@ export async function getUserSession(event: H3Event) {
119117
})
120118
}
121119

120+
const provider = userSession.provider as ProviderKeys
121+
122122
// Expiration check
123-
if (sessionConfig.expirationCheck) {
124-
const sessionId = await getUserSessionId(event)
123+
if (providerSessionConfigs[provider]?.expirationCheck) {
124+
const sessionId = session.id
125125
const persistentSession = await useStorage('oidc').getItem<PersistentSession>(sessionId as string) as PersistentSession | null
126126
if (!persistentSession)
127127
logger.warn('Persistent user session not found')
128128

129129
let expired = true
130130
if (persistentSession) {
131-
expired = persistentSession?.exp <= (Math.trunc(Date.now() / 1000) + (sessionConfig.expirationThreshold && typeof sessionConfig.expirationThreshold === 'number' ? sessionConfig.expirationThreshold : 0))
131+
expired = persistentSession?.exp <= (Math.trunc(Date.now() / 1000) + (providerSessionConfigs[provider].expirationThreshold && typeof providerSessionConfigs[provider].expirationThreshold === 'number' ? providerSessionConfigs[provider].expirationThreshold : 0))
132132
}
133133
else if (userSession) {
134-
expired = userSession?.expireAt <= (Math.trunc(Date.now() / 1000) + (sessionConfig.expirationThreshold && typeof sessionConfig.expirationThreshold === 'number' ? sessionConfig.expirationThreshold : 0))
134+
expired = userSession?.expireAt <= (Math.trunc(Date.now() / 1000) + (providerSessionConfigs[provider].expirationThreshold && typeof providerSessionConfigs[provider].expirationThreshold === 'number' ? providerSessionConfigs[provider].expirationThreshold : 0))
135135
}
136136
else {
137137
throw createError({
@@ -142,7 +142,7 @@ export async function getUserSession(event: H3Event) {
142142
if (expired) {
143143
logger.info('Session expired')
144144
// Automatic token refresh
145-
if (sessionConfig.automaticRefresh) {
145+
if (providerSessionConfigs[provider].automaticRefresh) {
146146
await refreshUserSession(event)
147147
logger.info('Successfully refreshed token')
148148
return userSession
@@ -162,9 +162,19 @@ export async function getUserSessionId(event: H3Event) {
162162
}
163163

164164
function _useSession(event: H3Event) {
165-
if (!sessionConfig) {
166-
// @ts-expect-error - Type mismatch
167-
sessionConfig = defu({ password: process.env.NUXT_OIDC_SESSION_SECRET, name: sessionName }, useRuntimeConfig(event).oidc.session)
165+
if (!sessionConfig || !Object.keys(providerSessionConfigs).length) {
166+
// Merge sessionConfig
167+
sessionConfig = defu({ password: process.env.NUXT_OIDC_SESSION_SECRET!, name: sessionName }, useRuntimeConfig(event).oidc.session)
168+
// Merge providerSessionConfigs
169+
Object.keys(useRuntimeConfig(event).oidc.providers).map(
170+
key => key as ProviderKeys,
171+
).forEach(
172+
key => providerSessionConfigs[key] = defu(useRuntimeConfig(event).oidc.providers[key]?.sessionConfiguration, {
173+
automaticRefresh: useRuntimeConfig(event).oidc.session.automaticRefresh,
174+
expirationCheck: useRuntimeConfig(event).oidc.session.expirationCheck,
175+
expirationThreshold: useRuntimeConfig(event).oidc.session.expirationThreshold,
176+
}) as ProviderSessionConfig,
177+
)
168178
}
169179
return useSession<UserSession>(event, sessionConfig)
170180
}

src/runtime/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export interface AuthorizationResponse {
162162
}
163163

164164
export interface UserSession {
165-
provider?: ProviderKeysWithDev
165+
provider: ProviderKeysWithDev
166166
canRefresh: boolean
167167
loggedInAt?: number
168168
expireAt: number
@@ -217,3 +217,5 @@ export interface AuthSessionConfig {
217217
secure?: boolean | undefined
218218
}
219219
}
220+
221+
export interface ProviderSessionConfig extends Omit<AuthSessionConfig, 'maxAge' | 'cookie'> {}

0 commit comments

Comments
 (0)