mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
🔒️ feat(auth): add Zod validation and endpoint rate limiting
Security improvements for Better Auth: - Add Zod schema for runtime role validation (user/admin/service) - Add rate limiting to sensitive endpoints: - Login: 5 requests/minute - Register: 10 requests/hour - Password reset: 3 requests/5 minutes - B2B register: 5 requests/hour - Skip rate limiting for /validate and /jwks endpoints
This commit is contained in:
parent
26ca921158
commit
fff2819b59
4 changed files with 162 additions and 13 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle, SkipThrottle } from '@nestjs/throttler';
|
||||
import { BetterAuthService } from './services/better-auth.service';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
|
|
@ -22,6 +23,26 @@ import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
|||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
|
||||
/**
|
||||
* Rate Limiting Configuration
|
||||
*
|
||||
* Security-focused rate limits for auth endpoints:
|
||||
* - Login: 5 attempts per minute (brute force protection)
|
||||
* - Register: 10 per hour (spam prevention)
|
||||
* - Password reset: 3 per 5 minutes (enumeration protection)
|
||||
* - Token validation: No limit (high-frequency internal operation)
|
||||
*/
|
||||
const RATE_LIMITS = {
|
||||
/** 5 login attempts per 60 seconds */
|
||||
LOGIN: { limit: 5, ttl: 60000 },
|
||||
/** 10 registrations per hour */
|
||||
REGISTER: { limit: 10, ttl: 3600000 },
|
||||
/** 3 password reset requests per 5 minutes */
|
||||
PASSWORD_RESET: { limit: 3, ttl: 300000 },
|
||||
/** 5 B2B registrations per hour */
|
||||
B2B_REGISTER: { limit: 5, ttl: 3600000 },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Auth Controller
|
||||
*
|
||||
|
|
@ -55,8 +76,11 @@ export class AuthController {
|
|||
* Register a new B2C user (individual)
|
||||
*
|
||||
* Creates a user account and initializes their credit balance.
|
||||
*
|
||||
* Rate limit: 10 registrations per hour per IP (spam prevention)
|
||||
*/
|
||||
@Post('register')
|
||||
@Throttle({ default: RATE_LIMITS.REGISTER })
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
return this.betterAuthService.registerB2C({
|
||||
email: registerDto.email,
|
||||
|
|
@ -69,8 +93,11 @@ export class AuthController {
|
|||
* Sign in with email and password
|
||||
*
|
||||
* Returns user data and JWT token.
|
||||
*
|
||||
* Rate limit: 5 attempts per minute per IP (brute force protection)
|
||||
*/
|
||||
@Post('login')
|
||||
@Throttle({ default: RATE_LIMITS.LOGIN })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
return this.betterAuthService.signIn({
|
||||
|
|
@ -121,8 +148,11 @@ export class AuthController {
|
|||
* Validate a token
|
||||
*
|
||||
* Checks if a token is valid and returns the payload.
|
||||
*
|
||||
* No rate limiting - high-frequency internal operation used by all backends.
|
||||
*/
|
||||
@Post('validate')
|
||||
@SkipThrottle()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async validate(@Body() body: { token: string }) {
|
||||
return this.betterAuthService.validateToken(body.token);
|
||||
|
|
@ -133,8 +163,11 @@ export class AuthController {
|
|||
*
|
||||
* Returns public keys for JWT verification.
|
||||
* This is a passthrough to Better Auth's JWKS.
|
||||
*
|
||||
* No rate limiting - cacheable public keys for JWT verification.
|
||||
*/
|
||||
@Get('jwks')
|
||||
@SkipThrottle()
|
||||
async getJwks() {
|
||||
return this.betterAuthService.getJwks();
|
||||
}
|
||||
|
|
@ -148,8 +181,11 @@ export class AuthController {
|
|||
*
|
||||
* Initiates the password reset flow by sending an email with a reset link.
|
||||
* Always returns success to prevent email enumeration attacks.
|
||||
*
|
||||
* Rate limit: 3 requests per 5 minutes per IP (enumeration protection)
|
||||
*/
|
||||
@Post('forgot-password')
|
||||
@Throttle({ default: RATE_LIMITS.PASSWORD_RESET })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
|
||||
return this.betterAuthService.requestPasswordReset(
|
||||
|
|
@ -181,8 +217,11 @@ export class AuthController {
|
|||
*
|
||||
* Creates an organization with the registering user as owner.
|
||||
* Also creates organization credit balance.
|
||||
*
|
||||
* Rate limit: 5 B2B registrations per hour per IP (spam prevention)
|
||||
*/
|
||||
@Post('register/b2b')
|
||||
@Throttle({ default: RATE_LIMITS.B2B_REGISTER })
|
||||
async registerB2B(@Body() registerDto: RegisterB2BDto) {
|
||||
return this.betterAuthService.registerB2B(registerDto);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,37 @@ import { betterAuth } from 'better-auth';
|
|||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||
import { jwt } from 'better-auth/plugins/jwt';
|
||||
import { organization } from 'better-auth/plugins/organization';
|
||||
import { z } from 'zod';
|
||||
import { getDb } from '../db/connection';
|
||||
import { organizations, members, invitations } from '../db/schema/organizations.schema';
|
||||
import { users, sessions, accounts, verificationTokens, jwks } from '../db/schema/auth.schema';
|
||||
import type { JWTPayloadContext } from './types/better-auth.types';
|
||||
|
||||
/**
|
||||
* User role schema with Zod runtime validation
|
||||
*
|
||||
* Ensures only valid role values can be assigned to users.
|
||||
* This provides defense-in-depth alongside `input: false`.
|
||||
*
|
||||
* Valid roles:
|
||||
* - 'user': Standard user (default)
|
||||
* - 'admin': Administrator with elevated privileges
|
||||
* - 'service': Service account for automated systems
|
||||
*/
|
||||
export const userRoleSchema = z.enum(['user', 'admin', 'service'], {
|
||||
errorMap: () => ({ message: 'Invalid user role. Must be one of: user, admin, service' }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Inferred TypeScript type from Zod schema
|
||||
*/
|
||||
export type UserRole = z.infer<typeof userRoleSchema>;
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a valid UserRole
|
||||
*/
|
||||
export function isValidUserRole(role: unknown): role is UserRole {
|
||||
return userRoleSchema.safeParse(role).success;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT Custom Payload Interface
|
||||
|
|
@ -121,6 +148,39 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
// Base URL for callbacks and redirects
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3001',
|
||||
|
||||
/**
|
||||
* User Additional Fields
|
||||
*
|
||||
* Define custom user fields that Better Auth should be aware of.
|
||||
* This enables proper type inference via $Infer pattern.
|
||||
*
|
||||
* @see https://www.better-auth.com/docs/concepts/database#additional-fields
|
||||
*/
|
||||
user: {
|
||||
additionalFields: {
|
||||
/**
|
||||
* User role (user, admin, service)
|
||||
*
|
||||
* Security:
|
||||
* - input=false prevents clients from setting their own role
|
||||
* - Zod validator ensures only valid role values are accepted
|
||||
*
|
||||
* Roles must be assigned server-side only.
|
||||
*
|
||||
* @see userRoleSchema for valid values
|
||||
*/
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
defaultValue: 'user',
|
||||
input: false, // Clients cannot set role during registration
|
||||
validator: {
|
||||
input: userRoleSchema, // Runtime validation with Zod
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Plugins
|
||||
plugins: [
|
||||
/**
|
||||
|
|
@ -205,11 +265,11 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
*
|
||||
* Only includes static user info that doesn't change frequently.
|
||||
*/
|
||||
definePayload({ user, session }: JWTPayloadContext) {
|
||||
definePayload({ user, session }) {
|
||||
return {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: (user as { role?: string }).role || 'user',
|
||||
role: user.role ?? 'user',
|
||||
sid: session.id,
|
||||
};
|
||||
},
|
||||
|
|
@ -223,3 +283,23 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
* Export type for Better Auth instance
|
||||
*/
|
||||
export type BetterAuthInstance = ReturnType<typeof createBetterAuth>;
|
||||
|
||||
/**
|
||||
* Inferred types from Better Auth instance
|
||||
*
|
||||
* These types are automatically derived from the auth configuration,
|
||||
* including all plugins and additional fields. Use these instead of
|
||||
* manual interface definitions.
|
||||
*
|
||||
* @see https://www.better-auth.com/docs/concepts/typescript
|
||||
*/
|
||||
export type AuthSession = BetterAuthInstance['$Infer']['Session'];
|
||||
export type AuthUser = AuthSession['user'];
|
||||
|
||||
/**
|
||||
* Inferred API type from Better Auth instance
|
||||
*
|
||||
* This type includes all methods from core auth and plugins
|
||||
* (organization, jwt, etc.). Use this instead of manual BetterAuthAPI interface.
|
||||
*/
|
||||
export type AuthAPI = BetterAuthInstance['api'];
|
||||
|
|
|
|||
|
|
@ -55,8 +55,11 @@ import type {
|
|||
TokenPayload,
|
||||
OrganizationMember,
|
||||
Organization,
|
||||
BetterAuthAPI,
|
||||
CreateOrganizationResponse,
|
||||
// BetterAuthAPI provides typed methods for all Better Auth operations
|
||||
// Note: AuthAPI (inferred) is also available but doesn't expose all methods
|
||||
BetterAuthAPI,
|
||||
// BetterAuthUser includes the role field (deprecated - use AuthUser when $Infer works)
|
||||
BetterAuthUser,
|
||||
} from '../types/better-auth.types';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
|
@ -87,8 +90,14 @@ export class BetterAuthService {
|
|||
|
||||
/**
|
||||
* Typed accessor for Better Auth API methods
|
||||
* Better Auth's plugins add methods dynamically, so we provide
|
||||
* a typed accessor to avoid casting throughout the service.
|
||||
*
|
||||
* Better Auth's plugins add methods dynamically. This accessor provides
|
||||
* typed access to all API methods including those from organization
|
||||
* and JWT plugins.
|
||||
*
|
||||
* Note: We use BetterAuthAPI interface which is manually maintained.
|
||||
* In the future, this could be replaced with inferred types once
|
||||
* Better Auth's $Infer fully supports API type inference.
|
||||
*
|
||||
* @see https://www.better-auth.com/docs/concepts/typescript
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@
|
|||
*
|
||||
* This file provides types for Better Auth integration.
|
||||
*
|
||||
* STRATEGY: Import base types from Better Auth packages, extend only when needed.
|
||||
* STRATEGY (2024-12 UPDATE):
|
||||
* Use inferred types from Better Auth's $Infer pattern when possible.
|
||||
* Manual interfaces are kept only for service-layer DTOs and result types.
|
||||
*
|
||||
* From 'better-auth/types':
|
||||
* - User, Session, Account, Auth, BetterAuthOptions, etc.
|
||||
* INFERRED TYPES (prefer these):
|
||||
* - AuthUser, AuthSession, AuthAPI - from better-auth.config.ts
|
||||
*
|
||||
* From 'better-auth/plugins/organization':
|
||||
* - Organization, Member, Invitation, OrganizationRole, InvitationStatus
|
||||
*
|
||||
* This file defines:
|
||||
* 1. Extended types (adding fields Better Auth doesn't have)
|
||||
* 1. Re-exports of inferred types from config
|
||||
* 2. API response/request types for our service layer
|
||||
* 3. Service-specific DTOs and result types
|
||||
* 4. Type guards for runtime safety
|
||||
|
|
@ -22,7 +24,12 @@
|
|||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Import core types from Better Auth packages
|
||||
// Import inferred types from Better Auth config
|
||||
// =============================================================================
|
||||
import type { AuthUser, AuthSession, AuthAPI } from '../better-auth.config';
|
||||
|
||||
// =============================================================================
|
||||
// Import base types from Better Auth packages
|
||||
// =============================================================================
|
||||
import type { User, Session } from 'better-auth/types';
|
||||
import type {
|
||||
|
|
@ -33,6 +40,9 @@ import type {
|
|||
InvitationStatus as BetterAuthInvitationStatus,
|
||||
} from 'better-auth/plugins/organization';
|
||||
|
||||
// Re-export inferred types as primary types
|
||||
export type { AuthUser, AuthSession, AuthAPI };
|
||||
|
||||
// Re-export base types for convenience
|
||||
export type { User, Session };
|
||||
export type {
|
||||
|
|
@ -45,7 +55,9 @@ export type {
|
|||
|
||||
/**
|
||||
* Extended User type with our additional fields
|
||||
* Better Auth's User type is the base, we extend it for our app
|
||||
*
|
||||
* @deprecated Use AuthUser (inferred from config) instead.
|
||||
* This type is kept for backward compatibility but may be removed in future.
|
||||
*/
|
||||
export interface BetterAuthUser extends User {
|
||||
role?: string;
|
||||
|
|
@ -53,7 +65,9 @@ export interface BetterAuthUser extends User {
|
|||
|
||||
/**
|
||||
* Extended Session type with organization support
|
||||
* Better Auth's Session type is the base, organization plugin adds activeOrganizationId
|
||||
*
|
||||
* @deprecated Use AuthSession (inferred from config) instead.
|
||||
* This type is kept for backward compatibility but may be removed in future.
|
||||
*/
|
||||
export interface BetterAuthSession extends Session {
|
||||
activeOrganizationId?: string | null;
|
||||
|
|
@ -62,6 +76,9 @@ export interface BetterAuthSession extends Session {
|
|||
|
||||
/**
|
||||
* JWT Payload context passed to definePayload
|
||||
*
|
||||
* @deprecated Better Auth now infers this type automatically from config.
|
||||
* This type is kept for backward compatibility but may be removed in future.
|
||||
*/
|
||||
export interface JWTPayloadContext {
|
||||
user: BetterAuthUser;
|
||||
|
|
@ -266,6 +283,10 @@ export interface AuthenticatedRequest<TBody = unknown, TQuery = unknown> {
|
|||
/**
|
||||
* Typed Better Auth API interface
|
||||
*
|
||||
* @deprecated Use AuthAPI (inferred from config) instead.
|
||||
* This interface is manually maintained and may become out of sync with Better Auth.
|
||||
* The inferred type from BetterAuthInstance['api'] is always accurate.
|
||||
*
|
||||
* This interface describes the methods available on auth.api
|
||||
* when using the organization plugin.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue