managarten/AUTH_VALIDATION_CHECKLIST.md
Wuesteon 4d15d9e764 🔒 security(auth): migrate to EdDSA JWT and add automated monitoring
BREAKING: JWT keys are now auto-managed by Better Auth (EdDSA/Ed25519)
- Remove all JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, JWT_SECRET references
- Keys stored in auth.jwks database table (auto-generated on first run)
- Delete obsolete generate-keys.sh and generate-staging-secrets.sh scripts
- Clean up legacy AUTH_*.md analysis files from root

Security Improvements:
- Add security_events table for audit logging
- Add SecurityEventsService for tracking auth events
- Enhanced security headers (HSTS, CSP, X-Frame-Options)
- Rate limiting configuration

Monitoring Setup:
- Add auth-health-check.sh for automated testing
- Add generate-dashboard.sh for HTML status dashboard
- Tests: health endpoint, JWKS (EdDSA), security headers, response time
- Ready for Hetzner cron deployment

Documentation:
- Update deployment docs with Better Auth notes
- Update environment variable references
- Add security improvements documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 21:42:47 +01:00

11 KiB

Authentication Architecture - Validation Checklist

This checklist ensures all NestJS backend services implement authentication according to canonical patterns defined in AUTH_ARCHITECTURE_REPORT.md.


Pre-Integration Checklist

Use this before integrating auth into a new backend service.

Package Selection

  • Reviewed AUTH_ARCHITECTURE_REPORT.md section 9 (Integration Best Practices)
  • Determined whether service needs credit tracking
    • No credits → Use @manacore/shared-nestjs-auth (lightweight)
    • Yes, credits → Use @mana-core/nestjs-integration (full-featured)
  • Package dependency documented in package.json

Environment Variables

  • .env file created with required variables:

    • MANA_CORE_AUTH_URL=http://localhost:3001
    • NODE_ENV=development (for dev mode)
    • DEV_BYPASS_AUTH=true (for testing without token)
    • DEV_USER_ID=test-user-uuid (optional, for custom test user)
  • Verified .env is NOT committed to git

  • Verified .env.example documents all variables

Documentation

  • Service's CLAUDE.md updated with:
    • Auth integration pattern used (Path A or B)
    • Example of @UseGuards usage
    • Example of @CurrentUser() usage
    • Required environment variables listed
    • Development bypass instructions

Implementation Checklist

Guard Setup

  • Guard imported from correct package:

    // Path A only
    import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
    
    // Path B only
    import { AuthGuard } from '@mana-core/nestjs-integration';
    
  • Guard applied globally in main.ts:

    app.useGlobalGuards(new JwtAuthGuard(app.get(ConfigService)));
    // OR
    app.useGlobalGuards(new AuthGuard(options));
    
  • Guard applied to protected controllers:

    @Controller('api')
    @UseGuards(JwtAuthGuard)  // or AuthGuard
    export class MyController { ... }
    

Decorator Usage

  • @CurrentUser() imported from correct package:

    // Path A
    import { CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
    
    // Path B
    import { CurrentUser } from '@mana-core/nestjs-integration';
    
  • Decorator used in protected route handlers:

    @Get('profile')
    getProfile(@CurrentUser() user: CurrentUserData) {
      // user.userId, user.email, user.role, user.sessionId available
    }
    

Public Routes (Path B Only)

If using @mana-core/nestjs-integration:

  • @Public() decorator imported:

    import { Public } from '@mana-core/nestjs-integration';
    
  • Applied to non-protected endpoints:

    @Get('health')
    @Public()
    health() {
      return { status: 'ok' };
    }
    
  • Verified all public routes are marked (health check, openapi, etc.)

Error Handling

  • Imported UnauthorizedException from @nestjs/common
  • Auth errors return 401 status code
  • Error messages don't leak implementation details
  • Example error response:
    {
      "statusCode": 401,
      "message": "Unauthorized"
    }
    

API Route Validation

Route Naming Convention

  • All endpoints prefixed with /api (NestJS convention)
  • Global prefix set in main.ts: ✗ app.setGlobalPrefix('api/v1'); (This is in mana-core-auth only)
    • For other backends: Regular /api prefix only
  • Controllers use appropriate path prefixes

Protected Routes

For each protected route, verify:

  • Decorated with @UseGuards(JwtAuthGuard) or @UseGuards(AuthGuard)
  • Uses @CurrentUser() to extract user data
  • Returns 401 Unauthorized if token is missing/invalid
  • Doesn't require JWT parsing in handler (guard does it)

Example:

@Controller('api/favorites')
@UseGuards(JwtAuthGuard)
export class FavoriteController {
  @Get()
  async list(@CurrentUser() user: CurrentUserData) {
    return { items: [] };  // user.userId available
  }
}

Health/Status Routes

  • Health endpoint does NOT require auth
  • Properly decorated with @Public() (if using Path B)
  • Returns { status: 'ok' } or similar

JWT Token Validation

Token Format

  • Tokens received in Authorization: Bearer {token} format
  • Guard extracts token correctly using split(' ')
  • No custom token parsing in controllers

Token Claims

  • Verified token contains minimal claims only:

    • sub (user ID)
    • email
    • role (user | admin | service)
    • sid or sessionId (session ID)
  • Verified token DOES NOT contain:

    • Organization data (fetch via API)
    • Credit balance (fetch via API)
    • Customer type (derive from org presence)
    • Device info (use session data)

Validation Endpoint Usage

  • Guard calls POST http://localhost:3001/api/v1/auth/validate
  • Validation is synchronous (guard waits for response)
  • Error handling works when auth service is unreachable

Database Considerations

Schema Assumptions

  • Service assumes auth.* schema exists in main database
  • Or uses separate auth database (mana-core-auth default)
  • Database connection URL correctly configured

User Data Storage

  • User IDs stored as TEXT (matching auth.users.id type)
  • No re-hashing of passwords (auth service handles)
  • Foreign keys to auth.users use TEXT type:
    user_id TEXT REFERENCES auth.users(id)
    

Testing Checklist

Manual Token Testing

# 1. Start mana-core-auth service
pnpm dev:auth

# 2. Register user
curl -X POST http://localhost:3001/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123",
    "name": "Test User"
  }'

