Authentication
This guide explains how to implement custom authentication in your Pika project using the authentication customization extension point.
Overview
Pika Framework provides a flexible authentication system that allows you to implement your own authentication logic while maintaining the framework's user management and chat functionality. Your custom authentication code is protected from framework updates and can handle complex flows like OAuth, SSO, and custom auth providers.
Critical Security Warning
THE DEFAULT MOCK AUTHENTICATION IS NOT SECURE
Pika Framework ships with a default mock authentication provider that:
- Hardcodes a single test user (
userId: '123', firstName: 'Test', lastName: 'User'
) - Requires no password or validation - anyone visiting your site is automatically authenticated
- Does not assign a
userType
- this can cause security issues with chat app access control - Returns the same mock user for every request
Production Deployment Requirements
- Replace the mock provider with your own authentication implementation
- Assign proper
userType
values (internal-user
orexternal-user
) to all users - Configure
userTypesAllowed
on all chat apps to control access - Test authentication flows thoroughly in staging environment
Security Risk
The mock provider creates a significant security vulnerability:
// CURRENT MOCK PROVIDER - INSECURE
async authenticate(_event: RequestEvent): Promise<AuthenticatedUser<MockAuthData, MockCustomData>> {
return {
userId: '123',
firstName: 'Test',
lastName: 'User',
userType: 'internal-user', // marked as internal user for development
// No actual authentication - anyone can access!
// ... rest of user data
};
}
Understanding User Data Types
The framework uses a two-tier data structure to separate authentication data from business data:
AuthenticatedUser<T, U>
T (Auth Data): Sensitive authentication information (tokens, sessions, etc.)
- Stored securely in encrypted cookies
- Never saved to database
- Available server-side only
- Not sent to agent or tools
- Auth Data must be of type
Record<string, string>
or undefined
U (Custom Data): Business-specific user information (company details, account info, etc.)
- Stored securely in encrypted cookies
- Saved to chat user database as
customData
- Available to agent tools (but NOT the agent itself)
- Persists across sessions
- Custom Data must be of type
Record<string, string>
or undefined
ChatUser <T>
- T (Custom Data): Same as the U type in
AuthenticatedUser< T, U>
- Contains business-specific user information
- Core fields:
userId
,firstName
,lastName
,userType
,role
,features
- Custom fields: stored in the
customData
property
This separation ensures sensitive auth data stays secure while allowing business data to be accessible where needed.
User Access Control Features
The framework provides two optional access control features you can leverage in your authentication implementation:
User Types (Internal vs External)
You can distinguish between different types of users by setting the userType
field:
internal-user
: Company employees, administrators, internal staffexternal-user
: Customers, partners, external users
Benefits:
- Control which chat apps are accessible to which user types
- Implement different UX flows for internal vs external users
- Apply different security policies
Usage in Chat Apps:
// Chat app only for internal users
const internalChatApp: ChatApp = {
chatAppId: 'internal-support',
title: 'Internal Support Chat',
userTypesAllowed: ['internal-user'] // Only internal users can access
// ... other properties
};
User Roles and Permissions
You can assign roles to users for advanced access control and administrative capabilities:
Special Pika Roles:
pika:content-admin
: Super-admin role that grants:- Ability to act as any other user in view only mode (impersonation)
- Access to view all chat sessions and messages (debugging)
- Administrative privileges across the platform
Custom Roles:
- You can define custom roles as plain strings for your business logic
- Important: Do not create custom roles starting with
pika:
(reserved for framework use) - Custom roles are not currently used by the framework but will be supported in future versions
Implementation Examples
Here are practical examples of how to implement user type and role logic in your authentication provider:
// Example: Determine user type based on email domain
function determineUserType(email: string, userData: any): UserType {
const companyDomains = ['yourcompany.com', 'corp.yourcompany.com'];
const emailDomain = email.split('@')[1];
if (companyDomains.includes(emailDomain)) {
return 'internal-user';
}
return 'external-user';
}
Chat App Access Control
Here's how to configure chat apps with user type restrictions:
// Internal-only chat app for employee support
const employeeChatApp: ChatApp = {
chatAppId: 'employee-support',
title: 'Employee IT Support',
userTypesAllowed: ['internal-user'], // Only employees can access
agentId: 'internal-support-agent',
mode: 'standalone',
enabled: true
// ... other properties
};
For comprehensive access control information, see the Chat App Access Control Guide for detailed explanations of all access rules, precedence order, override systems, and troubleshooting.
Production Deployment Checklist
Before deploying to production, ensure:
Security Requirement | Why It Matters | |
---|---|---|
Custom auth provider implemented | Mock provider has no security | |
All users have userType assigned | Controls chat app access | |
Chat apps have userTypesAllowed configured | Prevents unauthorized access | |
Internal tools restricted to ['internal-user'] | Protects admin functionality | |
Customer-facing apps restricted appropriately | Data privacy and access control |
User Type Quick Guide
// User type assignment for company employees
const user: AuthenticatedUser = {
// ... other fields
userType: 'internal-user', // Company employees, staff, admins
};
Chat App Access Control
// Restrict to internal users only (admin tools, employee resources)
const internalApp: ChatApp = {
chatAppId: 'admin-dashboard',
title: 'Admin Dashboard',
userTypesAllowed: ['internal-user'] // Only internal users
};
Common Security Mistakes
Bad Patterns to Avoid
// Missing userType assignment defaults to internal
const user: AuthenticatedUser = {
userId: 'user123',
firstName: 'John',
lastName: 'Doe'
// Missing userType = defaults to internal
};
Customization Location
Location: apps/pika-chat/src/lib/server/auth-provider/
Purpose: Implement custom authentication flows for your specific SSO or auth provider.
Protected: Yes - this directory is never overwritten by sync operations.
Cookie Size Management
The framework intelligently handles large authentication data by automatically splitting it across multiple cookies when needed:
- Single Cookie: For small user data (≤4KB), uses the standard
au
cookie - Multi-Cookie: For large user data (>4KB), splits data across multiple cookies:
au
- Contains metadata (part count, total size, timestamp)au_part_0
,au_part_1
, etc. - Contains encrypted data chunks
This allows you to store arbitrarily large authentication data without worrying about cookie size limits.
Generic Type System
The Pika authentication system uses TypeScript generics to provide type safety while allowing flexibility in your authentication data structure.
AuthProvider<T, U> Generic Parameters
When creating your authentication provider, you must extend AuthProvider<T, U>
where:
T (Auth Data Type): Contains authentication-specific data like tokens, session IDs, etc.
- Stored securely in encrypted cookies only
- Never saved to database
- Available server-side only
- Not accessible to agents or tools
U (Custom Data Type): Contains business-specific user information
- Stored in encrypted cookies AND database (
customData
field) - Available to agent tools (but NOT the agent itself)
- Persists across sessions
- Stored in encrypted cookies AND database (
Example Type Definitions
// T - Auth data (cookies only, not database)
interface MyAuthData {
accessToken: string;
refreshToken?: string;
expiresAt: number;
}
// U - Custom data (cookies + database)
interface MyCustomData {
companyId: string;
accountType: 'retailer' | 'supplier';
email: string;
}
Type Safety Benefits
- Compile-time checking: TypeScript ensures your auth data structure matches throughout your code
- IntelliSense support: IDEs provide accurate autocomplete for your specific auth data fields
- Refactoring safety: Changes to your auth data types are caught at compile time
- Clear contracts: The generic system makes it explicit what data is available where
Entity-Based Access Control Integration
Overview
The Pika framework provides entity-based access control through the dedicated Entity feature. This enables sophisticated access control where specific accounts, companies, or organizations can be granted or denied access to individual chat apps.
Entity Feature Configuration
Entity-based access control is configured declaratively in your pika-config.ts
:
export const pikaConfig: PikaConfig = {
siteFeatures: {
entity: {
enabled: true,
attributeName: 'accountId', // Field in user.customData containing entity identifier
searchPlaceholderText: 'Search for an account...',
displayNameSingular: 'Account',
displayNamePlural: 'Accounts',
tableColumnHeaderTitle: 'Account ID'
}
}
};
The attributeName
field specifies which field in the user's customData
should be used to match against entity access control lists defined in chat app overrides.
How Entity Matching Works
- Entity Feature Setup: Configure the
attributeName
in your entity feature settings (e.g.,'accountId'
) - Chat App Override Configuration: Admins can configure exclusive access lists for chat apps
- Access Check: Framework extracts the entity value from user's
customData
using the configuredattributeName
and checks against allowed entities - Access Granted/Denied: User gains access only if their entity is in the allowed list
Implementation Examples
// Entity feature configuration:
entity: {
enabled: true,
attributeName: 'accountId', // Will match against user's customData.accountId
// ... other config
}
// User's customData: { accountId: 'acct_123', email: 'user@company.com' }
// Chat app override allows: ['acct_123', 'acct_456']
// Result: User granted access because 'acct_123' is in the allowed list
Access Control Precedence
When chat app overrides are configured, the system follows this precedence:
- Disabled Chat App: If override sets
enabled: false
, no access regardless of other rules - Exclusive User ID Control: If
exclusiveUserIdAccessControl
is set, only those specific user IDs have access - Exclusive Entity Control:
- For internal users: Check
exclusiveInternalAccessControl
against user's entity - For external users: Check
exclusiveExternalAccessControl
against user's entity
- For internal users: Check
- General Access Rules: Fall back to standard
userTypes
/userRoles
checking
Complete Implementation Example
interface CompanyAuthData {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
interface CompanyCustomData {
email: string;
companyId: string;
accountId: string;
department: string;
}
export default class CompanyAuthProvider extends AuthProvider<CompanyAuthData, CompanyCustomData> {
async authenticate(event: RequestEvent): Promise<AuthenticateResult<CompanyAuthData, CompanyCustomData>> {
// Your authentication logic here
const token = this.extractToken(event);
const userData = await this.getUserData(token);
return {
authenticatedUser: {
userId: userData.id,
firstName: userData.firstName,
lastName: userData.lastName,
userType: userData.isEmployee ? 'internal-user' : 'external-user',
customData: {
email: userData.email,
companyId: userData.companyId,
accountId: userData.accountId, // This will be used for entity matching when configured in entity.attributeName
department: userData.department
},
authData: {
accessToken: token,
refreshToken: userData.refreshToken,
expiresAt: userData.expiresAt
},
features: {
instruction: { type: 'instruction', instruction: 'You are a helpful assistant.' },
history: { type: 'history', history: true }
}
}
};
}
// Optional: Display the current account context to users
async getCustomDataUiRepresentation(user: AuthenticatedUser<CompanyAuthData, CompanyCustomData>, chatAppId?: string): Promise<CustomDataUiRepresentation | undefined> {
const accountId = user.customData?.accountId;
if (!accountId) return undefined;
// Fetch account details for display
const accountDetails = await this.getAccountDetails(accountId);
return {
title: 'Current Account',
value: `${accountDetails.name} (${accountId})`
};
}
}
Use Cases for Entity-Based Access Control
// Different customers can only access their own support chat
// customData: { customerId: 'customer_123' }
// Chat app override: exclusiveExternalAccessControl: ['customer_123', 'customer_456']
Security Considerations
- Data Validation: Always validate that the custom data field exists and contains expected values
- Field Accessibility: Ensure the specified field path is always populated for users who need entity-based access
- Logging: Log entity matching decisions for audit trails
- Fallback Behavior: Consider what happens when entity data is missing or invalid
Complete Working Example
A fully functional authentication provider example is included at apps/pika-chat/src/lib/server/auth-provider/custom-example.ts
. This example demonstrates:
- Time-based validation (validates only every 5 minutes for performance)
- Multi-endpoint fallback (tries admin and regular endpoints)
- Comprehensive error handling and logging
- Generic, customizable structure for any HTTP-based auth system
- Cookie management and redirect handling
To use this example:
- Copy the contents of
custom-example.ts
- Paste into your
index.ts
file - Customize the URLs, types, and business logic for your auth provider
- Update the user lookup mechanism (database or API)
Implementation Steps
1. Create the Custom Auth Provider Directory
The customization directory should already exist, but if not:
mkdir -p apps/pika-chat/src/lib/server/auth-provider
2. Define Your Auth and Custom Data Types
Create type definitions for your authentication data and custom user data:
// apps/pika-chat/src/lib/server/auth-provider/types.ts
// Auth data - stored securely in cookies, never saved to database
// Available server-side only, not sent to agent or tools
export interface YourCustomAuthData {
accessToken: string;
refreshToken?: string;
expiresAt?: number;
// Add your auth-specific properties here (tokens, session data, etc.)
// ... any other auth-specific data (no size limit!)
}
// Custom data - saved to database with user record
// Available to agent tools (but NOT the agent itself)
export interface YourCustomUserData {
companyId?: string;
companyName?: string;
companyType?: 'retailer' | 'supplier';
email?: string;
accountId?: string;
// Add your custom user properties here
// ... any other user-specific data you want to persist
}
3. Implement Your Auth Provider
Create your main authentication provider. Your provider must extend the generic AuthProvider<T, U>
class where:
- T is your auth data type (stored securely in cookies, not database)
- U is your custom user data type (stored in database as
customData
)
Use extends AuthProvider<YourAuthType, YourCustomType>
not implements AuthProvider
.
// apps/pika-chat/src/lib/server/auth-provider/index.ts
import type { RequestEvent } from '@sveltejs/kit';
import type { AuthenticatedUser } from 'pika-shared/types/chatbot/chatbot-types';
import { AuthProvider, NotAuthenticatedError, ForceUserToReauthenticateError } from '../auth/types';
import { redirect } from '@sveltejs/kit';
import type { YourCustomAuthData, YourCustomUserData } from './types';
export default class YourAuthProvider extends AuthProvider<YourCustomAuthData, YourCustomUserData> {
async authenticate(event: RequestEvent): Promise<AuthenticateResult<YourCustomAuthData, YourCustomUserData>> {
// Check if this is an OAuth callback
if (event.url.pathname.startsWith('/oauth/callback')) {
return this.handleOAuthCallback(event);
}
// Check if this is a login route
if (event.url.pathname.startsWith('/auth/login')) {
return this.startOAuthFlow(event);
}
// Extract auth token from cookies or headers
const authToken = this.extractAuthToken(event);
if (!authToken) {
// No token found - redirect to login
return { redirectTo: redirect(302, '/login') };
}
try {
// Validate token and get user data
const userData = await this.getUserFromAuthProvider(authToken);
const user = this.createAuthenticatedUser(userData, authToken);
return { authenticatedUser: user };
} catch (error) {
// Token invalid or expired - throw NotAuthenticatedError
throw new NotAuthenticatedError('Invalid or expired token');
}
}
async validateUser(
event: RequestEvent,
user: AuthenticatedUser<YourCustomAuthData, YourCustomUserData>
): Promise<AuthenticatedUser<YourCustomAuthData, YourCustomUserData> | undefined> {
// Check if the user's access token is still valid
const accessToken = user.authData.accessToken;
try {
// Validate the token with your auth provider
const isValid = await this.validateTokenWithProvider(accessToken);
if (isValid) {
// Token is still valid - no action needed
return undefined;
}
// Token is invalid but we have a refresh token
if (user.authData.refreshToken) {
try {
const newTokens = await this.refreshTokens(user.authData.refreshToken);
const updatedUser = { ...user };
updatedUser.authData = {
...updatedUser.authData,
accessToken: newTokens.access_token,
refreshToken: newTokens.refresh_token,
expiresAt: newTokens.expires_at
};
return updatedUser;
} catch (refreshError) {
// Refresh failed - force re-authentication
throw new ForceUserToReauthenticateError('Token refresh failed');
}
}
// No refresh token available - force re-authentication
throw new ForceUserToReauthenticateError('Token expired and no refresh token available');
} catch (error) {
if (error instanceof ForceUserToReauthenticateError) {
throw error;
}
// Other errors - force re-authentication
throw new ForceUserToReauthenticateError('Token validation failed');
}
}
private extractAuthToken(event: RequestEvent): string | null {
// Extract token from cookies, headers, etc.
return event.cookies.get('your-auth-token') || event.request.headers.get('authorization')?.replace('Bearer ', '');
}
private async getUserFromAuthProvider(token: string): Promise<any> {
// Make API call to your auth provider to get user data
const response = await fetch('https://your-auth-provider.com/api/user', {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) {
throw new Error('Failed to get user data');
}
return response.json();
}
private createAuthenticatedUser(userData: any, token: string): AuthenticatedUser<YourCustomAuthData, YourCustomUserData> {
return {
userId: userData.id,
firstName: userData.firstName,
lastName: userData.lastName,
// User type for access control
userType: userData.isEmployee ? 'internal-user' : 'external-user',
// Roles for permissions and admin access
roles: userData.roles || [], // e.g., ['pika:content-admin', 'support-manager']
// Custom data - saved to database, available to agent tools
customData: {
email: userData.email,
companyId: userData.companyId,
companyName: userData.companyName,
companyType: userData.companyType,
accountId: userData.accountId
},
// Auth data - stored in secure cookie only, never saved to database
authData: {
accessToken: token,
refreshToken: userData.refreshToken,
expiresAt: userData.expiresAt
},
features: {
instruction: {
type: 'instruction',
instruction: 'You are a helpful assistant.'
},
history: {
type: 'history',
history: true
}
}
};
}
// Additional helper methods would go here...
}
Framework Integration
The framework automatically:
- Loads your custom provider when no user cookie exists
- Calls your
authenticate
method with the request event for initial authentication - Calls your
validateUser
method when a user cookie exists to validate/refresh tokens - Handles the responses:
- If
authenticate
returns aResponse
(redirect, OAuth flow), it returns that response - If
authenticate
returns anAuthenticatedUser<T, U>
, it sets cookies and continues - If
authenticate
throwsNotAuthenticatedError
, it redirects to login - If
validateUser
returnsundefined
, no action is taken - If
validateUser
returns anAuthenticatedUser<T, U>
, it updates the cookie - If
validateUser
throwsForceUserToReauthenticateError
, it redirects to login
- If
- Manages user creation/retrieval in the chat database
- Handles secure cookie storage with automatic size management
Data Storage and Access
The framework handles two types of user data:
- Auth Data (T): Stored securely in encrypted cookies, never saved to database. Available server-side only. Contains tokens, session data, etc.
- Custom Data (U): Saved to the chat user database as
customData
. Available to agent tools but NOT the agent itself. Contains business data like company info, account details, etc.
Token Validation Flow
When a user has an existing cookie, the framework:
- Parses the user cookie to get the current user data (including both auth data and custom data)
- Calls
validateUser(event, user)
with the currentAuthenticatedUser<T, U>
object - Handles the result:
undefined
→ Continue with existing user (no changes needed)AuthenticatedUser<T, U>
→ Update cookie with refreshed auth data and continueForceUserToReauthenticateError
→ Clear cookies and redirect to login
Client-Side Authentication Flow
For authentication flows that require client-side processing (such as OAuth popups, social login SDKs, or complex multi-step authentication), the framework provides a dedicated client-side authentication route and helper method.
Overview
The client-side authentication flow allows your auth provider to:
- Detect when client-side processing is needed in the
authenticate
method - Redirect to a protected client-side route (
/auth/client-auth
) - Pass data to the client using the
addValueToLocalsForRoute
method - Handle client-side authentication in the browser
- Return to server-side authentication once client-side processing is complete
Protected Client-Side Files
The framework provides protected files that won't be overwritten during sync operations:
Location: apps/pika-chat/src/routes/auth/client-auth/
+page.server.ts
: Server-side logic to access data passed from your auth provider+page.svelte
: Client-side component for handling authentication UI and logic
The addValueToLocalsForRoute Method
Your auth provider can implement the optional addValueToLocalsForRoute
method to pass data to the client-side authentication route:
async addValueToLocalsForRoute?(
event: RequestEvent,
user: AuthenticatedUser<T, U> | undefined
): Promise<Record<string, unknown> | undefined>
Parameters:
event
: The request event (so you can check the path)user
: The authenticated user (if there is one)
Returns: An object that will be added to the route's locals
object under the key customData
Implementation Example
// Example: OAuth with client-side popup handling
interface OAuthClientAuthData {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
interface OAuthClientUserData {
email: string;
provider: string;
}
export default class OAuthClientAuthProvider extends AuthProvider<OAuthClientAuthData, OAuthClientUserData> {
async authenticate(event: RequestEvent): Promise<AuthenticateResult<OAuthClientAuthData, OAuthClientUserData>> {
// Check if this is a callback from client-side auth
const authCode = event.url.searchParams.get('auth_code');
if (authCode) {
const tokens = await this.exchangeCodeForTokens(authCode);
const userData = await this.getUserFromProvider(tokens.access_token);
const user = this.createAuthenticatedUser(userData, tokens);
return { authenticatedUser: user };
}
// Check for existing token
const token = event.cookies.get('oauth-token');
if (token) {
try {
const userData = await this.getUserFromProvider(token);
const user = this.createAuthenticatedUser(userData, { access_token: token });
return { authenticatedUser: user };
} catch (error) {
event.cookies.delete('oauth-token');
}
}
// No valid token found - redirect to client-side auth
return { redirectTo: redirect(302, '/auth/client-auth') };
}
async addValueToLocalsForRoute(
event: RequestEvent,
user: AuthenticatedUser<OAuthClientAuthData, OAuthClientUserData> | undefined
): Promise<Record<string, unknown> | undefined> {
// Only add data for the client-auth route
if (event.url.pathname === '/auth/client-auth') {
return {
oauthClientId: process.env.OAUTH_CLIENT_ID,
oauthRedirectUri: `${event.url.origin}/auth/callback`,
oauthScopes: 'openid profile email',
authProviderUrl: 'https://your-oauth-provider.com/oauth/authorize'
};
}
return undefined;
}
}
Flow Sequence
- User visits protected page → Framework calls
authenticate()
- No valid token found →
authenticate()
returns redirect to/auth/client-auth
- Framework calls
addValueToLocalsForRoute()
→ Passes OAuth configuration data - User sees client-side auth page → JavaScript starts OAuth flow
- User completes OAuth → Redirects to
/auth/callback
- Callback redirects to main app → With
auth_code
parameter - Framework calls
authenticate()
again → Now withauth_code
parameter - Authentication completes → User is authenticated and redirected to original page
Use Cases
This pattern is ideal for:
- OAuth providers that require popup windows or complex client-side flows
- Social login SDKs (Facebook, Google, etc.) that need client-side initialization
- Multi-step authentication flows that require user interaction
- CAPTCHA or 2FA that needs to be handled in the browser
- Custom authentication widgets that require client-side processing
Security Considerations
- Validate all data passed from client to server
- Use secure redirect URIs and validate them on the server
- Implement proper state parameter validation for OAuth flows
- Set appropriate cookie security settings for your domain
- Handle authentication errors gracefully on both client and server
Example Use Cases
Before reviewing these examples, check out the complete working example at apps/pika-chat/src/lib/server/auth-provider/custom-example.ts
which includes time-based validation and multi-endpoint fallback patterns you can copy directly.
// Example: Google OAuth integration with token refresh
interface GoogleAuthData {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
interface GoogleUserData {
email: string;
domain?: string;
}
export default class GoogleAuthProvider extends AuthProvider<GoogleAuthData, GoogleUserData> {
async authenticate(event: RequestEvent): Promise<AuthenticateResult<GoogleAuthData, GoogleUserData>> {
const token = event.cookies.get('google-token');
if (!token) {
return { redirectTo: redirect(302, '/login') };
}
const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
}).then((r) => r.json());
const user = {
userId: userInfo.id,
firstName: userInfo.given_name,
lastName: userInfo.family_name,
// Determine user type based on Google Workspace domain
userType: userInfo.hd === 'yourcompany.com' ? 'internal-user' : 'external-user',
// Extract roles from Google groups or custom claims
roles: this.extractRolesFromGoogleUser(userInfo),
customData: {
email: userInfo.email,
domain: userInfo.hd // Google Workspace domain
},
authData: {
accessToken: token,
refreshToken: userInfo.refresh_token,
expiresAt: Date.now() + 3600000 // 1 hour
},
features: {
instruction: { type: 'instruction', instruction: 'You are a helpful assistant.' },
history: { type: 'history', history: true }
}
};
return { authenticatedUser: user };
}
async validateUser(event: RequestEvent, user: AuthenticatedUser<GoogleAuthData, GoogleUserData>): Promise<AuthenticateResult<GoogleAuthData, GoogleUserData> | undefined> {
const token = user.authData.accessToken;
try {
// Check if token is still valid
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
return undefined; // Token is still valid
}
// Token expired, try to refresh
if (user.authData.refreshToken) {
const newTokens = await this.refreshGoogleTokens(user.authData.refreshToken);
const updatedUser = { ...user };
updatedUser.authData = {
...updatedUser.authData,
accessToken: newTokens.access_token,
refreshToken: newTokens.refresh_token,
expiresAt: newTokens.expires_at
};
return updatedUser;
}
throw new ForceUserToReauthenticateError('Google token expired');
} catch (error) {
throw new ForceUserToReauthenticateError('Google token validation failed');
}
}
}
// Example: SAML SSO integration
interface SAMLAuthData {
sessionId: string;
assertion: string;
expiresAt: number;
}
interface SAMLUserData {
email: string;
department?: string;
organization?: string;
}
export default class SAMLAuthProvider extends AuthProvider<SAMLAuthData, SAMLUserData> {
async authenticate(event: RequestEvent): Promise<AuthenticateResult<SAMLAuthData, SAMLUserData>> {
const samlResponse = event.url.searchParams.get('SAMLResponse');
if (samlResponse) {
const userData = await this.validateSAMLResponse(samlResponse);
const user = this.createUserFromSAML(userData);
return { authenticatedUser: user };
}
// Check for existing session
const sessionId = event.cookies.get('saml-session');
if (sessionId) {
const userData = await this.getUserFromSession(sessionId);
const user = this.createUserFromSAML(userData);
return { authenticatedUser: user };
}
throw new NotAuthenticatedError('No valid SAML session');
}
async validateUser(event: RequestEvent, user: AuthenticatedUser<SAMLAuthData, SAMLUserData>): Promise<AuthenticatedUser<SAMLAuthData, SAMLUserData> | undefined> {
// For SAML, you might check if the session is still valid
const sessionValid = await this.validateSAMLSession(user.authData.sessionId);
if (sessionValid) {
return undefined; // Session is still valid
}
throw new ForceUserToReauthenticateError('SAML session expired');
}
private createUserFromSAML(userData: any): AuthenticatedUser<SAMLAuthData, SAMLUserData> {
return {
userId: userData.nameId,
firstName: userData.firstName,
lastName: userData.lastName,
customData: {
email: userData.email,
department: userData.department,
organization: userData.organization
},
authData: {
sessionId: userData.sessionId,
assertion: userData.assertion,
expiresAt: userData.expiresAt
},
features: {
instruction: { type: 'instruction', instruction: 'You are a helpful assistant.' },
history: { type: 'history', history: true }
}
};
}
}
// Example: JWT token validation with refresh
interface JWTAuthData {
accessToken: string;
refreshToken?: string;
tokenType: string;
expiresAt: number;
}
interface JWTUserData {
email: string;
scope: string[];
accountId: string;
}
export default class JWTAuthProvider extends AuthProvider<JWTAuthData, JWTUserData> {
async authenticate(event: RequestEvent): Promise<AuthenticateResult<JWTAuthData, JWTUserData>> {
const token = event.cookies.get('jwt-token');
if (!token) {
return { redirectTo: redirect(302, '/login') };
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userData = await this.getUserFromDatabase(decoded.userId);
const user = this.createUserFromDatabase(userData, token);
return { authenticatedUser: user };
} catch (error) {
// Token invalid or expired
event.cookies.delete('jwt-token');
throw new NotAuthenticatedError('Invalid or expired JWT token');
}
}
async validateUser(event: RequestEvent, user: AuthenticatedUser<JWTAuthData, JWTUserData>): Promise<AuthenticatedUser<JWTAuthData, JWTUserData> | undefined> {
const token = user.authData.accessToken;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Check if token is about to expire (within 5 minutes)
const expiresIn = decoded.exp * 1000 - Date.now();
if (expiresIn > 5 * 60 * 1000) {
return undefined; // Token is still valid
}
// Token is about to expire, refresh it
const newToken = await this.refreshJWTToken(user.userId);
const updatedUser = { ...user };
updatedUser.authData = {
...updatedUser.authData,
accessToken: newToken
};
return updatedUser;
} catch (error) {
throw new ForceUserToReauthenticateError('JWT token validation failed');
}
}
private createUserFromDatabase(userData: any, token: string): AuthenticatedUser<JWTAuthData, JWTUserData> {
return {
userId: userData.id,
firstName: userData.firstName,
lastName: userData.lastName,
customData: {
email: userData.email,
scope: userData.scope,
accountId: userData.accountId
},
authData: {
accessToken: token,
refreshToken: userData.refreshToken,
tokenType: 'Bearer',
expiresAt: userData.tokenExpiresAt
},
features: {
instruction: { type: 'instruction', instruction: 'You are a helpful assistant.' },
history: { type: 'history', history: true }
}
};
}
}
Best Practices for Access Control
User Types:
- Use clear, consistent logic to determine user types (e.g., email domain, explicit user properties)
- Document your user type assignment rules for your team
- Consider edge cases (e.g., contractors, partners) and how they should be classified
- Test access restrictions thoroughly for both user types
Roles:
- Only assign
pika:content-admin
to trusted administrators who need debugging access - Use descriptive names for custom roles that clearly indicate their purpose
- Avoid roles starting with
pika:
for custom business logic - Consider implementing role hierarchies in your business logic
- Document what each custom role represents for future development
Security Considerations:
- Validate user types and roles from your auth provider rather than trusting client data
- Implement proper authorization checks on the server side
- Log role assignments and changes for audit purposes
- Regularly review and audit admin role assignments
Best Practices
1. Error Handling
Always handle authentication errors gracefully. When extending AuthProvider<T, U>
, your methods are already properly typed:
async validateUser(event: RequestEvent, user: AuthenticatedUser<T, U>): Promise<AuthenticatedUser<T, U> | undefined> {
try {
// Your validation logic
const isValid = await this.validateToken(user.authData.accessToken);
if (isValid) {
return undefined; // No action needed
}
// Try to refresh
if (user.authData.refreshToken) {
const newTokens = await this.refreshTokens(user.authData.refreshToken);
return this.updateUserWithNewTokens(user, newTokens);
}
throw new ForceUserToReauthenticateError('Token expired and no refresh token');
} catch (error) {
if (error instanceof ForceUserToReauthenticateError) {
throw error;
}
// Other errors - force re-authentication
throw new ForceUserToReauthenticateError('Validation failed');
}
}
2. Token Refresh
Implement token refresh logic for long-lived sessions:
private async refreshTokenIfNeeded(token: string): Promise<string> {
if (this.isTokenExpired(token)) {
const refreshToken = this.getRefreshToken();
const newTokens = await this.refreshTokens(refreshToken);
return newTokens.access_token;
}
return token;
}
3. Security
- Always use HTTPS in production
- Implement CSRF protection for auth flows
- Validate all user data from external sources
- Use secure cookie settings
- Implement proper session management
4. Testing
Test your authentication provider thoroughly:
// Example test
describe('YourAuthProvider', () => {
it('should validate valid users', async () => {
const provider = new YourAuthProvider();
const mockEvent = createMockRequestEvent();
const mockUser = createMockUser();
const result = await provider.validateUser(mockEvent, mockUser);
expect(result).toBeUndefined(); // No action needed
});
});
Troubleshooting
Common Issues
Provider not detected: Ensure your provider is the default export
Type errors with generics: Common mistake - do NOT declare your own generic parameters:
// CORRECT - Use your concrete types
export default class MyProvider extends AuthProvider<MyAuthData, MyCustomData> {
- Type errors: Make sure your auth data types match your interface definitions
- Redirect loops: Check your authentication logic
- Cookie issues: Verify cookie settings and domain configuration
Debug Mode
Enable debug logging by setting the environment variable:
DEBUG_AUTH=true
This will log authentication flow details to help with troubleshooting.
Migration from Default Auth
If you're migrating from the default mock authentication:
- Create your custom auth provider
- Test thoroughly in development
- Deploy to staging environment
- Verify all authentication flows work
- Deploy to production
The framework will automatically use your custom provider once it's implemented.
Next Steps
- Choose your auth provider (OAuth, SAML, JWT, etc.)
- Implement the AuthProvider interface
- Test your authentication flow
- Deploy and monitor
For more complex authentication scenarios, consider implementing additional methods in your provider class to handle specific requirements.