# Phase 3.4 Implementation Plan: Self-Managed Authentication > **Progress snapshot (2026-04-24):** > - ✅ **Backend cutover complete** — commit `f860ab6`. All services, middleware, routes, WS auth, seed, env, deps done. Both apps (`app.public.js`, `app.internal.js`) build clean. > - ⚠ **Fazpass stubbed** — `otp.service.js` generates codes locally until real API docs arrive. Replace `fazpassSendStub` + `fazpassVerifyStub` at that time. > - ❌ **client_app / mitra_app / control_center rewrites** — not started. Until these land, the Flutter apps and control center will NOT work end-to-end against the new backend. > - ❌ **Apple Developer prereqs** still required before iOS E2E testing. > - ❌ **Consolidated `phase3.4-testing.md`** — not yet written. `phase3.3-testing.md` remains the canonical test list until then. ## Summary of Clarified Requirements | Topic | Decision | |---|---| | SMS/WhatsApp OTP provider | Fazpass (server-side flow, WhatsApp-first with SMS fallback) | | Google Sign-In | client_app only; native `google_sign_in` → backend verifies Google ID token | | Apple Sign-In | client_app only; native `sign_in_with_apple` → backend verifies Apple ID token | | Mitra auth | Phone OTP only (unchanged scope) | | Control center auth | Email/password; bcrypt; admin-only provisioning; no password reset | | Access token | JWT HS256, 1h TTL, includes `session_id` claim | | Refresh token | Opaque 32-byte random, 30d TTL, rotated on use, bcrypt-hashed in DB | | Multi-device | Each login = new `auth_sessions` row | | Revocation (now) | Delete `auth_sessions` row → 1h window until access token dies | | Revocation (later) | Valkey `revoked_sessions` set, pre-wired via `session_id` claim | | Anonymous customer | Server-issued anonymous JWT + refresh token (no device ID) | | Link account (existing customer) | Reject-on-existing for now; merge deferred | | Data migration | Dev DB can be wiped; cross-app prod migration deferred | | FCM push | Kept — `firebase-admin` stays but only `.messaging()` used | | CC access token storage | In-memory (JS variable in AuthContext) | | CC refresh token storage | httpOnly secure cookie | | Password hashing | bcrypt cost factor 12 | | Password complexity | min 8 chars, ≥1 digit, ≥1 uppercase, ≥1 lowercase | | Rate limits | Configurable via `app_config` (defaults: 3/phone/hour, 10/IP/hour, 60s resend, 5 verify attempts) | | CC login lockout | 5 failed attempts → 15-min lockout | | Session fingerprinting | `user_agent` + `ip` in `auth_sessions.device_info` JSONB | | JWT secret rotation | Single secret for now; rotation procedure documented but not implemented | | Fazpass API shape | ⚠ **TBD** — pending real API docs; placeholder fields in migration and service | --- ## Prerequisites (Must Be Done Before Coding) 1. **Apple Developer account** active ($99/year), Services ID created (e.g. `com.halobestie.client.signin`), `.p8` private key + Key ID + Team ID on hand 2. **Fazpass** account provisioned with API key; real API docs obtained (auth method, send OTP endpoint, verify endpoint, webhook spec) 3. **Google OAuth Client IDs** per platform recorded (Android, iOS) — likely already exist from Firebase setup, survive Firebase removal 4. **Agreement** that dev DB can be wiped 5. **Env vars provisioned** in dev / staging / prod: - `AUTH_JWT_SECRET` (min 32 random bytes) - `FAZPASS_API_KEY`, `FAZPASS_BASE_URL`, `FAZPASS_WEBHOOK_SECRET` (if callbacks used) - `GOOGLE_OAUTH_CLIENT_IDS` (comma-separated) - `APPLE_SERVICES_ID`, `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_PRIVATE_KEY` - `ADMIN_EMAIL`, `ADMIN_PASSWORD` (CC seed) --- ## Work Stream 1: Backend — Schema, Services, Routes ### 1.1 DB Migration **File:** [backend/src/db/migrate.js](backend/src/db/migrate.js) ```sql -- Drop firebase_uid columns ALTER TABLE customers DROP COLUMN IF EXISTS firebase_uid; ALTER TABLE mitras DROP COLUMN IF EXISTS firebase_uid; ALTER TABLE control_center_users DROP COLUMN IF EXISTS firebase_uid; -- Add social identity columns to customers ALTER TABLE customers ADD COLUMN IF NOT EXISTS email VARCHAR(255), ADD COLUMN IF NOT EXISTS google_sub VARCHAR(255) UNIQUE, ADD COLUMN IF NOT EXISTS apple_sub VARCHAR(255) UNIQUE; -- CC password columns ALTER TABLE control_center_users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(60) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS failed_login_count INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS lockout_until TIMESTAMPTZ; -- (after backfilling real hashes, drop the DEFAULT '') -- New auth_sessions table CREATE TABLE IF NOT EXISTS auth_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_type VARCHAR(16) NOT NULL, user_id UUID NOT NULL, refresh_token_hash VARCHAR(60) NOT NULL, device_info JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions (user_type, user_id); CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions (expires_at) WHERE revoked_at IS NULL; -- New otp_requests table (shape is provisional — confirm with Fazpass docs) CREATE TABLE IF NOT EXISTS otp_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), phone VARCHAR(20) NOT NULL, fazpass_reference VARCHAR(255) NOT NULL, channel VARCHAR(16), attempts INT NOT NULL DEFAULT 0, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_otp_requests_phone_created ON otp_requests (phone, created_at); -- app_config defaults INSERT INTO app_config (key, value) VALUES ('otp_max_per_phone_per_hour', '{"value": 3}'), ('otp_max_per_ip_per_hour', '{"value": 10}'), ('otp_resend_cooldown_seconds', '{"value": 60}'), ('otp_verify_max_attempts', '{"value": 5}'), ('cc_login_max_attempts', '{"value": 5}'), ('cc_login_lockout_minutes', '{"value": 15}') ON CONFLICT (key) DO NOTHING; ``` ### 1.2 Constants **File:** [backend/src/constants.js](backend/src/constants.js) ```js export const AuthProvider = Object.freeze({ ANONYMOUS: 'anonymous', PHONE: 'phone', GOOGLE: 'google', APPLE: 'apple', PASSWORD: 'password', }) export const OtpChannel = Object.freeze({ WHATSAPP: 'whatsapp', SMS: 'sms', }) ``` ### 1.3 Config Service Additions **File:** [backend/src/services/config.service.js](backend/src/services/config.service.js) Add getters/setters for: - `getOtpRateLimits()` → `{ max_per_phone_per_hour, max_per_ip_per_hour }` - `getOtpResendCooldownSeconds()` - `getOtpVerifyMaxAttempts()` - `getCcLoginLockoutConfig()` → `{ max_attempts, lockout_minutes }` - Matching setters for CC settings page ### 1.4 New Service: Token Service **New file:** `backend/src/services/token.service.js` Exports: - `issueTokens({ userType, userId, deviceInfo })` — creates `auth_sessions` row + returns `{ access_token, refresh_token, expires_at }` - `refreshTokens({ refresh_token, deviceInfo })` — validates + rotates + returns new pair - `revokeSession({ sessionId })` — marks `revoked_at`; deletes the row - `verifyAccessToken(token)` — returns decoded claims or throws - `hashRefreshToken(raw)` — bcrypt - `generateRefreshToken()` — 32-byte crypto random base64url Internals: - Access token claims: `{ sub, user_type, session_id, iat, exp }` - HS256 with `AUTH_JWT_SECRET` - Access TTL: 1h (`ACCESS_TOKEN_TTL_SECONDS` env, default 3600) - Refresh TTL: 30d (`REFRESH_TOKEN_TTL_DAYS` env, default 30) ### 1.5 New Service: OTP Service **New file:** `backend/src/services/otp.service.js` Exports: - `requestOtp({ phone, ipAddress })` — enforces rate limits, calls Fazpass send, inserts `otp_requests` row - `verifyOtp({ otpRequestId, code })` — calls Fazpass verify, increments attempts, marks used - Helpers: `checkOtpRateLimit(phone, ip)` using `app_config` + `otp_requests` history ⚠ **Fazpass-specific parts TBD** until real API docs are in hand. Placeholder functions: ```js async function fazpassSendOtp({ phone, channel }) { // TBD: POST to Fazpass send endpoint // Returns: { reference, channel_used, expires_at } } async function fazpassVerifyOtp({ reference, code }) { // TBD: POST to Fazpass verify endpoint // Returns: { valid: bool } } ``` ### 1.6 New Service: Social Identity Service **New file:** `backend/src/services/social-identity.service.js` Exports: - `verifyGoogleIdToken(idToken)` — uses `google-auth-library` `OAuth2Client.verifyIdToken`, validates audience against env `GOOGLE_OAUTH_CLIENT_IDS`, returns `{ sub, email, name, email_verified }` - `verifyAppleIdToken(idToken)` — fetches Apple JWKS, verifies JWT signature + audience, returns `{ sub, email? }` ### 1.7 New Service: Auth Service **New file:** `backend/src/services/auth.service.js` Orchestrates sign-in flows. Exports: - `signInAnonymous({ deviceInfo })` — creates customer row with generated display name, issues tokens - `signInWithPhone({ phone, deviceInfo, upgradeFromCustomerId? })` — lookup/create customer by phone (or upgrade anonymous), issue tokens - `signInMitraWithPhone({ phone, deviceInfo })` — lookup/create mitra by phone, issue tokens - `signInWithGoogle({ idToken, deviceInfo, upgradeFromCustomerId? })` — verify, lookup/create/upgrade, issue tokens (409 if google_sub already linked to another customer) - `signInWithApple({ idToken, deviceInfo, upgradeFromCustomerId? })` — same as Google - `signInCcUser({ email, password, deviceInfo })` — verify password, lockout check, issue tokens - `logout({ sessionId })` — delegate to token service ### 1.8 Password Service **New file:** `backend/src/services/password.service.js` Exports: - `hashPassword(plain)` — bcrypt 12 - `verifyPassword(plain, hash)` — bcrypt compare - `validateComplexity(plain)` — min 8 chars, ≥1 digit, ≥1 uppercase, ≥1 lowercase; throws with specific error code ### 1.9 Middleware Rewrite: `authenticate` **File:** [backend/src/plugins/auth.js](backend/src/plugins/auth.js) Replace Firebase verification with: - Extract `Authorization: Bearer ` header - Call `verifyAccessToken(token)` from token service - On success: attach `request.auth = { userType, userId, sessionId }` - On failure: return 401 UNAUTHORIZED - **Future hook for Valkey revocation**: after JWT verification, check `SISMEMBER revoked_sessions ` — skipped in this phase but the code location is noted for later Remove entirely: - `verifyFirebaseToken` usage - `request.firebaseUser.uid` references ### 1.10 Update All Routes Every route that currently does `getCustomerByFirebaseUid(request.firebaseUser.uid)` (or mitra/cc variant) must be updated to read `request.auth.userId` + `request.auth.userType` directly. No more DB lookup needed — the token already contains the resolved ID. Files: - [backend/src/routes/public/client.chat.routes.js](backend/src/routes/public/client.chat.routes.js) - [backend/src/routes/public/mitra.chat.routes.js](backend/src/routes/public/mitra.chat.routes.js) - [backend/src/routes/public/shared.chat.routes.js](backend/src/routes/public/shared.chat.routes.js) - [backend/src/routes/public/mitra.status.routes.js](backend/src/routes/public/mitra.status.routes.js) - [backend/src/routes/internal/config.routes.js](backend/src/routes/internal/config.routes.js) - [backend/src/routes/internal/roles.routes.js](backend/src/routes/internal/roles.routes.js) - [backend/src/routes/internal/mitra.routes.js](backend/src/routes/internal/mitra.routes.js) - [backend/src/routes/internal/mitra-activity.routes.js](backend/src/routes/internal/mitra-activity.routes.js) - [backend/src/routes/internal/cc-user.routes.js](backend/src/routes/internal/cc-user.routes.js) - [backend/src/routes/internal/session.routes.js](backend/src/routes/internal/session.routes.js) Convert `resolveCustomer` / `resolveMitra` / `attachCcUser` helpers to use `request.auth` instead of Firebase. ### 1.11 Replace Auth Route Files **Rewrite:** [backend/src/routes/public/client.auth.routes.js](backend/src/routes/public/client.auth.routes.js) New endpoints (all public, no auth unless noted): | Method | Path | Purpose | |---|---|---| | POST | `/api/shared/auth/anonymous` | Create anonymous customer + tokens | | POST | `/api/client/auth/otp/request` | Start phone OTP (customer) | | POST | `/api/client/auth/otp/verify` | Verify phone OTP (customer) | | POST | `/api/client/auth/google` | Google sign-in | | POST | `/api/client/auth/apple` | Apple sign-in | | POST | `/api/shared/auth/refresh` | Refresh token rotation | | POST | `/api/shared/auth/logout` | Logout (authenticated) | | GET | `/api/client/auth/me` | Current user profile (authenticated) | **Rewrite:** [backend/src/routes/public/mitra.auth.routes.js](backend/src/routes/public/mitra.auth.routes.js) | Method | Path | Purpose | |---|---|---| | POST | `/api/mitra/auth/otp/request` | Start phone OTP (mitra) | | POST | `/api/mitra/auth/otp/verify` | Verify phone OTP (mitra) | | GET | `/api/mitra/auth/me` | Current mitra profile (authenticated) | **Rewrite:** [backend/src/routes/internal/auth.routes.js](backend/src/routes/internal/auth.routes.js) | Method | Path | Purpose | |---|---|---| | POST | `/internal/auth/login` | CC email/password login (sets httpOnly refresh cookie + returns access token in body) | | POST | `/internal/auth/refresh` | Refresh using httpOnly cookie | | POST | `/internal/auth/logout` | Clear cookie + delete session | | GET | `/internal/auth/me` | Current CC user profile | **New:** `backend/src/routes/internal/cc-user.routes.js` — password endpoints | Method | Path | Purpose | |---|---|---| | POST | `/internal/cc-user` | Create CC user (super admin only; accepts initial password) | | PATCH | `/internal/cc-user/me/password` | Self-service change (current_password + new_password) | | PATCH | `/internal/cc-user/:id/password` | Admin-forced reset (super admin only) | Existing CC user listing / create (currently calls Firebase) is reworked to accept plain-password fields. ### 1.12 WebSocket Plugin Update **File:** [backend/src/plugins/websocket.js](backend/src/plugins/websocket.js) Replace `verifyFirebaseToken(msg.token)` with `verifyAccessToken(msg.token)`. Remove customer/mitra lookup (now encoded in the JWT claims). ### 1.13 FCM Plugin Isolation **File:** [backend/src/plugins/firebase.js](backend/src/plugins/firebase.js) → rename to `backend/src/plugins/fcm.js` Keep `firebase-admin` import but only expose `admin.messaging()`. Delete `verifyFirebaseToken` and Firebase Auth exports. All callers of `verifyFirebaseToken` are already rewritten in 1.9. ### 1.14 Seed Script Rewrite **File:** [backend/src/db/seed.js](backend/src/db/seed.js) Replace `admin.auth().createUser()` / `admin.auth().getUserByEmail()` with direct DB insert: ```js const email = process.env.ADMIN_EMAIL const password = process.env.ADMIN_PASSWORD const passwordHash = await hashPassword(password) await sql` INSERT INTO control_center_users (email, password_hash, display_name, role_id) VALUES (${email}, ${passwordHash}, 'Super Admin', ${superAdminRoleId}) ON CONFLICT (email) DO NOTHING ` ``` ### 1.15 Dependency Changes **File:** `backend/package.json` Add: - `jsonwebtoken` - `bcrypt` - `google-auth-library` - `apple-signin-auth` OR implement manual Apple JWKS verification with `jsonwebtoken` + `jwks-rsa` - HTTP client for Fazpass (can reuse `undici` if Node 18+) Keep: - `firebase-admin` (for FCM only) - `@fastify/cors`, `@fastify/websocket`, `postgres`, `dotenv`, etc. --- ## Work Stream 2: client_app (Flutter) ### 2.1 Dependencies **File:** `client_app/pubspec.yaml` Remove: - `firebase_auth` Keep: - `firebase_core`, `firebase_messaging` (for FCM) - `google_sign_in`, `sign_in_with_apple` Add: - `flutter_secure_storage` (if not present) - `jwt_decoder` (optional, for reading token expiry on client) ### 2.2 New: Secure Storage Service **New file:** `client_app/lib/core/auth/token_storage.dart` Wrapper around `flutter_secure_storage`: - `saveRefreshToken(String)` - `getRefreshToken()` → `String?` - `clearRefreshToken()` - `saveAccessToken(String)` / `getAccessToken()` / `clearAccessToken()` (access token can live in memory but keeping encrypted storage is safer against cold-start race) ### 2.3 New: Auth Notifier Rewrite **File:** [client_app/lib/core/auth/auth_notifier.dart](client_app/lib/core/auth/auth_notifier.dart) Replace Firebase-based flows. New methods (Riverpod `Notifier`): - `bootstrap()` — on app start, check for refresh token → refresh → set authed state; else call `signInAnonymous()` - `signInAnonymous()` — POST `/api/shared/auth/anonymous` - `requestOtp(phone)` — POST `/api/client/auth/otp/request` - `verifyOtp(otpRequestId, code)` — POST `/api/client/auth/otp/verify`, passes current anonymous customer session_id for upgrade - `signInWithGoogle()` — native `google_sign_in` → POST `/api/client/auth/google` - `signInWithApple()` — native `sign_in_with_apple` → POST `/api/client/auth/apple` - `logout()` — POST `/api/shared/auth/logout`, clear secure storage State shape: `{ accessToken, refreshToken, profile, authStatus }` ### 2.4 API Client Interceptor Rewrite **File:** [client_app/lib/core/api/api_client.dart](client_app/lib/core/api/api_client.dart) Replace `user.getIdToken()` with in-memory access token from `authNotifier`: - Request interceptor: attach `Authorization: Bearer ` - Response interceptor: on 401, try refresh once; if refresh fails, trigger logout - No more Firebase dependency ### 2.5 WebSocket Auth Update **File:** [client_app/lib/core/chat/chat_notifier.dart](client_app/lib/core/chat/chat_notifier.dart) Replace `FirebaseAuth.instance.currentUser!.getIdToken()` with current access token from `authNotifier`. If expired, refresh first. ### 2.6 Remove Firebase Auth Initialization **File:** [client_app/lib/main.dart](client_app/lib/main.dart) Keep `Firebase.initializeApp()` for FCM. Remove all `FirebaseAuth.instance` references (auth state listener, etc.). Replace with `authNotifier.bootstrap()` call. ### 2.7 Remove Firebase Options (Partial) **File:** [client_app/lib/firebase_options.dart](client_app/lib/firebase_options.dart) Keep — FCM still needs Firebase project config. Just don't call `FirebaseAuth` anywhere. ### 2.8 Auth Screens **Files:** `client_app/lib/features/auth/screens/*` Update each screen to call new `AuthNotifier` methods. The screen structure (welcome, OTP, register, force-register, display name) stays the same — only the underlying method calls change. ### 2.9 FCM Token Registration **File:** [client_app/lib/core/notifications/notification_service.dart](client_app/lib/core/notifications/notification_service.dart) `FirebaseMessaging.instance.getToken()` still works. Registration endpoint (`POST /api/shared/device-token`) is unchanged — only auth header changes (new JWT instead of Firebase token). --- ## Work Stream 3: mitra_app (Flutter) Identical pattern to client_app but phone-only. No Google/Apple code added. ### 3.1 Dependencies **File:** `mitra_app/pubspec.yaml` Remove `firebase_auth`. Keep `firebase_core`, `firebase_messaging`. Add `flutter_secure_storage`. ### 3.2 Token Storage + Auth Notifier + API Client + WebSocket Same shape as client_app changes above. **Files:** - New: `mitra_app/lib/core/auth/token_storage.dart` - Rewrite: [mitra_app/lib/core/auth/auth_notifier.dart](mitra_app/lib/core/auth/auth_notifier.dart) — methods: `bootstrap`, `requestOtp`, `verifyOtp`, `logout` - Update: [mitra_app/lib/core/api/api_client.dart](mitra_app/lib/core/api/api_client.dart) - Update: [mitra_app/lib/core/chat/mitra_chat_notifier.dart](mitra_app/lib/core/chat/mitra_chat_notifier.dart) and [mitra_app/lib/core/chat/chat_request_notifier.dart](mitra_app/lib/core/chat/chat_request_notifier.dart) — replace Firebase token retrieval in WS auth ### 3.3 Auth Screens **Files:** `mitra_app/lib/features/auth/screens/*` Update OTP screens to call new notifier methods. Screen structure stays. --- ## Work Stream 4: control_center (React) ### 4.1 Dependencies **File:** `control_center/package.json` Remove `firebase`. ### 4.2 Remove Firebase Initialization **Delete:** `control_center/src/core/auth/firebase.js` ### 4.3 AuthContext Rewrite **File:** [control_center/src/core/auth/AuthContext.jsx](control_center/src/core/auth/AuthContext.jsx) New state: - `accessToken` in memory (React state) - `profile` - `authStatus` = `loading | authed | unauthenticated` New methods: - `bootstrap()` — on mount, call `/internal/auth/refresh` (uses existing httpOnly cookie if present); set state - `login(email, password)` — POST `/internal/auth/login`, server sets cookie + returns access token in body - `logout()` — POST `/internal/auth/logout`, clear state - `refreshAccessToken()` — used by API interceptor on 401 ### 4.4 API Client Update **File:** [control_center/src/core/api/api-client.js](control_center/src/core/api/api-client.js) Replace `auth.currentUser.getIdToken()` with access token from AuthContext. On 401, call `refreshAccessToken()` and retry. All requests send `credentials: 'include'` (for cookie). ### 4.5 CORS Update **File:** [backend/src/app.internal.js](backend/src/app.internal.js) Ensure `@fastify/cors` config allows credentials from the CC origin (required for httpOnly cookie): ```js await app.register(cors, { origin: process.env.CC_ORIGIN, credentials: true, }) ``` ### 4.6 Login Page **File:** [control_center/src/pages/login/LoginPage.jsx](control_center/src/pages/login/LoginPage.jsx) (or wherever the login page lives) Update form to call `authContext.login(email, password)`. Error handling for lockout (15-min) and wrong-credentials. ### 4.7 CC User Management Page **File:** existing users management page (likely `control_center/src/pages/users/UsersPage.jsx`) Update create-user form: - Add "Initial Password" field with "Generate" button (`crypto.randomUUID().slice(0,16)`) - Submit calls `POST /internal/cc-user` with plain password; backend hashes Add password-change UI: - Self: account menu → "Change password" modal (current + new) - Admin-forced: row action on user list → "Reset password" modal (new password only) --- ## Work Stream 5: Env & Config ### 5.1 Backend `.env.example` Add: ``` AUTH_JWT_SECRET= ACCESS_TOKEN_TTL_SECONDS=3600 REFRESH_TOKEN_TTL_DAYS=30 FAZPASS_API_KEY= FAZPASS_BASE_URL= FAZPASS_WEBHOOK_SECRET= GOOGLE_OAUTH_CLIENT_IDS= APPLE_SERVICES_ID= APPLE_TEAM_ID= APPLE_KEY_ID= APPLE_PRIVATE_KEY= ADMIN_EMAIL=admin@halobestie.com ADMIN_PASSWORD= CC_ORIGIN=http://localhost:5173 ``` Remove: ``` FIREBASE_PROJECT_ID= FIREBASE_CLIENT_EMAIL= FIREBASE_PRIVATE_KEY= ``` ### 5.2 control_center `.env.example` Remove: ``` VITE_FIREBASE_API_KEY= VITE_FIREBASE_AUTH_DOMAIN= VITE_FIREBASE_PROJECT_ID= ``` ### 5.3 Mobile Firebase Configs - Keep `google-services.json` (Android) / `GoogleService-Info.plist` (iOS) — required by `firebase_core` + `firebase_messaging` for FCM - Keep `firebase_options.dart` — only referenced for FCM --- ## 6. Implementation Order | Step | What | Apps | Dependencies | |---|---|---|---| | **Prerequisites** | | | | | 0 | Apple Dev account, Fazpass creds, env vars provisioned | Ops | — | | **Work Stream 1: Backend** | | | | | 1 | DB migration + constants + config service additions | Backend | Step 0 | | 2 | Token service (JWT issue / verify / refresh rotation) | Backend | Step 1 | | 3 | Password service (hash, verify, complexity) | Backend | — | | 4 | Rewrite `authenticate` middleware (JWT-based) | Backend | Step 2 | | 5 | Social identity service (Google + Apple JWKS verify) | Backend | — | | 6 | OTP service (Fazpass send + verify + rate-limit) | Backend | Step 1, Fazpass docs | | 7 | Auth service orchestrator (all sign-in flows) | Backend | Steps 2, 3, 5, 6 | | 8 | Rewrite auth routes (public/client, public/mitra, internal/auth) | Backend | Step 7 | | 9 | CC user provisioning + password change routes | Backend | Step 3 | | 10 | Update all other routes to use `request.auth` | Backend | Step 4 | | 11 | Rewrite WebSocket auth handshake | Backend | Step 2 | | 12 | Isolate Firebase to FCM-only (`firebase.js` → `fcm.js`) | Backend | Step 10, 11 | | 13 | Rewrite seed script | Backend | Step 3 | | **Work Stream 2: client_app** | | | | | 14 | Secure storage wrapper + auth notifier skeleton | client_app | Step 8 | | 15 | API client interceptor (JWT attach + 401 refresh) | client_app | Step 14 | | 16 | Phone OTP flow (request + verify + bootstrap anonymous) | client_app | Step 14 | | 17 | Google sign-in wiring | client_app | Step 16 | | 18 | Apple sign-in wiring | client_app | Step 17 | | 19 | WebSocket auth handshake update | client_app | Step 15 | | 20 | Remove all `FirebaseAuth.instance` references | client_app | Step 19 | | **Work Stream 3: mitra_app** | | | | | 21 | Secure storage + auth notifier (phone-only) | mitra_app | Step 8 | | 22 | API client + WebSocket auth update | mitra_app | Step 21 | | 23 | Remove `FirebaseAuth.instance` references | mitra_app | Step 22 | | **Work Stream 4: control_center** | | | | | 24 | AuthContext rewrite (in-memory access + cookie refresh) | control_center | Step 8 | | 25 | API client update (credentials: include, 401 refresh) | control_center | Step 24 | | 26 | CORS config to allow credentials | Backend + control_center | Step 24 | | 27 | Login page rewrite | control_center | Step 24 | | 28 | CC user management password UI | control_center | Step 9 | | 29 | Remove `firebase` dependency + firebase.js | control_center | Step 27 | | **Testing / Cleanup** | | | | | 30 | E2E: anonymous → OTP → Google → Apple → refresh → logout (client_app) | All | Steps 1–20 | | 31 | E2E: OTP → refresh → logout (mitra_app) | All | Steps 21–23 | | 32 | E2E: CC login → password change → lockout → logout | All | Steps 24–29 | | 33 | Regression: all Phase 3 / 3.1 / 3.2 / 3.3 flows still work with new auth | All | All above | --- ## 7. New Files | File | Purpose | |---|---| | `backend/src/services/token.service.js` | JWT + refresh rotation + session storage | | `backend/src/services/otp.service.js` | Fazpass integration + rate-limit enforcement | | `backend/src/services/social-identity.service.js` | Google + Apple ID token verification | | `backend/src/services/auth.service.js` | Sign-in flow orchestrator | | `backend/src/services/password.service.js` | bcrypt + complexity validation | | `backend/src/plugins/fcm.js` | Renamed from firebase.js, FCM-only | | `client_app/lib/core/auth/token_storage.dart` | Secure storage wrapper | | `mitra_app/lib/core/auth/token_storage.dart` | Secure storage wrapper | ## 8. Modified Files (Primary) | File | Change | |---|---| | `backend/src/db/migrate.js` | Drop firebase_uid, add new columns, add auth_sessions + otp_requests tables, seed config keys | | `backend/src/db/seed.js` | Bcrypt-based admin seed instead of Firebase | | `backend/src/constants.js` | AuthProvider + OtpChannel | | `backend/src/services/config.service.js` | OTP + CC lockout config getters/setters | | `backend/src/services/customer.service.js` | Drop firebase_uid lookup; add google_sub/apple_sub/phone lookups | | `backend/src/services/mitra.service.js` | Drop firebase_uid lookup; phone-based lookup | | `backend/src/services/cc-user.service.js` | Drop firebase_uid; email + password_hash based | | `backend/src/services/notification.service.js` | Use FCM-only import from new fcm.js | | `backend/src/plugins/auth.js` | JWT-based middleware | | `backend/src/plugins/websocket.js` | JWT auth in WS handshake | | `backend/src/routes/public/client.auth.routes.js` | Full rewrite | | `backend/src/routes/public/mitra.auth.routes.js` | Full rewrite | | `backend/src/routes/internal/auth.routes.js` | Full rewrite | | `backend/src/routes/internal/cc-user.routes.js` | Password endpoints + plain-password provisioning | | All route files currently using `request.firebaseUser.uid` | Switch to `request.auth.userId` / `request.auth.userType` | | `backend/src/app.internal.js` | CORS with credentials for cookie | | `backend/package.json` | Add jsonwebtoken, bcrypt, google-auth-library, apple-signin-auth | | `client_app/pubspec.yaml` | Remove firebase_auth; add flutter_secure_storage | | `client_app/lib/main.dart` | Remove FirebaseAuth listener; call authNotifier.bootstrap | | `client_app/lib/core/auth/auth_notifier.dart` | Full rewrite | | `client_app/lib/core/api/api_client.dart` | JWT interceptor | | `client_app/lib/core/chat/chat_notifier.dart` | JWT in WS handshake | | All client_app auth screens | Call new notifier methods | | `mitra_app/pubspec.yaml` | Remove firebase_auth; add flutter_secure_storage | | `mitra_app/lib/main.dart` | Remove FirebaseAuth listener | | `mitra_app/lib/core/auth/auth_notifier.dart` | Full rewrite (phone-only) | | `mitra_app/lib/core/api/api_client.dart` | JWT interceptor | | `mitra_app/lib/core/chat/mitra_chat_notifier.dart` | JWT in WS handshake | | `mitra_app/lib/core/chat/chat_request_notifier.dart` | JWT in WS handshake | | All mitra_app auth screens | Call new notifier methods | | `control_center/package.json` | Remove firebase | | `control_center/src/core/auth/AuthContext.jsx` | Full rewrite | | `control_center/src/core/api/api-client.js` | In-memory token + cookie refresh | | `control_center/src/pages/login/LoginPage.jsx` | Rewrite | | `control_center/src/pages/users/UsersPage.jsx` | Password provisioning UI | ## 9. Deleted Files | File | Reason | |---|---| | `control_center/src/core/auth/firebase.js` | No longer needed | ## 10. Env File Changes See Section 5 above. --- ## 11. Risks & Mitigations | Risk | Mitigation | |---|---| | Fazpass API shape assumed wrong → service rewrite mid-implementation | Block on real docs before Step 6; mark `otp.service.js` as stub until then | | Apple Developer setup blocks iOS testing | Flagged as Prerequisite 0; other flows (Google, phone) still testable on Android | | CC cookie auth breaks with cross-origin CC ↔ backend | Set `SameSite=None; Secure` on cookie in prod; use `sameSite=lax` in dev; CORS `credentials: true` | | JWT secret leak compromises all sessions | Env var stored in secret manager; rotation procedure documented (deferred impl) | | Existing users locked out after cutover | Dev DB wiped by agreement; production migration explicitly deferred | | Refresh token theft | Rotation-on-use means stolen token becomes invalid on next legitimate use; device_info audit trail | | bcrypt cost 12 is slow on low-end hardware | Cost 12 takes ~250ms on modern CPU; acceptable for OTP/login frequency; revisit if latency spikes | | Anonymous refresh token loss (uninstall / secure storage wipe) | Accepted — reinstall = fresh anonymous session + lost chat history (matches Firebase behavior today) | | Google/Apple sign-in fails on first launch after install | Both native SDKs handle this; backend returns 401 with clear error code; client shows "Try again" | | Fazpass rate-limit separate from our rate-limit | Our backend enforces first (configurable); Fazpass enforces second (provider's own); log both distinctly | | Concurrent OTP requests for same phone | DB row per request; rate-limit check before Fazpass call prevents double-charge | | Token claim drift (adding fields later) | JWT is versionless; all claims read via property access, missing claims tolerated in verifier | --- ## 12. Testing Checklist **Backend unit (per service):** - [ ] `token.service.js`: issue → verify → refresh rotation → logout → verify fails - [ ] `password.service.js`: hash → verify → complexity rejections (short, missing digit, missing case) - [ ] `social-identity.service.js`: mocked Google + Apple valid tokens verified; invalid signature rejected; wrong audience rejected - [ ] `otp.service.js`: rate-limit throws; resend cooldown honored; verify increments attempts; stub Fazpass with a mock **Backend integration:** - [ ] Anonymous → phone OTP → upgrade (same customer row) - [ ] Anonymous → Google → upgrade (same customer row) - [ ] Anonymous → Google → 409 if google_sub already linked elsewhere - [ ] OTP resend within 60s → 429 - [ ] 4 OTP requests from same phone in an hour → 429 - [ ] CC 5 failed logins → lockout 15 min; 6th attempt even with correct password → 423/429 - [ ] Refresh token reuse (after rotation) → rejected - [ ] Logout → refresh rejected, access expires naturally **client_app E2E:** - [ ] First launch → anonymous session created → can request chat - [ ] Request OTP → verify → upgraded, chat history preserved - [ ] Google sign-in (fresh customer) - [ ] Apple sign-in (fresh customer) - [ ] Token refresh transparent to user (break access token early to test) - [ ] Logout → returns to splash → anonymous auto-created - [ ] Kill app with active chat → restart → session restored via refresh **mitra_app E2E:** - [ ] OTP request + verify → new mitra row created - [ ] Token refresh transparent - [ ] Chat request WebSocket auth works - [ ] Logout **control_center E2E:** - [ ] Seeded super admin can log in - [ ] Create second admin with initial password → new admin logs in - [ ] Self-service password change - [ ] Admin-forced password reset - [ ] Refresh cookie persists across browser reloads - [ ] Logout clears cookie + state **Regression:** - [ ] Phase 3 flows (chat, extension, closure) work end-to-end with new auth - [ ] Phase 3.2 overlay still works with JWT-based WS auth - [ ] Phase 3.3 topic sensitivity flow still works **Negative:** - [ ] Tampered JWT → 401 - [ ] Expired JWT → 401 → client auto-refreshes - [ ] Refresh token from another device/user → 401 - [ ] Missing `Authorization` header on protected route → 401 - [ ] Customer calling mitra-only endpoint with valid customer JWT → 403