276 lines
6.2 KiB
Markdown
276 lines
6.2 KiB
Markdown
# Quick Reference: Clean Authentication & Authorization
|
|
|
|
## The Golden Rules
|
|
|
|
1. **API is the source of truth** - Never trust the client for security
|
|
2. **Client is UX only** - Redirect, show loading, hide buttons
|
|
3. **One clear flow** - Middleware → API → Guard → Controller
|
|
4. **Roles are server-side** - Client only knows "can access" or "can't"
|
|
|
|
## What Goes Where
|
|
|
|
### Server-Side (API)
|
|
```typescript
|
|
// ✅ DO: Check permissions
|
|
@RequireRoles('admin')
|
|
@Get('users')
|
|
getUsers() { ... }
|
|
|
|
// ✅ DO: Return 401/403
|
|
throw new UnauthorizedException('Auth required')
|
|
throw new ForbiddenException('No permission')
|
|
|
|
// ❌ DON'T: Redirect
|
|
res.redirect('/login') // Never do this
|
|
|
|
// ❌ DON'T: Trust client identity
|
|
const userId = req.body.userId // Wrong!
|
|
const userId = req.user.userId // Correct
|
|
```
|
|
|
|
### Client-Side (Website)
|
|
```typescript
|
|
// ✅ DO: Redirect unauthenticated users
|
|
if (!session && !loading) {
|
|
router.push('/auth/login')
|
|
}
|
|
|
|
// ✅ DO: Show loading states
|
|
if (loading) return <Loading />
|
|
|
|
// ✅ DO: Hide UI elements
|
|
{canAccess && <button>Delete</button>}
|
|
|
|
// ❌ DON'T: Make security decisions
|
|
if (user.role === 'admin') // Wrong! API decides
|
|
|
|
// ❌ DON'T: Trust your own checks
|
|
// Client checks are UX only, API is the gatekeeper
|
|
```
|
|
|
|
## Route Protection Patterns
|
|
|
|
### Public Route
|
|
```typescript
|
|
// app/leagues/page.tsx
|
|
export default function LeaguesPage() {
|
|
return <LeaguesList />;
|
|
}
|
|
// No protection needed - accessible by all
|
|
```
|
|
|
|
### Authenticated Route
|
|
```typescript
|
|
// app/dashboard/layout.tsx
|
|
import { AuthLayout } from '@/lib/guards/AuthLayout';
|
|
|
|
export default function DashboardLayout({ children }) {
|
|
return <AuthLayout>{children}</AuthLayout>;
|
|
}
|
|
|
|
// app/dashboard/page.tsx
|
|
export default function DashboardPage() {
|
|
return <DashboardContent />;
|
|
}
|
|
// Layout handles auth check, page is clean
|
|
```
|
|
|
|
### Role-Protected Route
|
|
```typescript
|
|
// app/admin/layout.tsx
|
|
import { RoleLayout } from '@/lib/guards/RoleLayout';
|
|
|
|
export default function AdminLayout({ children }) {
|
|
return (
|
|
<RoleLayout requiredRoles={['owner', 'admin']}>
|
|
{children}
|
|
</RoleLayout>
|
|
);
|
|
}
|
|
|
|
// app/admin/page.tsx
|
|
export default function AdminPage() {
|
|
return <AdminDashboard />;
|
|
}
|
|
// Layout handles role check
|
|
```
|
|
|
|
### Scoped Route (League Admin)
|
|
```typescript
|
|
// app/leagues/[id]/settings/layout.tsx
|
|
import { AuthLayout } from '@/lib/guards/AuthLayout';
|
|
import { LeagueAccessGuard } from '@/components/leagues/LeagueAccessGuard';
|
|
|
|
export default function LeagueSettingsLayout({ children, params }) {
|
|
return (
|
|
<AuthLayout>
|
|
<LeagueAccessGuard leagueId={params.id}>
|
|
{children}
|
|
</LeagueAccessGuard>
|
|
</AuthLayout>
|
|
);
|
|
}
|
|
// Multiple guards for complex scenarios
|
|
```
|
|
|
|
## API Endpoint Patterns
|
|
|
|
### Public Endpoint
|
|
```typescript
|
|
@Public()
|
|
@Get('pricing')
|
|
getPricing() { ... }
|
|
// No auth required
|
|
```
|
|
|
|
### Authenticated Endpoint
|
|
```typescript
|
|
@RequireAuthenticatedUser()
|
|
@Get('profile')
|
|
getProfile(@User() user: UserEntity) { ... }
|
|
// Any logged-in user
|
|
```
|
|
|
|
### Role-Protected Endpoint
|
|
```typescript
|
|
@RequireRoles('admin')
|
|
@Get('users')
|
|
getUsers() { ... }
|
|
// Only admins
|
|
```
|
|
|
|
### Scoped Endpoint
|
|
```typescript
|
|
@RequireAuthenticatedUser()
|
|
@Get('leagues/:leagueId/admin')
|
|
getLeagueAdmin(
|
|
@Param('leagueId') leagueId: string,
|
|
@User() user: UserEntity
|
|
) {
|
|
// Check if user is league admin
|
|
this.leagueService.verifyLeagueAdmin(leagueId, user.id);
|
|
...
|
|
}
|
|
// Check scope in service
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### API Returns
|
|
- **401 Unauthorized**: No/invalid session
|
|
- **403 Forbidden**: Has session but no permission
|
|
- **404 Not Found**: Resource doesn't exist OR non-disclosure
|
|
|
|
### Client Handles
|
|
```typescript
|
|
try {
|
|
const data = await apiFetch('/api/admin/users');
|
|
return data;
|
|
} catch (error) {
|
|
if (error.message.includes('401')) {
|
|
// Redirect to login
|
|
window.location.href = '/auth/login';
|
|
} else if (error.message.includes('403')) {
|
|
// Show access denied
|
|
toast.error('You need admin access');
|
|
router.push('/dashboard');
|
|
} else {
|
|
// Show error
|
|
toast.error(error.message);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Mistakes
|
|
|
|
### ❌ Wrong
|
|
```typescript
|
|
// Client making security decisions
|
|
function AdminPage() {
|
|
const { session } = useAuth();
|
|
if (session?.role !== 'admin') return <AccessDenied />;
|
|
return <AdminDashboard />;
|
|
}
|
|
|
|
// API trusting client
|
|
@Post('delete')
|
|
deleteUser(@Body() body: { userId: string }) {
|
|
const userId = body.userId; // Could be anyone!
|
|
...
|
|
}
|
|
|
|
// Middleware doing too much
|
|
if (user.role === 'admin') { // Wrong place for this!
|
|
return NextResponse.next();
|
|
}
|
|
```
|
|
|
|
### ✅ Correct
|
|
```typescript
|
|
// Client handles UX only
|
|
function AdminPage() {
|
|
return (
|
|
<RoleLayout requiredRoles={['admin']}>
|
|
<AdminDashboard />
|
|
</RoleLayout>
|
|
);
|
|
}
|
|
|
|
// API is source of truth
|
|
@Post('delete')
|
|
@RequireRoles('admin')
|
|
deleteUser(@User() user: UserEntity, @Body() body: { userId: string }) {
|
|
// user.id is from session, body.userId is target
|
|
// Service verifies permissions
|
|
...
|
|
}
|
|
|
|
// Middleware only checks auth
|
|
if (!hasAuthCookie) {
|
|
return redirect('/login');
|
|
}
|
|
// Let API handle roles
|
|
```
|
|
|
|
## Testing Checklist
|
|
|
|
### Before Deploy
|
|
- [ ] Unauthenticated user can't access protected routes
|
|
- [ ] Authenticated user can access their routes
|
|
- [ ] Wrong role gets redirected/denied
|
|
- [ ] Session expiry redirects to login
|
|
- [ ] API returns proper 401/403 codes
|
|
- [ ] Public routes work without login
|
|
|
|
### Quick Test Commands
|
|
```bash
|
|
# Test API directly
|
|
curl -I http://localhost:3000/api/admin/users
|
|
# Should return 401 (no auth)
|
|
|
|
# Test with session
|
|
curl -I http://localhost:3000/api/admin/users \
|
|
-H "Cookie: gp_session=valid_token"
|
|
# Should return 200 or 403 depending on role
|
|
|
|
# Test public route
|
|
curl -I http://localhost:3000/api/leagues/all
|
|
# Should return 200
|
|
```
|
|
|
|
## Migration Steps
|
|
|
|
1. **Simplify middleware** - Remove role logic
|
|
2. **Create clean guards** - AuthLayout, RoleLayout
|
|
3. **Update layouts** - Replace old RouteGuard
|
|
4. **Test all routes** - Check redirects work
|
|
5. **Verify API** - All endpoints have proper decorators
|
|
|
|
## Remember
|
|
|
|
- **Server**: Security, permissions, data filtering
|
|
- **Client**: UX, loading states, redirects
|
|
- **Flow**: Always the same, always predictable
|
|
- **Debug**: Check each layer in order
|
|
|
|
**When in doubt**: The API decides. The client just shows what the API says. |