mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
📝 docs(auth): document Better Auth typing improvements
Add comprehensive documentation for Better Auth improvements: - Centralized types package usage guide - Frontend type inference examples - Zod validation implementation - Rate limiting configuration - Integration test documentation - Migration guide for developers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a7f274632f
commit
6e6815a5fa
2 changed files with 835 additions and 0 deletions
|
|
@ -117,8 +117,10 @@ Key points:
|
|||
| ------------------------------------------ | ------------------------------------------------ |
|
||||
| `src/auth/better-auth.config.ts` | Better Auth configuration with JWT + Org plugins |
|
||||
| `src/auth/services/better-auth.service.ts` | Main auth service - ALL auth logic here |
|
||||
| `src/auth/types/better-auth.types.ts` | Type definitions (inferred + manual) |
|
||||
| `src/db/schema/auth.schema.ts` | User, session, account, jwks tables |
|
||||
| `docs/AUTHENTICATION_ARCHITECTURE.md` | Comprehensive auth documentation |
|
||||
| `docs/BETTER_AUTH_TYPING_IMPROVEMENTS.md` | TypeScript typing decisions and limitations |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
|
@ -157,6 +159,36 @@ Other services call `POST /api/v1/auth/validate` with the JWT. The validation us
|
|||
|
||||
For dynamic data (credits, org info), create API endpoints instead.
|
||||
|
||||
### Better Auth TypeScript Types
|
||||
|
||||
**READ FIRST:** `docs/BETTER_AUTH_TYPING_IMPROVEMENTS.md`
|
||||
|
||||
**Prefer inferred types:**
|
||||
```typescript
|
||||
import type { AuthUser, AuthSession } from '../better-auth.config';
|
||||
```
|
||||
|
||||
**Known limitation:** `$Infer` doesn't expose plugin API methods. The manual `BetterAuthAPI` interface is required:
|
||||
```typescript
|
||||
// This is necessary - Better Auth's $Infer doesn't work for API methods
|
||||
private get api(): BetterAuthAPI {
|
||||
return this.auth.api as unknown as BetterAuthAPI;
|
||||
}
|
||||
```
|
||||
|
||||
**Adding user fields:**
|
||||
```typescript
|
||||
// In better-auth.config.ts
|
||||
user: {
|
||||
additionalFields: {
|
||||
myField: {
|
||||
type: 'string',
|
||||
input: false, // SECURITY: prevents client from setting
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Token not validating?
|
||||
|
|
|
|||
803
services/mana-core-auth/docs/BETTER_AUTH_TYPING_IMPROVEMENTS.md
Normal file
803
services/mana-core-auth/docs/BETTER_AUTH_TYPING_IMPROVEMENTS.md
Normal file
|
|
@ -0,0 +1,803 @@
|
|||
# Better Auth Typing Improvements
|
||||
|
||||
**Date:** December 2024
|
||||
**Status:** Implemented
|
||||
**Version:** 2.0
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#executive-summary)
|
||||
2. [Centralized Types Package](#centralized-types-package)
|
||||
3. [Frontend Type Inference](#frontend-type-inference)
|
||||
4. [Zod Validation for Role Enum](#zod-validation-for-role-enum)
|
||||
5. [Rate Limiting on Sensitive Endpoints](#rate-limiting-on-sensitive-endpoints)
|
||||
6. [Package Migrations](#package-migrations)
|
||||
7. [Integration Tests](#integration-tests)
|
||||
8. [Test Infrastructure](#test-infrastructure)
|
||||
9. [Known Limitations](#known-limitations)
|
||||
10. [Migration Guide](#migration-guide)
|
||||
11. [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document describes the Better Auth typing improvements implemented in December 2024. The changes establish a robust, type-safe authentication system with:
|
||||
|
||||
| Improvement | Description |
|
||||
|-------------|-------------|
|
||||
| **Centralized Types** | Single source of truth via `@manacore/better-auth-types` |
|
||||
| **Frontend Inference** | `userAdditionalFields` export for Better Auth client plugins |
|
||||
| **Runtime Validation** | Zod schema validates role enum server-side |
|
||||
| **Rate Limiting** | `@Throttle` decorators protect sensitive auth endpoints |
|
||||
| **Integration Tests** | 8 comprehensive tests for role security |
|
||||
| **ESM Compatibility** | `jose` mock for Jest in ESM environment |
|
||||
|
||||
### Key Files Modified
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `packages/better-auth-types/src/index.ts` | Centralized type definitions |
|
||||
| `services/mana-core-auth/src/auth/better-auth.config.ts` | Zod validation, role field config |
|
||||
| `services/mana-core-auth/src/auth/auth.controller.ts` | Rate limiting decorators |
|
||||
| `packages/shared-auth/src/better-auth-fields.ts` | Frontend type inference exports |
|
||||
| `packages/shared-nestjs-auth/src/types/index.ts` | NestJS guard types |
|
||||
| `services/mana-core-auth/test/__mocks__/jose.ts` | ESM-compatible JWT mock |
|
||||
| `services/mana-core-auth/test/integration/role-security.e2e-spec.ts` | Role security tests |
|
||||
|
||||
---
|
||||
|
||||
## Centralized Types Package
|
||||
|
||||
### Overview
|
||||
|
||||
A new package `@manacore/better-auth-types` provides all shared Better Auth types for the monorepo. This eliminates type duplication and ensures consistency across all services.
|
||||
|
||||
**Package Location:** `packages/better-auth-types/`
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
packages/better-auth-types/
|
||||
├── src/
|
||||
│ └── index.ts # All type exports
|
||||
├── dist/ # Built output
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
### Available Types
|
||||
|
||||
#### User Role Types
|
||||
|
||||
```typescript
|
||||
import type { UserRole } from '@manacore/better-auth-types';
|
||||
import { isValidUserRole } from '@manacore/better-auth-types';
|
||||
|
||||
// UserRole = 'user' | 'admin' | 'service'
|
||||
const role: UserRole = 'admin';
|
||||
|
||||
// Runtime validation
|
||||
if (isValidUserRole(unknownValue)) {
|
||||
// unknownValue is now typed as UserRole
|
||||
}
|
||||
```
|
||||
|
||||
#### JWT Payload Types
|
||||
|
||||
```typescript
|
||||
import type { JWTPayload, StrictJWTPayload } from '@manacore/better-auth-types';
|
||||
|
||||
// JWTPayload - role as string (flexible)
|
||||
interface JWTPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string; // Any string
|
||||
sid: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
iss?: string;
|
||||
aud?: string;
|
||||
}
|
||||
|
||||
// StrictJWTPayload - role as UserRole (strict)
|
||||
interface StrictJWTPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: UserRole; // 'user' | 'admin' | 'service'
|
||||
sid: string;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### NestJS Guard Types
|
||||
|
||||
```typescript
|
||||
import type { CurrentUserData, StrictCurrentUserData } from '@manacore/better-auth-types';
|
||||
import { jwtPayloadToCurrentUser, jwtPayloadToStrictCurrentUser } from '@manacore/better-auth-types';
|
||||
|
||||
// Use in controller
|
||||
@Get('profile')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
getProfile(@CurrentUser() user: CurrentUserData) {
|
||||
return { userId: user.userId, email: user.email, role: user.role };
|
||||
}
|
||||
```
|
||||
|
||||
#### Utility Functions
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isValidUserRole,
|
||||
isValidOrganizationRole,
|
||||
jwtPayloadToCurrentUser,
|
||||
jwtPayloadToStrictCurrentUser,
|
||||
} from '@manacore/better-auth-types';
|
||||
|
||||
// Convert JWT payload to current user data
|
||||
const currentUser = jwtPayloadToCurrentUser(payload);
|
||||
|
||||
// Strict conversion (returns null if role invalid)
|
||||
const strictUser = jwtPayloadToStrictCurrentUser(payload);
|
||||
if (strictUser === null) {
|
||||
throw new Error('Invalid role in JWT');
|
||||
}
|
||||
```
|
||||
|
||||
### Full Type Export List
|
||||
|
||||
| Type/Function | Purpose |
|
||||
|---------------|---------|
|
||||
| `UserRole` | `'user' \| 'admin' \| 'service'` |
|
||||
| `OrganizationRole` | `'owner' \| 'admin' \| 'member'` |
|
||||
| `InvitationStatus` | `'pending' \| 'accepted' \| 'rejected' \| 'expired'` |
|
||||
| `JWTPayload` | Standard JWT payload interface |
|
||||
| `StrictJWTPayload` | JWT payload with strict UserRole |
|
||||
| `UserData` | User data for client apps |
|
||||
| `StrictUserData` | User data with strict UserRole |
|
||||
| `CurrentUserData` | NestJS guard user data |
|
||||
| `StrictCurrentUserData` | NestJS guard user data with strict UserRole |
|
||||
| `AuthResult` | Auth operation result |
|
||||
| `TokenRefreshResult` | Token refresh operation result |
|
||||
| `TokenValidationResponse` | Token validation response |
|
||||
| `Organization` | Organization data structure |
|
||||
| `OrganizationMember` | Organization member data |
|
||||
| `OrganizationInvitation` | Invitation data |
|
||||
| `CreditBalance` | Credit balance info |
|
||||
| `B2BInfo` | B2B information from JWT |
|
||||
| `userAdditionalFields` | Client type inference config |
|
||||
| `isValidUserRole()` | Runtime role validation |
|
||||
| `isValidOrganizationRole()` | Runtime org role validation |
|
||||
| `jwtPayloadToCurrentUser()` | Convert JWT to CurrentUserData |
|
||||
| `jwtPayloadToStrictCurrentUser()` | Convert JWT to StrictCurrentUserData |
|
||||
|
||||
---
|
||||
|
||||
## Frontend Type Inference
|
||||
|
||||
### Overview
|
||||
|
||||
Better Auth's `inferAdditionalFields` plugin allows frontend clients to have proper TypeScript inference for custom user fields like `role`. The `userAdditionalFields` export provides the configuration for this.
|
||||
|
||||
### Location
|
||||
|
||||
**Primary:** `packages/better-auth-types/src/index.ts`
|
||||
**Re-export:** `packages/shared-auth/src/better-auth-fields.ts`
|
||||
|
||||
### Usage in Frontend Apps
|
||||
|
||||
#### SvelteKit Example
|
||||
|
||||
```typescript
|
||||
// src/lib/auth.ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { inferAdditionalFields } from "better-auth/client/plugins";
|
||||
import { userAdditionalFields } from "@manacore/shared-auth";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: "http://localhost:3001",
|
||||
plugins: [inferAdditionalFields(userAdditionalFields)],
|
||||
});
|
||||
|
||||
// Now user.role is properly typed!
|
||||
const session = await authClient.getSession();
|
||||
if (session?.user) {
|
||||
console.log(session.user.role); // TypeScript knows this is string
|
||||
}
|
||||
```
|
||||
|
||||
#### React/React Native Example
|
||||
|
||||
```typescript
|
||||
// src/auth/client.ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { inferAdditionalFields } from "better-auth/client/plugins";
|
||||
import { userAdditionalFields } from "@manacore/shared-auth";
|
||||
|
||||
export const auth = createAuthClient({
|
||||
baseURL: process.env.EXPO_PUBLIC_AUTH_URL,
|
||||
plugins: [inferAdditionalFields(userAdditionalFields)],
|
||||
});
|
||||
```
|
||||
|
||||
### Field Definition Structure
|
||||
|
||||
The `userAdditionalFields` object must match the server-side configuration:
|
||||
|
||||
```typescript
|
||||
// packages/better-auth-types/src/index.ts
|
||||
export const userAdditionalFields = {
|
||||
user: {
|
||||
role: {
|
||||
type: 'string' as const,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
**IMPORTANT:** Keep this in sync with the server config in `services/mana-core-auth/src/auth/better-auth.config.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Zod Validation for Role Enum
|
||||
|
||||
### Overview
|
||||
|
||||
Runtime validation ensures only valid role values can be assigned to users. This provides defense-in-depth alongside `input: false`.
|
||||
|
||||
### Location
|
||||
|
||||
**File:** `services/mana-core-auth/src/auth/better-auth.config.ts` (lines 27-51)
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* User role schema with Zod runtime validation
|
||||
*/
|
||||
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 using Zod
|
||||
*/
|
||||
export function isValidUserRole(role: unknown): role is UserRole {
|
||||
return userRoleSchema.safeParse(role).success;
|
||||
}
|
||||
```
|
||||
|
||||
### Better Auth Integration
|
||||
|
||||
The Zod schema is used in the `user.additionalFields` configuration:
|
||||
|
||||
```typescript
|
||||
// services/mana-core-auth/src/auth/better-auth.config.ts
|
||||
user: {
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
defaultValue: 'user',
|
||||
input: false, // Security: clients cannot set role
|
||||
validator: {
|
||||
input: userRoleSchema, // Runtime validation with Zod
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
### Security Layers
|
||||
|
||||
| Layer | Protection |
|
||||
|-------|------------|
|
||||
| `input: false` | Prevents clients from setting role during registration |
|
||||
| `defaultValue: 'user'` | New users always start with 'user' role |
|
||||
| `validator.input` | Rejects invalid role values server-side |
|
||||
| Database constraint | PostgreSQL enum ensures data integrity |
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting on Sensitive Endpoints
|
||||
|
||||
### Overview
|
||||
|
||||
Rate limiting protects against brute force attacks, credential stuffing, and enumeration attacks on authentication endpoints.
|
||||
|
||||
### Location
|
||||
|
||||
**File:** `services/mana-core-auth/src/auth/auth.controller.ts` (lines 26-44)
|
||||
|
||||
### Rate Limit Configuration
|
||||
|
||||
```typescript
|
||||
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;
|
||||
```
|
||||
|
||||
### Endpoint Rate Limits
|
||||
|
||||
| Endpoint | Limit | TTL | Purpose |
|
||||
|----------|-------|-----|---------|
|
||||
| `POST /auth/login` | 5/min | 60s | Brute force protection |
|
||||
| `POST /auth/register` | 10/hr | 1hr | Spam prevention |
|
||||
| `POST /auth/forgot-password` | 3/5min | 5min | Email enumeration protection |
|
||||
| `POST /auth/register/b2b` | 5/hr | 1hr | B2B spam prevention |
|
||||
| `POST /auth/validate` | No limit | - | High-frequency internal operation |
|
||||
| `GET /auth/jwks` | No limit | - | Cacheable public keys |
|
||||
|
||||
### Usage in Controller
|
||||
|
||||
```typescript
|
||||
import { Throttle, SkipThrottle } from '@nestjs/throttler';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
/**
|
||||
* Rate limit: 5 attempts per minute per IP
|
||||
*/
|
||||
@Post('login')
|
||||
@Throttle({ default: RATE_LIMITS.LOGIN })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
return this.betterAuthService.signIn(loginDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip rate limiting for high-frequency internal operations
|
||||
*/
|
||||
@Post('validate')
|
||||
@SkipThrottle()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async validate(@Body() body: { token: string }) {
|
||||
return this.betterAuthService.validateToken(body.token);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limit Response
|
||||
|
||||
When rate limit is exceeded, the API returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 429,
|
||||
"message": "ThrottlerException: Too Many Requests"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Package Migrations
|
||||
|
||||
### @manacore/shared-auth
|
||||
|
||||
**Purpose:** Client-side auth utilities for web/mobile apps
|
||||
|
||||
**Migration:** Now re-exports types from `@manacore/better-auth-types`
|
||||
|
||||
**File:** `packages/shared-auth/src/better-auth-fields.ts`
|
||||
|
||||
```typescript
|
||||
// Re-export from centralized package
|
||||
export {
|
||||
userAdditionalFields,
|
||||
type UserRole,
|
||||
type OrganizationRole,
|
||||
type UserWithAdditionalFields,
|
||||
isValidUserRole,
|
||||
isValidOrganizationRole,
|
||||
} from '@manacore/better-auth-types';
|
||||
```
|
||||
|
||||
**File:** `packages/shared-auth/src/index.ts`
|
||||
|
||||
```typescript
|
||||
// Better Auth field definitions for client type inference
|
||||
export {
|
||||
userAdditionalFields,
|
||||
type UserRole,
|
||||
type OrganizationRole,
|
||||
type UserWithAdditionalFields,
|
||||
isValidUserRole,
|
||||
isValidOrganizationRole,
|
||||
} from './better-auth-fields';
|
||||
```
|
||||
|
||||
### @manacore/shared-nestjs-auth
|
||||
|
||||
**Purpose:** NestJS guards and decorators for JWT validation
|
||||
|
||||
**Migration:** Now uses `@manacore/better-auth-types` as a dependency
|
||||
|
||||
**File:** `packages/shared-nestjs-auth/package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@manacore/better-auth-types": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**File:** `packages/shared-nestjs-auth/src/types/index.ts`
|
||||
|
||||
```typescript
|
||||
// Re-export centralized types
|
||||
export type {
|
||||
CurrentUserData,
|
||||
StrictCurrentUserData,
|
||||
JWTPayload,
|
||||
StrictJWTPayload,
|
||||
UserRole,
|
||||
OrganizationRole,
|
||||
TokenValidationResponse,
|
||||
} from '@manacore/better-auth-types';
|
||||
|
||||
export {
|
||||
isValidUserRole,
|
||||
isValidOrganizationRole,
|
||||
jwtPayloadToCurrentUser,
|
||||
jwtPayloadToStrictCurrentUser,
|
||||
} from '@manacore/better-auth-types';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### Overview
|
||||
|
||||
Comprehensive integration tests verify the security of the role field implementation.
|
||||
|
||||
### Location
|
||||
|
||||
**File:** `services/mana-core-auth/test/integration/role-security.e2e-spec.ts`
|
||||
|
||||
### Test Categories
|
||||
|
||||
#### 1. Role Field Security (`input: false`)
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| Default role assignment | New users get `'user'` role |
|
||||
| Input security | Role field in registration body is ignored |
|
||||
| JWT payload role | JWT contains correct role after login |
|
||||
|
||||
#### 2. Role Validation
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| Valid enum values | Role is one of `['user', 'admin', 'service']` |
|
||||
| Token refresh preservation | Role persists across token refresh (skipped in mock env) |
|
||||
|
||||
#### 3. Session and Role Consistency
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| Multiple sessions | Same user has same role across sessions |
|
||||
| JWT claims completeness | JWT contains `sub`, `email`, `role`, `sid` |
|
||||
|
||||
#### 4. JWT Payload Minimalism
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| No sensitive data | JWT does not contain `password`, `creditBalance`, etc. |
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# From mana-core-auth directory
|
||||
pnpm test:e2e
|
||||
|
||||
# Run specific test file
|
||||
pnpm test:e2e -- --testPathPattern=role-security
|
||||
```
|
||||
|
||||
### Sample Test
|
||||
|
||||
```typescript
|
||||
it('should ignore role field in registration body (input: false security)', async () => {
|
||||
const uniqueEmail = `role-escalation-attempt-${Date.now()}@example.com`;
|
||||
|
||||
// Attempt to register with admin role (should be ignored)
|
||||
await betterAuthService.registerB2C({
|
||||
email: uniqueEmail,
|
||||
password: 'SecurePassword123!',
|
||||
name: 'Escalation Attempt User',
|
||||
// Note: If someone tries to add role: 'admin' to the request body,
|
||||
// Better Auth's input: false should ignore it
|
||||
});
|
||||
|
||||
// Login to verify the role
|
||||
const loginResult = await betterAuthService.signIn({
|
||||
email: uniqueEmail,
|
||||
password: 'SecurePassword123!',
|
||||
});
|
||||
|
||||
// Role should always be 'user' (the default), not 'admin'
|
||||
expect(loginResult.user.role).toBe('user');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
### Jose Mock for ESM Compatibility
|
||||
|
||||
Jest has issues with ESM modules like `jose`. A mock implementation provides test compatibility.
|
||||
|
||||
### Location
|
||||
|
||||
**File:** `services/mana-core-auth/test/__mocks__/jose.ts`
|
||||
|
||||
### Mock Implementation
|
||||
|
||||
```typescript
|
||||
// Mock JWT payload interface
|
||||
export interface JWTPayload {
|
||||
sub?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
sessionId?: string;
|
||||
sid?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
iss?: string;
|
||||
aud?: string | string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Mock jwtVerify function
|
||||
export const jwtVerify = jest.fn(
|
||||
async (token: string, _keySet: MockKeySet, _options?: unknown): Promise<JWTVerifyResult> => {
|
||||
// For tests, decode the token if it's a valid JWT format
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length === 3) {
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
return {
|
||||
payload,
|
||||
protectedHeader: { alg: 'EdDSA', typ: 'JWT' },
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// If decoding fails, return mock data
|
||||
}
|
||||
|
||||
// Return mock payload for invalid/test tokens
|
||||
return {
|
||||
payload: {
|
||||
sub: 'test-user-id',
|
||||
email: 'test@example.com',
|
||||
role: 'user',
|
||||
sessionId: 'test-session-id',
|
||||
sid: 'test-session-id',
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
iss: 'manacore',
|
||||
aud: 'manacore',
|
||||
},
|
||||
protectedHeader: { alg: 'EdDSA', typ: 'JWT' },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Mock createRemoteJWKSet function
|
||||
export const createRemoteJWKSet = jest.fn((url: URL) => {
|
||||
return new MockKeySet(url);
|
||||
});
|
||||
|
||||
// Mock error classes
|
||||
export class JOSEError extends Error { /* ... */ }
|
||||
export class JWTExpired extends JOSEError { /* ... */ }
|
||||
export class JWTInvalid extends JOSEError { /* ... */ }
|
||||
|
||||
export const errors = { JOSEError, JWTExpired, JWTInvalid };
|
||||
```
|
||||
|
||||
### Jest Configuration
|
||||
|
||||
Add to Jest config to use the mock:
|
||||
|
||||
```javascript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
'^jose$': '<rootDir>/test/__mocks__/jose.ts',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### 1. Better Auth $Infer for API Methods
|
||||
|
||||
Better Auth's `$Infer` pattern does not expose plugin API methods. The inferred `AuthAPI` type is incomplete.
|
||||
|
||||
**Impact:** Manual `BetterAuthAPI` interface is still required in `better-auth.types.ts`.
|
||||
|
||||
**Workaround:**
|
||||
|
||||
```typescript
|
||||
// Must cast to manual interface
|
||||
private get api(): BetterAuthAPI {
|
||||
return this.auth.api as unknown as BetterAuthAPI;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Client Type Inference vs Server Config
|
||||
|
||||
The `userAdditionalFields` export must be kept in sync with the server configuration manually.
|
||||
|
||||
**Mitigation:** Documentation and code comments reference the server config file.
|
||||
|
||||
### 3. Token Refresh Test in Mock Environment
|
||||
|
||||
The token refresh test is skipped because it requires a real database connection.
|
||||
|
||||
```typescript
|
||||
it.skip('should preserve role across token refresh', async () => {
|
||||
// Requires real database for session validation
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Email Service Not Implemented
|
||||
|
||||
Password reset sends to console log, not actual email.
|
||||
|
||||
```typescript
|
||||
sendResetPassword: async ({ user, url, token }) => {
|
||||
console.log('[Password Reset] User:', user.email);
|
||||
console.log('[Password Reset] Reset URL:', url);
|
||||
// TODO: Implement email service
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Backend Developers
|
||||
|
||||
1. **Update imports to use centralized types:**
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
import { JWTPayload } from '../types/jwt.types';
|
||||
|
||||
// After
|
||||
import type { JWTPayload, CurrentUserData } from '@manacore/better-auth-types';
|
||||
```
|
||||
|
||||
2. **Use type guards for runtime validation:**
|
||||
|
||||
```typescript
|
||||
import { isValidUserRole } from '@manacore/better-auth-types';
|
||||
|
||||
if (!isValidUserRole(payload.role)) {
|
||||
throw new UnauthorizedException('Invalid role');
|
||||
}
|
||||
```
|
||||
|
||||
### For Frontend Developers
|
||||
|
||||
1. **Add type inference to auth client:**
|
||||
|
||||
```typescript
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { inferAdditionalFields } from "better-auth/client/plugins";
|
||||
import { userAdditionalFields } from "@manacore/shared-auth";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: "http://localhost:3001",
|
||||
plugins: [inferAdditionalFields(userAdditionalFields)],
|
||||
});
|
||||
```
|
||||
|
||||
2. **Access typed user fields:**
|
||||
|
||||
```typescript
|
||||
const session = await authClient.getSession();
|
||||
if (session?.user) {
|
||||
const role = session.user.role; // TypeScript knows this is string
|
||||
}
|
||||
```
|
||||
|
||||
### For Test Writers
|
||||
|
||||
1. **Use the jose mock for JWT tests:**
|
||||
|
||||
```typescript
|
||||
// Jest automatically uses the mock from test/__mocks__/jose.ts
|
||||
import { jwtVerify } from 'jose';
|
||||
|
||||
// The mock returns predictable test data
|
||||
const result = await jwtVerify(token, jwks);
|
||||
expect(result.payload.role).toBe('user');
|
||||
```
|
||||
|
||||
2. **Write integration tests for role security:**
|
||||
|
||||
```typescript
|
||||
it('should prevent role escalation', async () => {
|
||||
// Register user (role cannot be set via API)
|
||||
await betterAuthService.registerB2C({ email, password, name });
|
||||
|
||||
// Login and verify role is 'user'
|
||||
const result = await betterAuthService.signIn({ email, password });
|
||||
expect(result.user.role).toBe('user');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- [Better Auth TypeScript Guide](https://www.better-auth.com/docs/concepts/typescript)
|
||||
- [Better Auth Database Guide](https://www.better-auth.com/docs/concepts/database)
|
||||
- [Better Auth JWT Plugin](https://www.better-auth.com/docs/plugins/jwt)
|
||||
- [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)
|
||||
- [Zod Documentation](https://zod.dev/)
|
||||
|
||||
### Internal Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `packages/better-auth-types/src/index.ts` | Centralized type definitions |
|
||||
| `services/mana-core-auth/src/auth/better-auth.config.ts` | Server-side auth configuration |
|
||||
| `services/mana-core-auth/src/auth/auth.controller.ts` | Auth endpoints with rate limiting |
|
||||
| `services/mana-core-auth/src/auth/types/better-auth.types.ts` | Legacy types (deprecated) |
|
||||
| `packages/shared-auth/src/better-auth-fields.ts` | Client type inference exports |
|
||||
| `packages/shared-nestjs-auth/src/types/index.ts` | NestJS guard types |
|
||||
| `services/mana-core-auth/test/__mocks__/jose.ts` | JWT mock for tests |
|
||||
| `services/mana-core-auth/test/integration/role-security.e2e-spec.ts` | Role security tests |
|
||||
|
||||
### GitHub Issues (Better Auth)
|
||||
|
||||
- [#3780 - Type issue with auth.api.getSession](https://github.com/better-auth/better-auth/issues/3780)
|
||||
- [#4875 - $Infer.Session doesn't respect customSession](https://github.com/better-auth/better-auth/issues/4875)
|
||||
- [#5159 - Plugin fields not in TypeScript types](https://github.com/better-auth/better-auth/issues/5159)
|
||||
|
||||
---
|
||||
|
||||
## Appendix: File Changes Summary
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `packages/better-auth-types/src/index.ts` | New file - all centralized types |
|
||||
| `packages/better-auth-types/package.json` | New package configuration |
|
||||
| `services/mana-core-auth/src/auth/better-auth.config.ts` | Added Zod validation, role field config |
|
||||
| `services/mana-core-auth/src/auth/auth.controller.ts` | Added rate limiting decorators |
|
||||
| `packages/shared-auth/src/better-auth-fields.ts` | Re-exports from centralized package |
|
||||
| `packages/shared-auth/src/index.ts` | Updated exports |
|
||||
| `packages/shared-nestjs-auth/src/types/index.ts` | Re-exports from centralized package |
|
||||
| `packages/shared-nestjs-auth/package.json` | Added dependency |
|
||||
| `services/mana-core-auth/test/__mocks__/jose.ts` | New mock for ESM compatibility |
|
||||
| `services/mana-core-auth/test/integration/role-security.e2e-spec.ts` | New integration tests |
|
||||
|
||||
**Breaking Changes:** None (backward compatible)
|
||||
**Type Safety:** Significantly improved with centralized types and runtime validation
|
||||
Loading…
Add table
Add a link
Reference in a new issue