OAuth Event Handlers
Define OAuth provider endpoints using event handler functions.
Overview
Nuxt Aegis provides specialized event handlers for each OAuth provider. These handlers create dual-purpose endpoints that handle both OAuth initiation and callback.
The shared handlers automatically protect every login, including Mock, with a generated state and an independent per-attempt proof cookie. Callbacks containing code or error require a valid pending transaction before token exchange or authentication hooks. Unsolicited callbacks, replay, malformed/repeated state, and missing/wrong proof are rejected. Provider denial consumes a correlated transaction; a token-exchange failure requires a fresh login.
An initial redirectTo must be a local application path. External origins, protocol-relative paths, backslashes, and control characters are rejected. The destination is restored from storage, ignoring callback redirect metadata. An initial application state is stored as metadata only, not sent as security state or exposed through a new callback API. authorizationParams.state is protected.
Manual HTTP clients must retain the initiation proof cookie and send it only to the application's provider callback, never to the OAuth provider. Browser navigation does this automatically. Cookie-free CLI OAuth is not yet provided; issued JWTs remain usable through cookie-free bearer requests. See single-process storage limits.
Callbacks receive the portable RequestEvent from nuxt/server. Use event.req, event.url, and event.res.headers instead of event.node or event.path. Import helpers explicitly from nuxt/server, even inside Aegis callbacks.
onError(event, error) receives a NuxtErrorLike with a numeric status, optional statusText, and the original exception as cause when it is normalized. It may return a string, a Web Response, or nothing, synchronously or asynchronously. Return redirect bodies rather than discarding them:
import { sendRedirect } from 'nuxt/server'
export default defineOAuthGoogleEventHandler({
onError: (event, error) => {
console.error('OAuth failed:', error.status)
return sendRedirect(event, '/auth/callback?error=authentication_failed')
},
})Provider Event Handlers
defineOAuthGoogleEventHandler(config)
Define a Google OAuth event handler.
Type Signature:
function defineOAuthGoogleEventHandler(config: {
config?: {
clientId?: string
clientSecret?: string
scope?: string[]
authorizationParams?: Record<string, string>
}
customClaims?: Record<string, unknown> | CustomClaimsCallback
onUserInfo?: UserInfoCallback
onSuccess?: SuccessCallback
}): EventHandlerExample:
// server/routes/auth/google.get.ts
export default defineOAuthGoogleEventHandler({
config: {
scope: ['openid', 'email', 'profile'],
authorizationParams: {
access_type: 'offline',
prompt: 'consent',
},
},
customClaims: async (user, tokens) => {
return {
role: await getUserRole(user.email),
premium: await checkPremium(user.email),
}
},
onSuccess: async ({ user, provider }) => {
console.log('Google login:', user.email)
},
})defineOAuthAuth0EventHandler(config)
Define an Auth0 Universal Login event handler.
Type Signature:
function defineOAuthAuth0EventHandler(config: {
config?: {
domain?: string
clientId?: string
clientSecret?: string
audience?: string
scope?: string[]
authorizationParams?: Record<string, string>
}
customClaims?: Record<string, unknown> | CustomClaimsCallback
onUserInfo?: UserInfoCallback
onSuccess?: SuccessCallback
}): EventHandlerExample:
// server/routes/auth/auth0.get.ts
export default defineOAuthAuth0EventHandler({
config: {
scope: ['openid', 'email', 'profile'],
authorizationParams: {
prompt: 'login',
screen_hint: 'signup',
},
},
customClaims: {
role: 'user',
accountType: 'standard',
},
})defineOAuthGitHubEventHandler(config)
Define a GitHub OAuth event handler.
Type Signature:
function defineOAuthGitHubEventHandler(config: {
config?: {
clientId?: string
clientSecret?: string
scope?: string[]
authorizationParams?: Record<string, string>
}
customClaims?: Record<string, unknown> | CustomClaimsCallback
onUserInfo?: UserInfoCallback
onSuccess?: SuccessCallback
}): EventHandlerExample:
// server/routes/auth/github.get.ts
export default defineOAuthGitHubEventHandler({
config: {
scope: ['user:email', 'read:user'],
authorizationParams: {
allow_signup: 'true',
},
},
customClaims: async (user, tokens) => {
return {
username: user.login,
isVerified: user.verified,
}
},
})defineOAuthMockEventHandler(config)
Define a Mock OAuth event handler for development/testing.
Type Signature:
function defineOAuthMockEventHandler(config: {
config: {
clientId: string
clientSecret: string
mockUsers: MockUser[]
defaultUser?: number
enableInProduction?: boolean
}
customClaims?: Record<string, unknown> | CustomClaimsCallback
onUserInfo?: UserInfoCallback
onSuccess?: SuccessCallback
}): EventHandlerExample:
// server/routes/auth/mock.get.ts
export default defineOAuthMockEventHandler({
config: {
clientId: 'mock-client',
clientSecret: 'mock-secret',
mockUsers: [
{
sub: 'mock:admin',
email: 'admin@example.com',
name: 'Admin User',
role: 'admin',
},
{
sub: 'mock:user',
email: 'user@example.com',
name: 'Regular User',
role: 'user',
},
],
defaultUser: 0,
},
})Configuration Options
Common Options
| Option | Type | Description |
|---|---|---|
config | object | Provider-specific configuration |
customClaims | object | function | Custom JWT claims |
onUserInfo | function | User info transformation callback |
onSuccess | function | Success callback |
Config Object
Provider-specific configuration:
interface Config {
clientId?: string
clientSecret?: string
scope?: string[]
authorizationParams?: Record<string, string>
// ... provider-specific options
}Custom Claims
Static claims:
customClaims: {
role: 'admin',
premium: true,
}Dynamic claims:
customClaims: async (user, tokens) => {
return {
role: await getUserRole(user.email),
permissions: await getPermissions(user.email),
}
}Callbacks
UserInfoCallback
Transform user data after fetching from OAuth provider:
type UserInfoCallback = (
user: Record<string, unknown>,
tokens: {
access_token: string
refresh_token?: string
id_token?: string
expires_in?: number
},
event: RequestEvent
) => Record<string, unknown> | Promise<Record<string, unknown>>Example:
onUserInfo: async (user, tokens, event) => {
// Add custom fields
user.customField = 'value'
// Normalize data
if (user.avatar_url) {
user.picture = user.avatar_url
delete user.avatar_url
}
return user
}SuccessCallback
Handle successful authentication:
type SuccessCallback = (payload: {
user: Record<string, unknown>
tokens: {
access_token: string
refresh_token?: string
id_token?: string
expires_in?: number
}
provider: string
event: RequestEvent
}) => void | Promise<void>Example:
onSuccess: async ({ user, provider, event }) => {
// Save to database
await db.users.upsert({
where: { email: user.email },
update: { lastLogin: new Date() },
create: {
id: user.sub,
email: user.email,
name: user.name,
provider,
},
})
// Send analytics
await analytics.track('user_login', {
userId: user.sub,
provider,
})
}Complete Example
// server/routes/auth/google.get.ts
export default defineOAuthGoogleEventHandler({
config: {
scope: ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/calendar.readonly'],
authorizationParams: {
access_type: 'offline',
prompt: 'consent',
hd: 'example.com', // Restrict to domain
},
},
customClaims: async (user, tokens) => {
// Fetch user profile from database
const profile = await db.users.findUnique({
where: { email: user.email },
include: { organization: true },
})
return {
role: profile?.role || 'user',
permissions: profile?.permissions || [],
organizationId: profile?.organization?.id,
premium: profile?.premium || false,
}
},
onUserInfo: async (user, tokens, event) => {
// Fetch additional data from Google
const calendar = await $fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
})
user.hasCalendar = calendar.items.length > 0
return user
},
onSuccess: async ({ user, provider, event }) => {
// Update user in database
await db.users.upsert({
where: { email: user.email },
update: {
name: user.name,
picture: user.picture,
lastLogin: new Date(),
},
create: {
id: user.sub,
email: user.email,
name: user.name,
picture: user.picture,
provider,
createdAt: new Date(),
},
})
// Send welcome email for new users
const isNewUser = await db.users.count({
where: { email: user.email },
}) === 1
if (isNewUser) {
await sendWelcomeEmail(user.email)
}
// Track analytics
await analytics.track('user_authenticated', {
userId: user.sub,
provider,
isNewUser,
})
},
})