mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
Two interlocking fixes driven by a production lockout incident. ## Bug that motivated this A fresh schema-drift column (auth.users.onboarding_completed_at) made every Better Auth query crash with Postgres 42703. The /login wrapper swallowed the non-2xx and mapped it onto a generic "401 Invalid credentials" AND bumped the password lockout counter — so 5 legit login attempts against a broken DB would have locked every real user out of their own account. Same wrapper pattern on /register, /refresh, /reset-password etc. The 30-minute hunt ended in a one-off repro script that finally surfaced the real Postgres error. The user-facing passkey button additionally returned generic 404s on every login-page mount because the route wasn't registered (the DB schema existed, the Better Auth plugin wasn't wired). ## Phase 1 — Error classification (services/mana-auth/src/lib/auth-errors) - 19-code AuthErrorCode taxonomy (INVALID_CREDENTIALS, EMAIL_NOT_VERIFIED, ACCOUNT_LOCKED, SERVICE_UNAVAILABLE, PASSKEY_VERIFICATION_FAILED, …) - classifyFromResponse/classifyFromError handle: Better Auth APIError (duck-typed on `name === 'APIError'`), Postgres errors (23505 unique, 42703/08xxx → infra), ZodError, fetch/ECONNREFUSED network errors, bare Error, unknown. - respondWithError routes the structured response, logs at the right level, fires the correct security event, and CRITICALLY only bumps the lockout counter for actual credential failures — SERVICE_UNAVAILABLE and INTERNAL never touch lockout. - All 12 endpoints in routes/auth.ts refactored (/login, /register, /logout, /session-to-token, /refresh, /validate, /forgot-password, /reset-password, /resend-verification, /profile GET+POST, /change-email, /change-password, /account DELETE). - Fixed pre-existing auth.api.forgetPassword typo (→ requestPasswordReset). - shared-logger + requestLogger middleware wired in index.ts; all console.* calls in the service removed. ## Phase 2 — Passkey end-to-end (@better-auth/passkey 1.6+) - sql/007_passkey_bootstrap.sql: idempotent schema alignment — friendly_name→name, +aaguid, transports jsonb→text, +method column on login_attempts. - better-auth.config.ts: passkey plugin wired with rpID/rpName/origin from new webauthn config section. rpID defaults to mana.how in prod (from COOKIE_DOMAIN), localhost in dev. - routes/passkeys.ts: 7 wrapper endpoints (capability probe, register/options+verify, authenticate/options+verify with JWT mint, list, delete, rename). Each routes errors through the classifier; authenticate/verify promotes generic INVALID_CREDENTIALS to PASSKEY_VERIFICATION_FAILED. - PasskeyRateLimitService: in-memory per-IP (options: 20/min) and per-credential (verify: 10 failures/min → 5 min cooldown) buckets. Deliberately separate from the password lockout — different factor, different blast radius. - Client: authService.getPasskeyCapability() async probe, memoised per session. authStore.passkeyAvailable reactive state. LoginPage gates on === true so a slow probe doesn't flash the button in. - AuthResult grew a code: AuthErrorCode field; handleAuthError in shared-auth prefers the server envelope over the legacy message heuristics. ## Tests - 30 unit tests for the classifier covering every branch (including the exact Postgres 42703 shape that started this). - 9 unit tests for the rate limiter. - 14 integration tests for the auth routes — the regression test explicitly asserts "upstream 500 → 503 + zero lockout bumps". - 101 tests pass, 0 fail, 30 pre-existing skips unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.js | ||
@mana/shared-auth
Shared authentication utilities for Mana apps. This package provides a configurable authentication service that can be used across React Native (Expo) and web apps.
Features
- Configurable Auth Service: Create auth services with custom base URLs and endpoints
- Token Manager: Handle token refresh, queueing, and state management
- JWT Utilities: Decode tokens, check expiration, extract user data
- Fetch Interceptor: Automatically attach tokens and handle 401 responses
- Platform Adapters: Pluggable storage, device, and network adapters
Installation
pnpm add @mana/shared-auth
Quick Start
Web (SvelteKit, React, etc.)
import { initializeWebAuth } from '@mana/shared-auth';
const { authService, tokenManager } = initializeWebAuth({
baseUrl: 'https://api.example.com',
});
// Sign in
const result = await authService.signIn('user@example.com', 'password');
if (result.success) {
console.log('Signed in!');
}
// Get current user
const user = await authService.getUserFromToken();
console.log(user?.email);
// Sign out
await authService.signOut();
React Native (Expo)
import {
createAuthService,
createTokenManager,
setStorageAdapter,
setDeviceAdapter,
setNetworkAdapter,
setupFetchInterceptor,
} from '@mana/shared-auth';
import * as SecureStore from 'expo-secure-store';
// Create storage adapter for Expo
const expoStorageAdapter = {
async getItem<T = string>(key: string): Promise<T | null> {
const value = await SecureStore.getItemAsync(key);
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return value as T;
}
},
async setItem(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value);
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
};
// Set up adapters
setStorageAdapter(expoStorageAdapter);
setDeviceAdapter(yourDeviceAdapter);
setNetworkAdapter(yourNetworkAdapter);
// Create services
const authService = createAuthService({
baseUrl: process.env.EXPO_PUBLIC_API_URL,
});
const tokenManager = createTokenManager(authService);
// Set up fetch interceptor
setupFetchInterceptor(authService, tokenManager);
API Reference
createAuthService(config)
Creates an authentication service instance.
const authService = createAuthService({
baseUrl: 'https://api.example.com',
storageKeys: {
APP_TOKEN: '@auth/appToken',
REFRESH_TOKEN: '@auth/refreshToken',
USER_EMAIL: '@auth/userEmail',
},
endpoints: {
signIn: '/auth/signin',
signUp: '/auth/signup',
// ... other endpoints
},
});
createTokenManager(authService, config?)
Creates a token manager for handling token refresh and state.
const tokenManager = createTokenManager(authService, {
maxQueueSize: 50,
queueTimeoutMs: 30000,
maxRefreshAttempts: 3,
refreshCooldownMs: 5000,
});
// Subscribe to state changes
const unsubscribe = tokenManager.subscribe((state, token) => {
console.log('Token state:', state);
});
// Get valid token (refreshes if needed)
const token = await tokenManager.getValidToken();
JWT Utilities
import {
decodeToken,
isTokenValidLocally,
getUserFromToken,
isB2BUser,
getB2BInfo,
} from '@mana/shared-auth';
const payload = decodeToken(token);
const isValid = isTokenValidLocally(token);
const user = getUserFromToken(token);
const isB2B = isB2BUser(token);
Adapters
The package uses adapters for platform-specific functionality:
- StorageAdapter: For storing tokens securely
- DeviceAdapter: For getting device information
- NetworkAdapter: For checking network connectivity
import { setStorageAdapter, setDeviceAdapter, setNetworkAdapter } from '@mana/shared-auth';
setStorageAdapter(myStorageAdapter);
setDeviceAdapter(myDeviceAdapter);
setNetworkAdapter(myNetworkAdapter);
Migration from Existing Auth
To migrate from existing auth implementations:
- Install the package
- Set up the adapters for your platform
- Replace direct authService calls with the shared service
- Update token manager usage
Before
// memoro/apps/mobile/features/auth/services/authService.ts
import { authService } from './authService';
await authService.signIn(email, password);
After
// Use the shared auth service
import { authService } from '@/services/auth'; // Your configured instance
await authService.signIn(email, password);
Token States
The token manager tracks these states:
IDLE: Initial stateVALID: Token is validREFRESHING: Token refresh in progressEXPIRED: Token has expiredEXPIRED_OFFLINE: Token expired while offline (preserves auth)
Contributing
- Make changes to the source files in
src/ - Run
pnpm run type-checkto validate TypeScript - Run
pnpm run buildto compile