# 3. Login to get tokens
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123"
  }' | jq -r '.accessToken')

# 4. Test protected endpoint
curl http://localhost:3007/api/favorites \
  -H "Authorization: Bearer $TOKEN"

# 5. Test without token (should fail)
curl http://localhost:3007/api/favorites
# Should return: 401 Unauthorized
  • Login returns valid JWT token
  • Protected endpoint accepts valid token
  • Protected endpoint rejects missing token
  • Protected endpoint rejects expired token
  • Token refresh works
  • User data correctly injected via @CurrentUser()

Development Mode Testing

  • Set DEV_BYPASS_AUTH=true
  • Set DEV_USER_ID=test-123 (optional)
  • Protected endpoint works WITHOUT token
  • Returns mock user data when DEV_BYPASS_AUTH enabled

Unit Tests

  • Mock ConfigService in tests
  • Mock HTTP fetch for token validation
  • Test guard with valid token
  • Test guard with invalid token
  • Test guard with missing token
  • Test @CurrentUser() decorator injection

Example test:

it('should attach user to request when token is valid', async () => {
  const mockUser = { userId: 'user-123', email: 'test@example.com', role: 'user' };
  const guard = new JwtAuthGuard(mockConfigService);
  
  const result = await guard.canActivate(mockContext);
  
  expect(result).toBe(true);
  expect(request.user).toEqual(mockUser);
});

Integration Testing

With mana-core-auth Service

  • Start mana-core-auth on port 3001
  • Start backend service (e.g., on port 3007)
  • Test real token validation flow
  • Verify JWKS endpoint accessible: curl http://localhost:3001/api/v1/auth/jwks
  • Verify validation endpoint accessible: curl -X POST http://localhost:3001/api/v1/auth/validate

Multi-Service Auth

If multiple backends run simultaneously:

  • All backends point to same MANA_CORE_AUTH_URL
  • Token from one backend works in another
  • No auth service conflicts on port 3001
  • JWKS cached or refetched appropriately

Production Readiness

Environment Variables

  • .env file NOT committed
  • .env.example documents all variables
  • All secrets retrieved from environment (not hardcoded)
  • NODE_ENV set to production in prod
  • DEV_BYPASS_AUTH set to false or unset in prod

Security

  • HTTPS used for auth requests (in production)
  • CORS properly configured for frontend domains
  • No auth tokens in logs
  • No user passwords in logs
  • Rate limiting enabled on auth endpoints (mana-core-auth)

Monitoring

  • Logging captures auth failures
  • Metrics track token validation latency
  • Alerts for repeated validation failures
  • JWKS fetch errors monitored

Error Messages

  • Don't reveal implementation details in 401 responses
  • Generic "Unauthorized" message (not "invalid signature" or "token expired")
  • Development logging more verbose than production

Code Review Checklist

When reviewing auth integration PR:

  • Uses only canonical guard (JwtAuthGuard or AuthGuard)
  • No custom JWT parsing or validation code
  • No hardcoded auth URLs (uses ConfigService)
  • No plain-text tokens in logs or responses
  • All protected routes have guard
  • All public routes marked with @Public() (if using Path B)
  • @CurrentUser() used correctly
  • Error handling appropriate (401 for auth errors)
  • Tests cover auth scenarios
  • Documentation updated

Common Issues & Fixes

Issue: "No token provided" on every request

Cause: Guard not applied or incorrectly applied

Fix:

// Check main.ts - guard must be global OR per-controller
app.useGlobalGuards(new JwtAuthGuard(app.get(ConfigService)));

// Verify @UseGuards decorator present
@Controller('api')
@UseGuards(JwtAuthGuard)  // Must be here if not global
export class MyController { ... }

Issue: @CurrentUser() returns undefined

Cause: Guard not running before decorator

Fix:

  1. Ensure guard applied to route/controller
  2. Ensure guard successfully attaches request.user
  3. Check guard implementation:
    request.user = { userId, email, role, sessionId };
    

Issue: Dev bypass not working

Cause: Environment variables not set correctly

Fix:

# Must be EXACT strings
NODE_ENV=development    # NOT 'dev' or 'test'
DEV_BYPASS_AUTH=true    # String 'true', not boolean
DEV_USER_ID=test-123    # Optional, any UUID-like string

Issue: Token validation always fails

Cause: Wrong MANA_CORE_AUTH_URL or service not running

Fix:

# Verify service running
curl http://localhost:3001/api/v1/auth/jwks

# Verify config
echo $MANA_CORE_AUTH_URL  # Should be http://localhost:3001

# Check logs in both services

Sign-Off

Service Name: ___________________________

Backend Port: ___________________________

Integration Path: [ ] A: Lightweight Auth [ ] B: Auth + Credits

Completed By: ___________________________ Date: ___________________________

Reviewed By: ___________________________ Date: ___________________________


Approval Checklist

  • All items in Implementation Checklist verified
  • All items in Testing Checklist verified
  • Code review passed
  • Integration test passed
  • Documentation updated
  • Production-ready configuration verified

Auth Architecture Approved: ___________________________ Date: ___________________________