# 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 // ✅ DO: Hide UI elements {canAccess && } // ❌ 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 ; } // 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 {children}; } // app/dashboard/page.tsx export default function DashboardPage() { return ; } // 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 ( {children} ); } // app/admin/page.tsx export default function AdminPage() { return ; } // 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 ( {children} ); } // 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 ; return ; } // 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 ( ); } // 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.