managarten/services/mana-core-auth/APPLY_SECURITY_FIXES.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

10 KiB

Apply Security Fixes - Quick Start Guide

This guide provides the quickest path to implementing all critical security fixes.

Pre-Flight Checklist

  • Backup current code: git stash or create a branch
  • Review the complete analysis: docs/MANA_CORE_AUTH_ANALYSIS.md
  • Review implementation guide: docs/SECURITY_FIXES_IMPLEMENTATION_GUIDE.md

Fix 1: JWT Fallback - MANUAL EDIT REQUIRED ⚠️

The JWT fallback code needs to be manually edited because the file has been recently modified.

File: src/auth/services/better-auth.service.ts Lines: ~449-508

Find this block (search for "Generate JWT access token"):

// Generate JWT access token using Better Auth's JWT plugin
let accessToken = '';
try {
	const jwtResult = await this.api.signJWT({
		// ... lots of code ...
	});
	// ... fallback code with RS256 ...
} catch (jwtError) {
	// Manual JWT generation fallback
}

Replace entire block with:

// Generate JWT access token using Better Auth's JWT plugin
const jwtResult = await this.api.signJWT({
	body: {
		payload: {
			sub: user.id,
			email: user.email,
			role: (user as BetterAuthUser).role || 'user',
			sid: session?.id || '',
		},
	},
	headers: {
		authorization: `Bearer ${sessionToken}`,
	},
});

const accessToken = jwtResult?.token;

if (!accessToken) {
	throw new UnauthorizedException('Failed to generate access token');
}

Verification:

cd services/mana-core-auth
pnpm start:dev
# Test login, check console for EdDSA tokens

File: src/auth/better-auth.config.ts Line: ~148

Run this command to apply:

cd services/mana-core-auth

# Backup first
cp src/auth/better-auth.config.ts src/auth/better-auth.config.ts.backup

# Then manually edit or use this patch

Manual edit: Find session: block, add after updateAge:

session: {
	expiresIn: 60 * 60 * 24 * 7,
	updateAge: 60 * 60 * 24,

	// ✅ ADD THIS BLOCK:
	cookieCache: {
		enabled: true,
		maxAge: 5 * 60, // 5 minutes
		strategy: "jwe", // Encrypted
		refreshCache: true,
	}
},

Verification:

# Check response headers after login
curl -v http://localhost:3001/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@test.com","password":"yourpassword"}' \
  | grep -i "set-cookie"

Fix 3: Remember Me Feature - MULTI-STEP

Step 3a: Schema Change

File: src/db/schema/auth.schema.ts Line: ~32 (in sessions table)

Add this field:

export const sessions = authSchema.table('sessions', {
	// ... existing fields ...

	// ✅ ADD THIS:
	rememberMe: boolean('remember_me').default(false),
});

Step 3b: Run Migration

cd services/mana-core-auth
pnpm db:generate
pnpm db:migrate

Step 3c: Update DTO

File: src/auth/dto/login.dto.ts

import { IsEmail, IsString, MinLength, IsOptional, IsBoolean } from 'class-validator';

export class LoginDto {
	@IsEmail()
	email: string;

	@IsString()
	@MinLength(12) // ✅ FIXED: was 8
	password: string;

	@IsOptional()
	@IsString()
	deviceId?: string;

	@IsOptional()
	@IsString()
	deviceName?: string;

	// ✅ NEW:
	@IsOptional()
	@IsBoolean()
	rememberMe?: boolean;

	@IsOptional()
	@IsString()
	ipAddress?: string;

	@IsOptional()
	@IsString()
	userAgent?: string;
}

Step 3d: Update signIn Method

File: src/auth/services/better-auth.service.ts

After line 447 (after const sessionToken = ...), add:

// Adjust session expiration based on rememberMe
if (dto.rememberMe && session?.id) {
	const db = getDb(this.databaseUrl);
	const { sessions } = await import('../../db/schema');
	const { eq } = await import('drizzle-orm');

	const extendedExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

	await db
		.update(sessions)
		.set({
			expiresAt: extendedExpiresAt,
			rememberMe: true,
		})
		.where(eq(sessions.id, session.id));
}

Fix 4: Security Logging - NEW FILES

Step 4a: Create SecurityEventsService

mkdir -p services/mana-core-auth/src/security

Create file: src/security/security-events.service.ts

cat > services/mana-core-auth/src/security/security-events.service.ts << 'EOF'
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getDb } from '../db/connection';
import { securityEvents } from '../db/schema/auth.schema';
import { randomUUID } from 'crypto';

export type SecurityEventType =
	| 'login_success'
	| 'login_failure'
	| 'logout'
	| 'password_change'
	| 'account_created'
	| 'token_refresh'
	| 'token_validation_failure';

export interface LogSecurityEventParams {
	userId?: string;
	eventType: SecurityEventType;
	ipAddress?: string;
	userAgent?: string;
	metadata?: Record<string, unknown>;
}

@Injectable()
export class SecurityEventsService {
	private databaseUrl: string;

	constructor(private configService: ConfigService) {
		this.databaseUrl = this.configService.get<string>('database.url')!;
	}

	async logEvent(params: LogSecurityEventParams): Promise<void> {
		try {
			const db = getDb(this.databaseUrl);

			await db.insert(securityEvents).values({
				id: randomUUID(),
				userId: params.userId || null,
				eventType: params.eventType,
				ipAddress: params.ipAddress || null,
				userAgent: params.userAgent || null,
				metadata: params.metadata || null,
				createdAt: new Date(),
			});
		} catch (error) {
			console.error('[SecurityEventsService] Failed to log security event:', error);
		}
	}
}
EOF

Step 4b: Create SecurityModule

Create file: src/security/security.module.ts

cat > services/mana-core-auth/src/security/security.module.ts << 'EOF'
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SecurityEventsService } from './security-events.service';

@Module({
	imports: [ConfigModule],
	providers: [SecurityEventsService],
	exports: [SecurityEventsService],
})
export class SecurityModule {}
EOF

Step 4c: Add to AppModule

File: src/app.module.ts

Add import:

import { SecurityModule } from './security/security.module';

Add to imports array:

@Module({
	imports: [
		// ... existing imports ...
		SecurityModule, // ✅ ADD THIS
	],
})

Step 4d: Inject into BetterAuthService

File: src/auth/services/better-auth.service.ts

Add import:

import { SecurityEventsService } from '../../security/security-events.service';

Add to constructor:

constructor(
	private configService: ConfigService,
	private securityEventsService: SecurityEventsService, // ✅ ADD THIS
	// ... other services
) {
	// ...
}

Add logging after successful login (after line ~519):

// Log successful login
await this.securityEventsService
	.logEvent({
		userId: user.id,
		eventType: 'login_success',
		ipAddress: dto.ipAddress,
		userAgent: dto.userAgent,
		metadata: { deviceId: dto.deviceId, rememberMe: dto.rememberMe },
	})
	.catch((err) => console.error('Failed to log login success:', err));

Add logging for failed login (in catch block):

// Log failed login
await this.securityEventsService
	.logEvent({
		eventType: 'login_failure',
		ipAddress: dto.ipAddress,
		userAgent: dto.userAgent,
		metadata: { email: dto.email },
	})
	.catch((err) => console.error('Failed to log login failure:', err));

Fix 5: Security Headers - APPLY TO MAIN.TS

File: src/main.ts

Replace existing helmet() call with this:

// Comprehensive security headers
app.use(
	helmet({
		strictTransportSecurity: {
			maxAge: 31536000,
			includeSubDomains: true,
			preload: true,
		},
		contentSecurityPolicy: {
			directives: {
				defaultSrc: ["'self'"],
				styleSrc: ["'self'", "'unsafe-inline'"],
				scriptSrc: ["'self'"],
				imgSrc: ["'self'", 'data:', 'https:'],
				connectSrc: ["'self'"],
				fontSrc: ["'self'", 'data:'],
				objectSrc: ["'none'"],
				mediaSrc: ["'self'"],
				frameSrc: ["'none'"],
			},
		},
		frameguard: { action: 'deny' },
		noSniff: true,
		xssFilter: true,
		referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
		crossOriginResourcePolicy: { policy: 'cross-origin' },
		crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
		hidePoweredBy: true,
	})
);

Add HTTPS enforcement (after helmet, before other middleware):

// HTTPS enforcement in production
if (process.env.NODE_ENV === 'production') {
	app.use((req: any, res: any, next: any) => {
		const protocol = req.header('x-forwarded-proto') || req.protocol;
		if (protocol !== 'https') {
			return res.redirect(301, `https://${req.header('host')}${req.url}`);
		}
		next();
	});
}

Testing Checklist

After applying all fixes:

# 1. Build
cd services/mana-core-auth
pnpm build

# 2. Start
pnpm start:dev

# 3. Test login
curl -X POST http://localhost:3001/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@test.com","password":"test123456789","rememberMe":true}'

# 4. Check token algorithm
# (Should be EdDSA, not RS256)

# 5. Check security events table
psql $DATABASE_URL -c "SELECT * FROM auth.security_events ORDER BY created_at DESC LIMIT 5;"

# 6. Check session with rememberMe
psql $DATABASE_URL -c "SELECT id, user_id, remember_me, expires_at FROM auth.sessions ORDER BY created_at DESC LIMIT 5;"

Rollback if Needed

# Restore backups
git restore .

# Or if you made backups:
cp src/auth/better-auth.config.ts.backup src/auth/better-auth.config.ts

# Revert migration
pnpm db:drop
pnpm db:push

Success Criteria

JWT Fix: Login generates EdDSA tokens (not RS256) Cookie Cache: Response includes encrypted session cookie Remember Me: Can login with 30-day session Security Logging: Events appear in auth.security_events Security Headers: HSTS, CSP headers present in responses


Get Help

If you encounter issues:

  1. Check the detailed guide: docs/SECURITY_FIXES_IMPLEMENTATION_GUIDE.md
  2. Check the analysis: docs/MANA_CORE_AUTH_ANALYSIS.md
  3. Review Better Auth docs: https://www.better-auth.com/docs

🏗️ ManaCore Monorepo