Logic Handlers
Handlers allow you to customize the core logic of Nuxt Aegis, such as transforming user data, persisting to a database, generating custom claims, or handling impersonation checks. Unlike hooks, handlers are expected to return data that affects the authentication flow.
Overview
Handlers are defined using defineAegisHandler in a server plugin. This function is auto-imported in your server directory.
// server/plugins/aegis.ts
export default defineNitroPlugin((nitroApp) => {
defineAegisHandler({
// ... handler configuration
})
})Available Handlers
onUserInfo
Called after fetching user information from the OAuth provider, but before persisting to database.
Use cases:
- Normalize user data across different providers
- Transform provider-specific fields to common format
- Add metadata (timestamps, provider name, etc.)
Signature:(payload: UserInfoHookPayload) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined
Example:
defineAegisHandler({
onUserInfo: async (payload) => {
// Normalize user data
payload.providerUserInfo.authenticatedAt = new Date().toISOString()
payload.providerUserInfo.authProvider = payload.provider
// Return the modified user object to use it
return payload.providerUserInfo
}
})onUserPersist
Unified handler for database persistence across all authentication methods (OAuth and password).
Called after onUserInfo transformation. This is where you persist user data to your database and return enriched information to be included in JWT claims.
Use cases:
- Create or update user records in your database
- Link OAuth providers to existing accounts
- Update last login timestamps in a database
- Return database-specific fields (internal user ID, role, permissions)
Signature:(user: Record<string, unknown>, context: { provider: string, event: H3Event }) => Promise<Record<string, unknown>> | Record<string, unknown>
Parameters:
user- User data with provider-specific fields (includeshashedPasswordfor password provider)context.provider- Provider name ('google','github','password', etc.)context.event- H3Event for server context
Returns: Enriched user object with database fields (userId, role, permissions, etc.) that will be merged into the user data.
Example:
defineAegisHandler({
onUserPersist: async (user, { provider, event }) => {
const email = user.email as string
// Handle password authentication
if (provider === 'password') {
const hashedPassword = user.hashedPassword as string
const dbUser = await db.users.upsert({
where: { email },
update: { hashedPassword },
create: { email, hashedPassword, role: 'user' },
})
return { userId: dbUser.id, role: dbUser.role, permissions: dbUser.permissions }
}
// Handle OAuth providers
const providerId = String(user.id || user.sub)
let dbUser = await db.users.findByProvider(provider, providerId)
if (dbUser) {
// Update last login
await db.users.update(dbUser.id, { lastLogin: new Date() })
} else {
// Create or link user
const existingUser = await db.users.findByEmail(email)
if (existingUser) {
await db.users.linkProvider(existingUser.id, provider, providerId)
dbUser = existingUser
} else {
dbUser = await db.users.create({
email,
name: user.name,
picture: user.picture,
providers: [{ name: provider, id: providerId }],
})
}
}
// Return enriched data for JWT claims
return {
userId: dbUser.id,
role: dbUser.role,
permissions: dbUser.permissions,
organizationId: dbUser.organizationId,
}
}
})TIP
The returned object from onUserPersist is merged into the user data. This means you can selectively add or override fields without replacing the entire object.
customClaims
Handler-level custom claims generation (recommended for database-driven claims).
Called after onUserPersist, receives the merged user data. This is the recommended location for adding database-driven claims to your JWTs.
Use cases:
- Add role and permissions from database to JWT
- Include organization/tenant information
- Add subscription tier or feature flags
Signature:(user: Record<string, unknown>) => Promise<Record<string, unknown>> | Record<string, unknown>
Priority: Provider-level customClaims (defined in route handlers) take precedence over handler-level customClaims.
Example:
defineAegisHandler({
customClaims: async (user) => {
return {
role: user.role,
permissions: user.permissions,
organizationId: user.organizationId,
}
}
})Integration with onUserPersist
The customClaims handler receives data from onUserPersist. This separation allows you to:
- Persist data in
onUserPersist(runs for all auth methods) - Transform that data into JWT claims in
customClaims
impersonation.canImpersonate
Determines if a user is allowed to impersonate another user.
Use cases:
- Restrict impersonation to specific roles (e.g. 'admin', 'support')
- Implement complex permission logic
Signature:(requester: BaseTokenClaims, targetId: string, event: H3Event) => Promise<boolean> | boolean
Example:
defineAegisHandler({
impersonation: {
canImpersonate: async (requester, targetId, event) => {
// Only allow admins to impersonate
return requester.role === 'admin'
}
}
})impersonation.fetchTarget
Retrieves the target user's data when starting an impersonation session.
Use cases:
- Fetch user details from your database
- Validate that the target user exists
- Enforce organization boundaries (e.g. admin can only impersonate users in their org)
Signature:(targetId: string, event: H3Event) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null
Example:
defineAegisHandler({
impersonation: {
fetchTarget: async (targetId, event) => {
const user = await db.users.findById(targetId)
if (!user) return null
return {
sub: user.id,
email: user.email,
name: user.name,
role: user.role
}
}
}
})