mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
- AUTHENTICATION_ARCHITECTURE.md: JWT flow, EdDSA vs RS256, JWKS usage - CLAUDE.md: Guidelines to always use Better Auth native features - Common mistakes and fixes documented - Developer checklist for auth changes
13 KiB
13 KiB
Authentication Architecture
Decision Date: December 2024 Status: Active Last Updated: December 1, 2024
Overview
Mana Core Auth uses Better Auth as the authentication framework. This document explains the architecture, common pitfalls, and how to correctly implement authentication.
⚠️ CRITICAL: Always Use Better Auth Native Features
DO NOT implement custom JWT signing/verification. Better Auth handles everything.
Better Auth Provides:
- ✅ JWT signing with EdDSA (via JWT plugin)
- ✅ JWKS endpoint for public keys
- ✅ Session management
- ✅ Organization/multi-tenant support
- ✅ Token refresh
DO NOT:
- ❌ Use
jsonwebtokenlibrary for signing (Better Auth usesjosewith EdDSA) - ❌ Configure RS256 keys in
.env(Better Auth uses EdDSA with auto-generated keys) - ❌ Implement custom JWKS endpoints (Better Auth exposes
/api/auth/jwks) - ❌ Store JWT keys manually (Better Auth stores them in
jwkstable)
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ MANA CORE AUTH │
│ (localhost:3001) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ Better Auth │ │ JWT Plugin │ │ Organization │ │
│ │ (Core) │ │ (EdDSA) │ │ Plugin │ │
│ │ │ │ │ │ │ │
│ │ - Sign Up │ │ - Sign JWT │ │ - Create Org │ │
│ │ - Sign In │ │ - Verify JWT │ │ - Invite │ │
│ │ - Sessions │ │ - JWKS Endpoint │ │ - Roles │ │
│ └─────────────────┘ └──────────────────┘ └────────────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
│ ┌─────────────▼─────────────┐ │
│ │ PostgreSQL (auth) │ │
│ │ │ │
│ │ - users │ │
│ │ - sessions │ │
│ │ - accounts │ │
│ │ - jwks (EdDSA keys) │ │
│ │ - organizations │ │
│ │ - members │ │
│ └───────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
│
│ JWT (EdDSA)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT SERVICES │
│ (Chat Backend, Mobile App, Web App) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Client sends JWT in Authorization header │
│ 2. Service calls POST /api/v1/auth/validate │
│ 3. mana-core-auth verifies via JWKS (EdDSA) │
│ 4. Returns { valid: true, payload: {...} } │
│ │
└─────────────────────────────────────────────────────────────────────┘
JWT Configuration
Better Auth JWT Plugin (EdDSA - DEFAULT)
Better Auth's JWT plugin uses EdDSA algorithm by default with auto-generated keys stored in the jwks table.
// src/auth/better-auth.config.ts
jwt({
jwt: {
issuer: process.env.JWT_ISSUER || 'manacore',
audience: process.env.JWT_AUDIENCE || 'manacore',
expirationTime: '15m',
definePayload({ user, session }) {
return {
sub: user.id,
email: user.email,
role: user.role || 'user',
sid: session.id,
};
},
},
}),
JWT Claims (Minimal)
ONLY these claims should be in the JWT:
{
sub: string; // User ID
email: string; // User email
role: string; // User role (user, admin, service)
sid: string; // Session ID for reference
iss: string; // Issuer (manacore)
aud: string; // Audience (manacore)
exp: number; // Expiration timestamp
}
DO NOT add:
credit_balance- Changes too frequently, fetch via APIorganization- Use Better Auth org plugin APIscustomer_type- Derive fromactiveOrganizationIdpermissions- Fetch from org membership API
Token Validation Flow
How Services Validate JWTs
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Chat Backend│ │ mana-core-auth │ │ jwks table │
└─────┬───────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ POST /api/v1/auth/validate │
│ { token: "eyJ..." } │ │
│───────────────────────>│ │
│ │ │
│ │ GET /api/v1/auth/jwks │
│ │─────────────────────────>│
│ │ │
│ │<─────────────────────────│
│ │ { keys: [...] } │
│ │ │
│ │ jwtVerify(token, JWKS) │
│ │ (using jose library) │
│ │ │
│<───────────────────────│ │
│ { valid: true, │ │
│ payload: {...} } │ │
Implementation
// src/auth/services/better-auth.service.ts
async validateToken(token: string): Promise<ValidateTokenResult> {
// Use jose library (NOT jsonwebtoken!)
const JWKS = createRemoteJWKSet(
new URL('/api/v1/auth/jwks', 'http://localhost:3001')
);
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'manacore',
audience: 'manacore',
});
return { valid: true, payload };
}
Common Mistakes & Fixes
❌ Mistake 1: Using RS256 with jsonwebtoken
// WRONG - Don't do this!
import * as jwt from 'jsonwebtoken';
const token = jwt.sign(payload, privateKey, {
algorithm: 'RS256', // Better Auth uses EdDSA!
});
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Will fail for Better Auth tokens
});
Fix: Use jose library with Better Auth's JWKS:
// CORRECT
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(new URL('/api/v1/auth/jwks', baseUrl));
const { payload } = await jwtVerify(token, JWKS, { issuer, audience });
❌ Mistake 2: Configuring JWT keys in .env
# WRONG - These are for RS256, Better Auth uses EdDSA
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----..."
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
Fix: Better Auth auto-generates EdDSA keys and stores them in auth.jwks table. No manual key configuration needed for JWT signing.
❌ Mistake 3: Issuer Mismatch
// WRONG - Hardcoded issuer different from config
jwt({
jwt: {
issuer: 'mana-core', // Signing with this
},
});
// But validating with:
jwtVerify(token, JWKS, {
issuer: 'manacore', // Different! Will fail.
});
Fix: Use consistent issuer from environment:
issuer: process.env.JWT_ISSUER || 'manacore',
API Endpoints
Authentication
| Endpoint | Method | Description |
|---|---|---|
/api/v1/auth/register |
POST | Register B2C user |
/api/v1/auth/login |
POST | Sign in, returns JWT |
/api/v1/auth/logout |
POST | Sign out |
/api/v1/auth/refresh |
POST | Refresh access token |
/api/v1/auth/validate |
POST | Validate JWT token |
/api/v1/auth/jwks |
GET | Get JWKS public keys |
/api/v1/auth/session |
GET | Get current session |
Organizations (B2B)
| Endpoint | Method | Description |
|---|---|---|
/api/v1/auth/register/b2b |
POST | Register organization |
/api/v1/auth/organizations |
GET | List user's orgs |
/api/v1/auth/organizations/:id |
GET | Get org details |
/api/v1/auth/organizations/:id/invite |
POST | Invite employee |
/api/v1/auth/organizations/set-active |
POST | Switch active org |
Token Storage (Frontend)
// Storage keys used by @manacore/shared-auth
const STORAGE_KEYS = {
APP_TOKEN: '@auth/appToken', // JWT access token
REFRESH_TOKEN: '@auth/refreshToken', // Session token for refresh
USER_EMAIL: '@auth/userEmail',
};
// Reading token for API calls
const token = localStorage.getItem('@auth/appToken');
Database Schema
jwks Table (Better Auth JWT Plugin)
CREATE TABLE auth.jwks (
id TEXT PRIMARY KEY,
public_key TEXT NOT NULL, -- EdDSA public key (JSON)
private_key TEXT NOT NULL, -- EdDSA private key (JSON)
created_at TIMESTAMPTZ DEFAULT NOW()
);
Better Auth automatically:
- Creates keys on first JWT sign
- Stores them in this table
- Uses them for all subsequent operations
Debugging
Check JWT Algorithm
# Decode JWT header (without verification)
echo "eyJhbG..." | cut -d'.' -f1 | base64 -d
# Should show: { "alg": "EdDSA", "kid": "..." }
# If you see "RS256", something is wrong!
Test JWKS Endpoint
curl http://localhost:3001/api/v1/auth/jwks
# Should return: { "keys": [{ "crv": "Ed25519", "kty": "OKP", ... }] }
Test Token Validation
curl -X POST http://localhost:3001/api/v1/auth/validate \
-H "Content-Type: application/json" \
-d '{"token": "eyJhbGciOiJFZERTQSIs..."}'
# Should return: { "valid": true, "payload": {...} }
Related Files
| File | Purpose |
|---|---|
src/auth/better-auth.config.ts |
Better Auth configuration |
src/auth/services/better-auth.service.ts |
Auth service with JWT validation |
src/auth/auth.controller.ts |
Auth endpoints including /jwks |
src/db/schema/auth.schema.ts |
Database schema including jwks table |
src/config/configuration.ts |
Environment configuration |
Checklist for New Developers
- Read Better Auth documentation: https://www.better-auth.com/docs
- Understand that Better Auth uses EdDSA, not RS256
- Never use
jsonwebtokenfor Better Auth tokens - usejose - JWT validation must use JWKS endpoint, not static keys
- Keep JWT claims minimal - fetch dynamic data via APIs
- Test with actual Better Auth tokens, not manually created ones