Initial commit
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
# Authentication Patterns for Next.js + Supabase
|
||||
|
||||
## Common Authentication Patterns
|
||||
|
||||
### Pattern 1: Protected Route Groups
|
||||
|
||||
Use Next.js route groups to protect multiple routes with a single layout:
|
||||
|
||||
```
|
||||
app/
|
||||
├── (public)/
|
||||
│ ├── layout.tsx # Public layout
|
||||
│ ├── page.tsx # Home page
|
||||
│ └── about/
|
||||
│ └── page.tsx # About page
|
||||
└── (protected)/
|
||||
├── layout.tsx # Protected layout with requireAuth()
|
||||
├── dashboard/
|
||||
│ └── page.tsx
|
||||
└── settings/
|
||||
└── page.tsx
|
||||
```
|
||||
|
||||
**Protected layout example:**
|
||||
```typescript
|
||||
// app/(protected)/layout.tsx
|
||||
import { requireAuth } from '@/lib/auth/utils';
|
||||
|
||||
export default async function ProtectedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
await requireAuth();
|
||||
return <>{children}</>;
|
||||
}
|
||||
```
|
||||
|
||||
All routes inside `(protected)` automatically require authentication.
|
||||
|
||||
### Pattern 2: Conditional UI Based on Auth State
|
||||
|
||||
Show different content based on whether user is authenticated:
|
||||
|
||||
```typescript
|
||||
import { getCurrentUser } from '@/lib/auth/utils';
|
||||
|
||||
export default async function HomePage() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{user ? (
|
||||
<div>
|
||||
<h1>Welcome back, {user.email}!</h1>
|
||||
<a href="/dashboard">Go to Dashboard</a>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h1>Welcome to our app</h1>
|
||||
<a href="/login">Sign in</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Role-Based Access Control
|
||||
|
||||
Protect routes based on user roles:
|
||||
|
||||
```typescript
|
||||
// lib/auth/utils.ts - Add role checking
|
||||
export async function requireRole(allowedRoles: string[]) {
|
||||
const user = await requireAuth();
|
||||
const supabase = await createServerClient();
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('role')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (!profile || !allowedRoles.includes(profile.role)) {
|
||||
redirect('/unauthorized');
|
||||
}
|
||||
|
||||
return { user, profile };
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in admin page:**
|
||||
```typescript
|
||||
export default async function AdminPage() {
|
||||
await requireRole(['admin', 'moderator']);
|
||||
|
||||
return <div>Admin Dashboard</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Server Action Authentication
|
||||
|
||||
Protect Server Actions that modify data:
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { requireAuth } from '@/lib/auth/utils';
|
||||
import { createServerClient } from '@/lib/supabase/server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const user = await requireAuth();
|
||||
const supabase = await createServerClient();
|
||||
|
||||
const title = formData.get('title') as string;
|
||||
const content = formData.get('content') as string;
|
||||
|
||||
const { error } = await supabase.from('posts').insert({
|
||||
title,
|
||||
content,
|
||||
author_id: user.id,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
revalidatePath('/posts');
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: API Route Authentication
|
||||
|
||||
Protect API routes for external access:
|
||||
|
||||
```typescript
|
||||
// app/api/protected/route.ts
|
||||
import { createServerClient } from '@/lib/supabase/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const supabase = await createServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Return protected data
|
||||
return NextResponse.json({ data: 'Protected data' });
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 6: Parallel Data Fetching with Auth
|
||||
|
||||
Fetch user and their data in parallel:
|
||||
|
||||
```typescript
|
||||
export default async function ProfilePage() {
|
||||
const user = await requireAuth();
|
||||
|
||||
const supabase = await createServerClient();
|
||||
|
||||
// Fetch user profile and posts in parallel
|
||||
const [{ data: profile }, { data: posts }] = await Promise.all([
|
||||
supabase.from('profiles').select('*').eq('id', user.id).single(),
|
||||
supabase.from('posts').select('*').eq('author_id', user.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{profile?.name}</h1>
|
||||
<h2>Posts</h2>
|
||||
{posts?.map((post) => (
|
||||
<div key={post.id}>{post.title}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 7: Redirect After Login
|
||||
|
||||
Preserve intended destination after login:
|
||||
|
||||
**Middleware:**
|
||||
```typescript
|
||||
export async function middleware(request: NextRequest) {
|
||||
const { supabaseResponse, user } = await updateSession(request);
|
||||
|
||||
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
const redirectUrl = new URL('/login', request.url);
|
||||
// Save the intended destination
|
||||
redirectUrl.searchParams.set('redirect', request.nextUrl.pathname);
|
||||
return Response.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
return supabaseResponse;
|
||||
}
|
||||
```
|
||||
|
||||
**Login page:**
|
||||
```typescript
|
||||
export default async function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { redirect?: string };
|
||||
}) {
|
||||
// After successful login, redirect to intended page
|
||||
const redirectTo = searchParams.redirect || '/dashboard';
|
||||
|
||||
// Use redirectTo in form action or success handler
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 8: Email Verification Check
|
||||
|
||||
Require email verification before accessing certain features:
|
||||
|
||||
```typescript
|
||||
export async function requireVerifiedEmail() {
|
||||
const user = await requireAuth();
|
||||
|
||||
if (!user.email_confirmed_at) {
|
||||
redirect('/verify-email');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Server Components for Auth Checks**: Leverage Server Components for initial auth checks to avoid client-side flashing
|
||||
2. **Middleware for Session Refresh**: Always refresh sessions in middleware to keep auth state fresh
|
||||
3. **Revalidate After Auth Changes**: Use `revalidatePath()` after login/logout to clear cached content
|
||||
4. **Secure Cookie Configuration**: Ensure cookies use secure, httpOnly, and sameSite settings
|
||||
5. **Handle Auth Errors Gracefully**: Provide clear error messages and recovery paths
|
||||
6. **Use TypeScript**: Type your user objects and auth functions for better DX
|
||||
7. **Test Protected Routes**: Verify both authenticated and unauthenticated access
|
||||
8. **Implement Proper Redirects**: Always redirect after authentication changes to prevent stale UI
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
1. **Client-Side Only Auth**: Don't rely solely on client-side auth checks
|
||||
2. **Checking Auth on Every Component**: Use layouts and route groups instead
|
||||
3. **Exposing Sensitive Data**: Never send sensitive data to client without auth check
|
||||
4. **Hardcoded Redirects**: Use dynamic redirects based on user intent
|
||||
5. **Ignoring Middleware**: Always use middleware for session refresh
|
||||
6. **Not Handling Loading States**: Show appropriate loading states during auth checks
|
||||
@@ -0,0 +1,300 @@
|
||||
# Security Considerations for Supabase Auth
|
||||
|
||||
## Cookie Security
|
||||
|
||||
### Secure Cookie Configuration
|
||||
|
||||
The `@supabase/ssr` package automatically configures secure cookies with these settings:
|
||||
|
||||
- **httpOnly**: Prevents client-side JavaScript from accessing cookies
|
||||
- **secure**: Cookies only sent over HTTPS (except localhost)
|
||||
- **sameSite**: Prevents CSRF attacks by restricting cross-site cookie sending
|
||||
- **path**: Limits cookie scope to specific paths
|
||||
|
||||
**Do not modify** cookie settings unless you understand the security implications.
|
||||
|
||||
### Session Duration
|
||||
|
||||
Configure session timeout in Supabase dashboard:
|
||||
|
||||
1. Go to Authentication > Settings
|
||||
2. Adjust "JWT expiry limit" (default: 1 hour)
|
||||
3. Set "Refresh token expiry" (default: 30 days)
|
||||
|
||||
**Recommendations:**
|
||||
- Short JWT expiry (1 hour) for better security
|
||||
- Longer refresh token expiry (30 days) for better UX
|
||||
- Use middleware to automatically refresh sessions
|
||||
|
||||
## Row Level Security (RLS)
|
||||
|
||||
### Enable RLS on All Tables
|
||||
|
||||
Always enable Row Level Security on tables containing user data:
|
||||
|
||||
```sql
|
||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
### Create Restrictive Policies
|
||||
|
||||
**Read own data:**
|
||||
```sql
|
||||
CREATE POLICY "Users can view own profile"
|
||||
ON profiles FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
**Update own data:**
|
||||
```sql
|
||||
CREATE POLICY "Users can update own profile"
|
||||
ON profiles FOR UPDATE
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
**Insert on signup:**
|
||||
```sql
|
||||
CREATE POLICY "Users can insert own profile"
|
||||
ON profiles FOR INSERT
|
||||
WITH CHECK (auth.uid() = id);
|
||||
```
|
||||
|
||||
### Test RLS Policies
|
||||
|
||||
Always test policies with different user contexts:
|
||||
|
||||
```sql
|
||||
-- Test as specific user
|
||||
SET LOCAL ROLE authenticated;
|
||||
SET LOCAL request.jwt.claims.sub = 'user-uuid-here';
|
||||
|
||||
-- Run queries to verify access
|
||||
SELECT * FROM profiles;
|
||||
```
|
||||
|
||||
## Authentication Best Practices
|
||||
|
||||
### 1. Never Trust Client Input
|
||||
|
||||
Always validate on server-side:
|
||||
|
||||
```typescript
|
||||
export async function updateProfile(formData: FormData) {
|
||||
const user = await requireAuth();
|
||||
|
||||
// Validate input
|
||||
const name = formData.get('name') as string;
|
||||
if (!name || name.length < 2) {
|
||||
throw new Error('Invalid name');
|
||||
}
|
||||
|
||||
// Only update own profile
|
||||
const supabase = await createServerClient();
|
||||
await supabase
|
||||
.from('profiles')
|
||||
.update({ name })
|
||||
.eq('id', user.id); // Ensure user can only update their own data
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Server Components for Sensitive Data
|
||||
|
||||
Fetch sensitive data in Server Components, not Client Components:
|
||||
|
||||
```typescript
|
||||
// [OK] Good - Server Component
|
||||
export default async function ProfilePage() {
|
||||
const user = await requireAuth();
|
||||
const supabase = await createServerClient();
|
||||
|
||||
const { data: privateData } = await supabase
|
||||
.from('private_table')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
return <div>{/* Use privateData */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// [ERROR] Bad - Client Component exposes API
|
||||
'use client';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetching sensitive data on client
|
||||
fetch('/api/private-data').then(/* ... */);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement Rate Limiting
|
||||
|
||||
Protect authentication endpoints from brute force:
|
||||
|
||||
```typescript
|
||||
// Example using Upstash Rate Limit
|
||||
import { Ratelimit } from '@upstash/ratelimit';
|
||||
import { Redis } from '@upstash/redis';
|
||||
|
||||
const ratelimit = new Ratelimit({
|
||||
redis: Redis.fromEnv(),
|
||||
limiter: Ratelimit.slidingWindow(5, '15 m'), // 5 attempts per 15 minutes
|
||||
});
|
||||
|
||||
export async function signInWithPassword(formData: FormData) {
|
||||
const email = formData.get('email') as string;
|
||||
|
||||
const { success } = await ratelimit.limit(email);
|
||||
if (!success) {
|
||||
return { error: 'Too many attempts. Please try again later.' };
|
||||
}
|
||||
|
||||
// Proceed with sign in
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Validate Email Addresses
|
||||
|
||||
Require email verification before granting full access:
|
||||
|
||||
```typescript
|
||||
export async function requireVerifiedEmail() {
|
||||
const user = await requireAuth();
|
||||
|
||||
if (!user.email_confirmed_at) {
|
||||
redirect('/verify-email');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
Configure in Supabase:
|
||||
- Authentication > Settings > Enable "Confirm email"
|
||||
- Customize email templates in Authentication > Email Templates
|
||||
|
||||
### 5. Use Environment Variables Correctly
|
||||
|
||||
**Public variables** (safe to expose):
|
||||
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||
|
||||
**Private variables** (server-side only):
|
||||
- `SUPABASE_SERVICE_ROLE_KEY` (never expose to client!)
|
||||
|
||||
```typescript
|
||||
// [OK] Good - Service role only on server
|
||||
'use server';
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
export async function adminFunction() {
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY! // Only accessible server-side
|
||||
);
|
||||
|
||||
// Bypasses RLS - use carefully
|
||||
}
|
||||
```
|
||||
|
||||
## Common Security Mistakes
|
||||
|
||||
### 1. Exposing Service Role Key
|
||||
|
||||
[ERROR] **Never** use service role key in client-side code:
|
||||
|
||||
```typescript
|
||||
// [ERROR] DANGEROUS - Exposes admin access
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY // Exposed to client!
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Skipping RLS
|
||||
|
||||
[ERROR] **Never** disable RLS or use service role to bypass it unnecessarily:
|
||||
|
||||
```sql
|
||||
-- [ERROR] DANGEROUS - Allows anyone to read all data
|
||||
ALTER TABLE profiles DISABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
### 3. Client-Side Authorization
|
||||
|
||||
[ERROR] **Never** rely only on client-side checks:
|
||||
|
||||
```typescript
|
||||
// [ERROR] BAD - Can be bypassed
|
||||
'use client';
|
||||
|
||||
export default function AdminPanel() {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
if (user?.role !== 'admin') {
|
||||
return <div>Unauthorized</div>;
|
||||
}
|
||||
|
||||
return <div>Admin content</div>; // Still rendered in HTML!
|
||||
}
|
||||
```
|
||||
|
||||
[OK] **Always** enforce on server:
|
||||
|
||||
```typescript
|
||||
// [OK] GOOD - Server-side enforcement
|
||||
export default async function AdminPanel() {
|
||||
await requireRole('admin'); // Redirects if not admin
|
||||
|
||||
return <div>Admin content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Not Revalidating After Auth Changes
|
||||
|
||||
[ERROR] **Never** forget to revalidate after login/logout:
|
||||
|
||||
```typescript
|
||||
// [ERROR] BAD - Stale cached content may show
|
||||
export async function logout() {
|
||||
const supabase = await createServerClient();
|
||||
await supabase.auth.signOut();
|
||||
redirect('/'); // Missing revalidation!
|
||||
}
|
||||
```
|
||||
|
||||
[OK] **Always** revalidate:
|
||||
|
||||
```typescript
|
||||
// [OK] GOOD
|
||||
export async function logout() {
|
||||
const supabase = await createServerClient();
|
||||
await supabase.auth.signOut();
|
||||
revalidatePath('/', 'layout'); // Clears all cached pages
|
||||
redirect('/');
|
||||
}
|
||||
```
|
||||
|
||||
## Audit Checklist
|
||||
|
||||
Use this checklist when implementing authentication:
|
||||
|
||||
- [ ] RLS enabled on all tables with user data
|
||||
- [ ] RLS policies tested with different user contexts
|
||||
- [ ] Service role key never exposed to client
|
||||
- [ ] Email verification required for sensitive actions
|
||||
- [ ] Rate limiting on authentication endpoints
|
||||
- [ ] Input validation on all Server Actions
|
||||
- [ ] Secure cookies configured (httpOnly, secure, sameSite)
|
||||
- [ ] Middleware refreshes sessions on all requests
|
||||
- [ ] Sensitive data only fetched in Server Components
|
||||
- [ ] Authorization checks on server-side, not client-side
|
||||
- [ ] Proper revalidation after auth state changes
|
||||
- [ ] Error messages don't leak sensitive information
|
||||
- [ ] OAuth redirect URLs configured correctly
|
||||
- [ ] Session timeout appropriate for use case
|
||||
- [ ] Logout clears all auth state and cookies
|
||||
Reference in New Issue
Block a user