Assist_Design/docs/auth/AUTH-SCHEMA-IMPROVEMENTS.md
barsa 2266167467 Enhance JWT handling and authentication flow
- Introduced support for previous JWT secrets in the environment configuration to facilitate key rotation.
- Refactored the JoseJwtService to manage multiple signing and verification keys, improving security during token validation.
- Updated the AuthTokenService to include family identifiers for refresh tokens, enhancing session management and security.
- Modified the PasswordWorkflowService and SignupWorkflowService to return session metadata instead of token strings, aligning with security best practices.
- Improved error handling and token revocation logic in the TokenBlacklistService and AuthTokenService to prevent replay attacks.
- Updated documentation to reflect changes in the authentication architecture and security model.
2025-12-12 15:29:58 +09:00

7.4 KiB

Auth Schema Improvements - Explanation

Problems Identified and Fixed

1. z.unknown() Type Safety Issue

Problem:

// BEFORE - NO TYPE SAFETY
export const signupResultSchema = z.object({
  user: z.unknown(), // ❌ Loses all type information and validation
  tokens: authTokensSchema,
});

Why it was wrong:

  • z.unknown() provides zero validation at runtime
  • TypeScript can't infer proper types from it
  • Defeats the purpose of schema-first architecture
  • Any object could pass validation, even malformed data

Solution:

// AFTER - FULL TYPE SAFETY
export const userProfileSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(["USER", "ADMIN"]),
  // ... all fields properly typed
});

export const signupResultSchema = z.object({
  user: userProfileSchema, // ✅ Full validation and type inference
  tokens: authTokensSchema,
});

Benefits:

  • Runtime validation of user data structure
  • Proper TypeScript type inference
  • Catches invalid data early
  • Self-documenting API contract

2. Email Validation Issues

Problem: The z.string().email() in checkPasswordNeededResponseSchema was correct, but using z.unknown() elsewhere meant emails weren't being validated in user objects.

Solution:

export const userProfileSchema = z.object({
  email: z.string().email(), // ✅ Validates email format
  // ...
});

Now all user objects have their emails properly validated.


3. UserProfile Alias Confusion 🤔

Problem:

export type UserProfile = AuthenticatedUser;

This created confusion about why we have two names for the same thing.

Solution - Added Clear Documentation:

/**
 * UserProfile type alias
 *
 * Note: This is an alias for backward compatibility.
 * Both types represent the same thing: a complete user profile with auth state.
 *
 * Architecture:
 * - PortalUser: Only auth state from portal DB (id, email, role, emailVerified, etc.)
 * - CustomerProfile: Profile data from WHMCS (firstname, lastname, address, etc.)
 * - AuthenticatedUser/UserProfile: CustomerProfile + auth state = complete user
 */
export type UserProfile = AuthenticatedUser;

Why the alias exists:

  • Backward compatibility: Code may use either name
  • Domain language: "UserProfile" is more business-friendly than "AuthenticatedUser"
  • Convention: Many authentication libraries use "UserProfile"

Architecture Explanation

Three-Layer User Model

┌─────────────────────────────────────────────┐
│         AuthenticatedUser / UserProfile     │
│              (Complete User)                │
│  ┌────────────────────────────────────────┐ │
│  │      CustomerProfile (WHMCS)           │ │
│  │  - firstname, lastname                 │ │
│  │  - address, phone                      │ │
│  │  - language, currency                  │ │
│  └────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────┐ │
│  │      PortalUser (Portal DB)            │ │
│  │  - id, email                           │ │
│  │  - role, emailVerified, mfaEnabled     │ │
│  │  - lastLoginAt                         │ │
│  └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

1. PortalUser (Portal Database)

  • Purpose: Authentication state only
  • Source: Portal's Prisma database
  • Fields: id, email, role, emailVerified, mfaEnabled, lastLoginAt
  • Schema: portalUserSchema
  • Use Cases: JWT validation, token refresh, auth checks

2. CustomerProfile (WHMCS)

  • Purpose: Business profile data
  • Source: WHMCS API (single source of truth for profile data)
  • Fields: firstname, lastname, address, phone, language, currency
  • Schema: Interface only (external system)
  • Use Cases: Displaying user info, profile updates

3. AuthenticatedUser / UserProfile (Combined)

  • Purpose: Complete user representation
  • Source: Portal DB + WHMCS (merged)
  • Fields: All fields from PortalUser + CustomerProfile
  • Schema: userProfileSchema (for validation)
  • Use Cases: API responses, full user context

Why This Matters

Before (with z.unknown()):

// Could pass completely invalid data ❌
const badData = {
  user: { totally: "wrong", structure: true },
  tokens: {
    /* ... */
  },
};
// Would validate successfully! ❌

After (with proper schema):

// Validates structure correctly ✅
const goodData = {
  user: {
    id: "uuid-here",
    email: "valid@email.com",
    role: "USER",
    // ... all required fields
  },
  tokens: {
    /* ... */
  },
};
// Validates ✅

const badData = {
  user: { wrong: "structure" },
  tokens: {
    /* ... */
  },
};
// Throws validation error ✅

Implementation Details

Schema Definition Location

The userProfileSchema is defined before it's used:

// 1. First, define the schema
export const userProfileSchema = z.object({
  // All fields defined
});

// 2. Then use it in response schemas
export const authResponseSchema = z.object({
  user: userProfileSchema, // ✅ Now defined
  tokens: authTokensSchema,
});

Type Inference

TypeScript now properly infers types:

// Type is inferred from schema
type InferredUser = z.infer<typeof userProfileSchema>;

// Results in proper type:
// {
//   id: string;
//   email: string;
//   role: "USER" | "ADMIN";
//   emailVerified: boolean;
//   // ... etc
// }

Migration Impact

⚠️ Potential Breaking Change (Security Improvement)

Auth endpoints now return session metadata instead of token strings:

  • Before: { user, tokens: { accessToken, refreshToken, expiresAt, refreshExpiresAt } }
  • After: { user, session: { expiresAt, refreshExpiresAt } }

Token strings are delivered via httpOnly cookies only.

Improved Type Safety

  • Better autocomplete in IDEs
  • Compile-time error checking
  • Runtime validation of API responses

Better Documentation

  • Schema serves as living documentation
  • Clear separation of concerns
  • Self-describing data structures

Summary

Issue Before After
User validation z.unknown() userProfileSchema
Type safety None Full
Runtime validation None Complete
Documentation Unclear Self-documenting
Email validation Partial Complete
UserProfile clarity Confusing Documented

Result: Proper schema-first architecture with full type safety and validation! 🎉