Advanced Authentication Patterns with Next.js and Supabase
Master advanced authentication patterns including OAuth, magic links, passwordless auth, custom JWT, multi-tenant authentication, and enterprise SSO integration with Next.js and Supabase.
Advanced Authentication Patterns with Next.js and Supabase#
Authentication is the gateway to your application. While basic email/password authentication works for simple apps, production applications need sophisticated authentication patterns that balance security, user experience, and business requirements. This guide teaches you advanced authentication techniques used by leading SaaS companies.
Authentication Fundamentals#
Before diving into advanced patterns, understand these core concepts:
Authentication vs Authorization:
- Authentication: Verifies who you are (login)
- Authorization: Determines what you can do (permissions)
Stateful vs Stateless:
- Stateful: Server stores session data (traditional approach)
- Stateless: Client stores token, server verifies (JWT approach)
Token Types:
- Access Token: Short-lived (1 hour), used for API requests
- Refresh Token: Long-lived (7 days), used to get new access tokens
- ID Token: Contains user information, used for authentication
1. OAuth 2.0 Implementation#
OAuth delegates authentication to trusted providers, reducing your security burden.
Configuring OAuth Providers#
In Supabase Dashboard:
- Go to Authentication → Providers
- Enable desired providers (Google, GitHub, Discord, etc.)
- Add OAuth credentials from provider
- Configure redirect URLs
Implementing OAuth Sign-In#
// app/auth/oauth/page.tsx
'use client';
import { createClient } from '@/lib/supabase/client';
import { useRouter } from 'next/navigation';
export default function OAuthPage() {
const router = useRouter();
const supabase = createClient();
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
console.error('OAuth error:', error);
}
}
async function signInWithGitHub() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
console.error('OAuth error:', error);
}
}
return (
<div className="space-y-4">
<button onClick={signInWithGoogle} className="btn btn-google">
Sign in with Google
</button>
<button onClick={signInWithGitHub} className="btn btn-github">
Sign in with GitHub
</button>
</div>
);
}
OAuth Callback Handler#
// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const error = searchParams.get('error');
if (error) {
return NextResponse.redirect(
new URL(`/auth/error?error=${error}`, request.url)
);
}
if (code) {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
const { error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
if (!exchangeError) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.redirect(new URL('/auth/error', request.url));
}
Account Linking#
Allow users to link multiple OAuth providers:
// Link additional OAuth provider to existing account
async function linkOAuthProvider(provider: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.linkIdentity({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
console.error('Linking error:', error);
}
}
// Unlink OAuth provider
async function unlinkOAuthProvider(provider: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.unlinkIdentity({
identity_id: 'provider_id'
});
if (error) {
console.error('Unlinking error:', error);
}
}
2. Passwordless Authentication#
Passwordless auth improves security and user experience by eliminating passwords.
Magic Link Authentication#
// app/auth/magic-link/page.tsx
'use client';
import { createClient } from '@/lib/supabase/client';
import { useState } from 'react';
export default function MagicLinkPage() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const supabase = createClient();
async function handleMagicLink(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
setMessage(`Error: ${error.message}`);
} else {
setMessage('Check your email for the magic link!');
setEmail('');
}
setLoading(false);
}
return (
<form onSubmit={handleMagicLink} className="space-y-4">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Magic Link'}
</button>
{message && <p>{message}</p>}
</form>
);
}
One-Time Password (OTP)#
// Send OTP via SMS or email
async function sendOTP(phone: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.signInWithOtp({
phone,
options: {
shouldCreateUser: true
}
});
if (error) {
console.error('OTP error:', error);
}
}
// Verify OTP
async function verifyOTP(phone: string, token: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.verifyOtp({
phone,
token,
type: 'sms'
});
if (error) {
console.error('Verification error:', error);
}
return data;
}
3. Custom JWT Implementation#
For advanced use cases, implement custom JWT claims:
// lib/jwt.ts
import jwt from 'jsonwebtoken';
interface CustomClaims {
sub: string; // user id
email: string;
organization_id: string;
role: 'admin' | 'editor' | 'viewer';
permissions: string[];
iat: number;
exp: number;
}
export function createCustomJWT(user: {
id: string;
email: string;
organization_id: string;
role: string;
permissions: string[];
}): string {
const claims: CustomClaims = {
sub: user.id,
email: user.email,
organization_id: user.organization_id,
role: user.role as any,
permissions: user.permissions,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
};
return jwt.sign(claims, process.env.JWT_SECRET!);
}
export function verifyCustomJWT(token: string): CustomClaims | null {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as CustomClaims;
} catch (error) {
return null;
}
}
Using Custom Claims in Middleware#
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyCustomJWT } from '@/lib/jwt';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
const claims = verifyCustomJWT(token);
if (!claims) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Check authorization
if (request.nextUrl.pathname.startsWith('/admin') && claims.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
// Add claims to request headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', claims.sub);
requestHeaders.set('x-organization-id', claims.organization_id);
requestHeaders.set('x-user-role', claims.role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/api/:path*']
};
4. Multi-Tenant Authentication#
Implement authentication for multi-tenant SaaS applications:
// lib/multi-tenant-auth.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function getUserWithOrganization() {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return null;
}
// Get user's organizations
const { data: organizations } = await supabase
.from('organization_members')
.select(`
organization_id,
role,
organizations(id, name, slug)
`)
.eq('user_id', user.id);
return {
user,
organizations: organizations || []
};
}
// Verify user belongs to organization
export async function verifyOrganizationAccess(
userId: string,
organizationId: string
): Promise<boolean> {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
const { data } = await supabase
.from('organization_members')
.select('id')
.eq('user_id', userId)
.eq('organization_id', organizationId)
.single();
return !!data;
}
Multi-Tenant Middleware#
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyOrganizationAccess } from '@/lib/multi-tenant-auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Extract organization slug from URL
const match = pathname.match(/^\/org\/([^/]+)/);
if (!match) {
return NextResponse.next();
}
const organizationSlug = match[1];
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Verify user has access to organization
const hasAccess = await verifyOrganizationAccess(userId, organizationSlug);
if (!hasAccess) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/org/:path*']
};
5. Enterprise SSO (SAML)#
For enterprise customers, implement SAML-based SSO:
// Configure SAML provider
async function configureSAML(organizationId: string, samlConfig: {
entityId: string;
ssoUrl: string;
certificate: string;
}) {
const supabase = createClient();
const { data, error } = await supabase
.from('organization_sso')
.insert({
organization_id: organizationId,
provider: 'saml',
config: samlConfig
});
if (error) {
console.error('SAML config error:', error);
}
return data;
}
// SAML sign-in
async function signInWithSAML(organizationId: string) {
const supabase = createClient();
// Get SAML config
const { data: ssoConfig } = await supabase
.from('organization_sso')
.select('*')
.eq('organization_id', organizationId)
.eq('provider', 'saml')
.single();
if (!ssoConfig) {
throw new Error('SAML not configured for this organization');
}
// Redirect to SAML provider
window.location.href = ssoConfig.config.ssoUrl;
}
6. Step-Up Authentication#
Require additional verification for sensitive operations:
// lib/step-up-auth.ts
const STEP_UP_TIMEOUT = 15 * 60 * 1000; // 15 minutes
export async function requireStepUpAuth(userId: string): Promise<boolean> {
const lastStepUp = localStorage.getItem(`step-up-${userId}`);
const now = Date.now();
if (!lastStepUp || now - parseInt(lastStepUp) > STEP_UP_TIMEOUT) {
// Require re-authentication
return false;
}
return true;
}
export function recordStepUpAuth(userId: string) {
localStorage.setItem(`step-up-${userId}`, Date.now().toString());
}
// Usage in sensitive operation
async function changePassword(userId: string, newPassword: string) {
const hasStepUp = await requireStepUpAuth(userId);
if (!hasStepUp) {
// Redirect to re-authentication
throw new Error('Step-up authentication required');
}
// Change password
const supabase = createClient();
const { error } = await supabase.auth.updateUser({
password: newPassword
});
if (!error) {
recordStepUpAuth(userId);
}
return error;
}
7. Authentication Error Handling#
// lib/auth-errors.ts
export function getAuthErrorMessage(error: any): string {
const errorCode = error?.code || error?.message;
const messages: Record<string, string> = {
'invalid_credentials': 'Invalid email or password',
'user_not_found': 'User not found',
'email_not_confirmed': 'Please confirm your email',
'weak_password': 'Password is too weak',
'user_already_exists': 'User already exists',
'over_request_rate_limit': 'Too many requests. Please try again later',
'session_not_found': 'Session expired. Please log in again',
'invalid_grant': 'Invalid credentials',
'invalid_request': 'Invalid request'
};
return messages[errorCode] || 'An authentication error occurred';
}
// Usage
async function handleLogin(email: string, password: string) {
const supabase = createClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password
});
if (error) {
const message = getAuthErrorMessage(error);
console.error(message);
return { error: message };
}
return { success: true };
}
8. Authentication Best Practices Checklist#
- ✅ Use HTTPS in production
- ✅ Store tokens in httpOnly cookies
- ✅ Implement token refresh logic
- ✅ Use strong password requirements
- ✅ Implement rate limiting on auth endpoints
- ✅ Enable MFA for sensitive accounts
- ✅ Log authentication events
- ✅ Implement step-up authentication for sensitive operations
- ✅ Use OAuth for consumer apps
- ✅ Implement SAML for enterprise customers
- ✅ Handle authentication errors gracefully
- ✅ Implement account linking
- ✅ Regular security audits
Related Articles#
- Supabase Authentication & Authorization
- Security Best Practices
- Fix Supabase Auth Session Not Persisting
- Handle Supabase Auth Errors in Middleware
Conclusion#
Advanced authentication patterns enable you to build secure, scalable applications that meet diverse user and business requirements. Start with basic OAuth for consumer apps, add passwordless authentication for better UX, and implement enterprise features like SAML for B2B customers.
Remember: authentication is the foundation of security. Invest time in getting it right, and your users will thank you with their trust.