Files
halobestie-clone/requirement/phase3.4-plan.md
ramadhan sjamsani 3a25ddc41d docs: mark Phase 3.4 backend done in plan doc
Added progress snapshot at the top of phase3.4-plan.md noting:
- Backend cutover complete (commit f860ab6)
- Fazpass stubbed until real API docs arrive
- Frontend rewrites (client_app, mitra_app, control_center) pending
- Apple Developer prereqs still required
- Consolidated phase3.4-testing.md still to be written

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:51:19 +08:00

33 KiB
Raw Blame History

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 stubbedotp.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

-- 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

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

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:

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

Replace Firebase verification with:

  • Extract Authorization: Bearer <token> 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 <session_id> — 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:

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

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

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

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

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 → 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

Replace admin.auth().createUser() / admin.auth().getUserByEmail() with direct DB insert:

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

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

Replace user.getIdToken() with in-memory access token from authNotifier:

  • Request interceptor: attach Authorization: Bearer <access_token>
  • 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

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

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

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

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:

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

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

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

Ensure @fastify/cors config allows credentials from the CC origin (required for httpOnly cookie):

await app.register(cors, {
  origin: process.env.CC_ORIGIN,
  credentials: true,
})

4.6 Login Page

File: 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.jsfcm.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 120
31 E2E: OTP → refresh → logout (mitra_app) All Steps 2123
32 E2E: CC login → password change → lockout → logout All Steps 2429
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