Handle authentication errors gracefully in production
Robust error handling is critical for production authentication systems. This guide covers error types, handling patterns, and best practices for @warpy-auth-sdk/core.
Errors that occur during the authentication flow:
// Common authentication errors
type AuthenticationError =
| 'invalid_provider' // Unsupported provider type
| 'invalid_credentials' // Wrong client ID/secret
| 'invalid_token' // Expired or malformed magic link token
| 'invalid_code' // Wrong 2FA verification code
| 'csrf_token_mismatch' // CSRF validation failed
| 'redirect_uri_mismatch' // OAuth redirect URI doesn't match
| 'user_not_found' // User doesn't exist
| 'user_creation_failed' // Database error creating user
| 'captcha_failed'; // CAPTCHA verification failedErrors when retrieving or verifying sessions:
// Common session errors
type SessionError =
| 'no_session' // No session cookie present
| 'invalid_session' // JWT signature verification failed
| 'expired_session' // JWT token expired
| 'session_revoked'; // Session explicitly revokedErrors in SDK configuration:
// Common configuration errors
type ConfigurationError =
| 'missing_secret' // AUTH_SECRET not set
| 'weak_secret' // AUTH_SECRET too short
| 'invalid_provider' // Provider config missing required fields
| 'missing_callback'; // Required callback not providedThe authenticate() function returns errors in the result:
Check for errors after authentication
import { authenticate } from '@warpy-auth-sdk/core';
const result = await authenticate(config, request);
// Check for errors
if (result.error) {
console.error('Authentication failed:', result.error);
return Response.json(
{ error: result.error },
{ status: 401 }
);
}
// Check for redirect (OAuth flow)
if (result.redirectUrl) {
return Response.redirect(result.redirectUrl);
}
// Success - session available
if (result.session) {
const headers = new Headers();
if (result.cookies) {
result.cookies.forEach(cookie => {
headers.append('Set-Cookie', cookie);
});
}
return Response.json(
{ user: result.session.user },
{ headers }
);
}Handle specific error cases with custom logic:
Custom responses for different error types
import { authenticate } from '@warpy-auth-sdk/core';
import { logAuthEvent } from '@/lib/logging';
export async function POST(request: Request) {
try {
const result = await authenticate(config, request);
if (result.error) {
// Log error for monitoring
await logAuthEvent({
type: 'auth_error',
error: result.error,
timestamp: new Date(),
ip: request.headers.get('x-forwarded-for'),
});
// Handle specific error cases
switch (result.error) {
case 'Invalid or expired magic link':
return Response.json(
{
error: 'Magic link expired',
message: 'This link has expired. Please request a new one.',
code: 'LINK_EXPIRED',
},
{ status: 401 }
);
case 'Invalid CSRF token':
return Response.json(
{
error: 'Security check failed',
message: 'Please try signing in again.',
code: 'CSRF_FAILED',
},
{ status: 403 }
);
case 'CAPTCHA token required':
case 'CAPTCHA verification failed':
return Response.json(
{
error: 'CAPTCHA verification failed',
message: 'Please complete the CAPTCHA challenge.',
code: 'CAPTCHA_REQUIRED',
},
{ status: 400 }
);
default:
// Generic error response
return Response.json(
{
error: 'Authentication failed',
message: 'Unable to authenticate. Please try again.',
code: 'AUTH_FAILED',
},
{ status: 401 }
);
}
}
// Success handling
if (result.session) {
await logAuthEvent({
type: 'auth_success',
userId: result.session.user.id,
provider: config.provider.type,
timestamp: new Date(),
});
const headers = new Headers();
if (result.cookies) {
result.cookies.forEach(cookie => {
headers.append('Set-Cookie', cookie);
});
}
return Response.json({ user: result.session.user }, { headers });
}
// OAuth redirect
if (result.redirectUrl) {
return Response.redirect(result.redirectUrl);
}
// Fallback
return Response.json({ error: 'Unknown error' }, { status: 500 });
} catch (error) {
// Unexpected errors
console.error('Unexpected authentication error:', error);
await logAuthEvent({
type: 'auth_exception',
error: error instanceof Error ? error.message : 'Unknown',
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date(),
});
return Response.json(
{
error: 'Internal server error',
message: 'An unexpected error occurred. Please try again later.',
code: 'INTERNAL_ERROR',
},
{ status: 500 }
);
}
}The getSession() function returns null on error:
Handle missing or invalid sessions
import { getSession } from '@warpy-auth-sdk/core';
export async function GET(request: Request) {
const session = await getSession(request, process.env.AUTH_SECRET!);
// No session or invalid session
if (!session) {
return Response.json(
{ error: 'Not authenticated' },
{ status: 401 }
);
}
// Session exists and is valid
return Response.json({ user: session.user });
}Add custom validation logic for sessions:
Validate session claims and revocation
import { getSession } from '@warpy-auth-sdk/core';
import jwt from 'jsonwebtoken';
export async function GET(request: Request) {
const session = await getSession(request, process.env.AUTH_SECRET!);
if (!session) {
return Response.json(
{ error: 'No session found' },
{ status: 401 }
);
}
// Decode token to check custom claims
const decoded = jwt.decode(session.token!) as any;
// Check if user still exists
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});
if (!user) {
return Response.json(
{
error: 'User not found',
message: 'Your account no longer exists.',
code: 'USER_DELETED',
},
{ status: 401 }
);
}
// Check if user is banned
if (user.status === 'banned') {
return Response.json(
{
error: 'Account suspended',
message: 'Your account has been suspended.',
code: 'USER_BANNED',
},
{ status: 403 }
);
}
// Check admin token expiration
if (decoded.isAdmin && decoded.adminExpiresAt < Date.now()) {
return Response.json(
{
error: 'Admin session expired',
message: 'Please re-authenticate for admin access.',
code: 'ADMIN_SESSION_EXPIRED',
},
{ status: 401 }
);
}
// Session is valid
return Response.json({ user: session.user });
}Throw errors in callbacks to abort authentication:
Validate and abort authentication
callbacks: {
async user(oauthUser) {
// Check if email domain is allowed
const allowedDomains = ['company.com', 'partner.com'];
const domain = oauthUser.email.split('@')[1];
if (!allowedDomains.includes(domain)) {
throw new Error(`Email domain "${domain}" is not allowed`);
}
// Check if user is banned
const bannedUser = await prisma.bannedUser.findUnique({
where: { email: oauthUser.email },
});
if (bannedUser) {
throw new Error('Account has been suspended');
}
// Check account creation limits
const recentAccounts = await prisma.user.count({
where: {
email: { endsWith: `@${domain}` },
createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
});
if (recentAccounts > 10) {
throw new Error('Account creation limit exceeded for this domain');
}
// Continue with user creation
return await upsertUser(oauthUser);
},
}Handle database errors gracefully in callbacks:
Graceful handling of DB errors
callbacks: {
async user(oauthUser) {
try {
// Attempt to find or create user
let user = await prisma.user.findUnique({
where: { email: oauthUser.email },
});
if (!user) {
user = await prisma.user.create({
data: {
email: oauthUser.email,
name: oauthUser.name,
picture: oauthUser.picture,
},
});
}
return {
id: user.id,
email: user.email,
name: user.name ?? undefined,
picture: user.picture ?? undefined,
};
} catch (error) {
// Handle specific Prisma errors
if (error.code === 'P2002') {
// Unique constraint violation
throw new Error('An account with this email already exists');
}
if (error.code === 'P2003') {
// Foreign key constraint violation
throw new Error('Invalid organization or role');
}
if (error.code === 'P1001') {
// Database connection error
throw new Error('Database unavailable. Please try again later.');
}
// Generic database error
console.error('Database error in user callback:', error);
throw new Error('Failed to create user account');
}
},
}Handle errors in the useAuth hook:
Handle authentication errors in React
'use client';
import { useAuth } from '@warpy-auth-sdk/core/hooks';
import { useState } from 'react';
export default function LoginForm() {
const { signIn } = useAuth();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError(null);
setLoading(true);
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
try {
const response = await fetch('/api/auth/signin/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!response.ok) {
const data = await response.json();
// Handle specific error codes
switch (data.code) {
case 'LINK_EXPIRED':
setError('Magic link expired. Please request a new one.');
break;
case 'CAPTCHA_REQUIRED':
setError('Please complete the CAPTCHA challenge.');
break;
case 'USER_BANNED':
setError('Your account has been suspended.');
break;
default:
setError(data.message || 'Authentication failed');
}
setLoading(false);
return;
}
// Success
setError(null);
} catch (err) {
console.error('Sign-in error:', err);
setError('Network error. Please check your connection.');
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{error && (
<div className="error-banner" role="alert">
{error}
</div>
)}
<input
type="email"
name="email"
placeholder="Email"
required
disabled={loading}
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
);
}Log errors with context for debugging:
Log authentication events for monitoring
// lib/logging.ts
interface AuthLogEvent {
type: 'auth_success' | 'auth_error' | 'auth_exception' | 'session_error';
userId?: string;
email?: string;
provider?: string;
error?: string;
stack?: string;
ip?: string | null;
userAgent?: string | null;
timestamp: Date;
}
export async function logAuthEvent(event: AuthLogEvent) {
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log('[Auth Event]', JSON.stringify(event, null, 2));
}
// Send to monitoring service in production
if (process.env.NODE_ENV === 'production') {
// Example: Send to Sentry, Datadog, etc.
await fetch(process.env.LOG_ENDPOINT!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
}
// Store in database for analytics
await prisma.authLog.create({
data: event,
});
}
// Usage in authentication handler
const result = await authenticate(config, request);
if (result.error) {
await logAuthEvent({
type: 'auth_error',
error: result.error,
provider: config.provider.type,
ip: request.headers.get('x-forwarded-for'),
userAgent: request.headers.get('user-agent'),
timestamp: new Date(),
});
}Integrate with error monitoring services:
Track authentication errors in Sentry
import * as Sentry from '@sentry/nextjs';
export async function POST(request: Request) {
try {
const result = await authenticate(config, request);
if (result.error) {
// Report to Sentry with context
Sentry.captureMessage('Authentication failed', {
level: 'warning',
tags: {
provider: config.provider.type,
error_type: result.error,
},
extra: {
error: result.error,
timestamp: new Date().toISOString(),
},
});
return Response.json({ error: result.error }, { status: 401 });
}
// Handle success
return Response.json({ user: result.session?.user });
} catch (error) {
// Report unexpected errors to Sentry
Sentry.captureException(error, {
tags: {
provider: config.provider.type,
handler: 'authenticate',
},
});
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Retry failed operations with backoff:
Retry with exponential backoff
async function authenticateWithRetry(
config: AuthConfig,
request: Request,
maxRetries = 3
): Promise<AuthenticateResult> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const result = await authenticate(config, request);
// Only retry on specific errors
if (result.error &&
(result.error.includes('Database unavailable') ||
result.error.includes('Network error'))) {
if (attempt < maxRetries - 1) {
// Exponential backoff: 100ms, 200ms, 400ms
const delay = Math.pow(2, attempt) * 100;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
// Return result (success or non-retryable error)
return result;
} catch (error) {
// Unexpected error - retry
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 100;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// Max retries exceeded
return { error: 'Maximum retry attempts exceeded' };
}
}
return { error: 'Authentication failed after retries' };
}Now that you understand error handling, explore: