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>
33 KiB
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.jsgenerates codes locally until real API docs arrive. ReplacefazpassSendStub+fazpassVerifyStubat 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.mdremains 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)
- Apple Developer account active ($99/year), Services ID created (e.g.
com.halobestie.client.signin),.p8private key + Key ID + Team ID on hand - Fazpass account provisioned with API key; real API docs obtained (auth method, send OTP endpoint, verify endpoint, webhook spec)
- Google OAuth Client IDs per platform recorded (Android, iOS) — likely already exist from Firebase setup, survive Firebase removal
- Agreement that dev DB can be wiped
- 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_KEYADMIN_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 })— createsauth_sessionsrow + returns{ access_token, refresh_token, expires_at }refreshTokens({ refresh_token, deviceInfo })— validates + rotates + returns new pairrevokeSession({ sessionId })— marksrevoked_at; deletes the rowverifyAccessToken(token)— returns decoded claims or throwshashRefreshToken(raw)— bcryptgenerateRefreshToken()— 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_SECONDSenv, default 3600) - Refresh TTL: 30d (
REFRESH_TOKEN_TTL_DAYSenv, 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, insertsotp_requestsrowverifyOtp({ otpRequestId, code })— calls Fazpass verify, increments attempts, marks used- Helpers:
checkOtpRateLimit(phone, ip)usingapp_config+otp_requestshistory
⚠ 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)— usesgoogle-auth-libraryOAuth2Client.verifyIdToken, validates audience against envGOOGLE_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 tokenssignInWithPhone({ phone, deviceInfo, upgradeFromCustomerId? })— lookup/create customer by phone (or upgrade anonymous), issue tokenssignInMitraWithPhone({ phone, deviceInfo })— lookup/create mitra by phone, issue tokenssignInWithGoogle({ idToken, deviceInfo, upgradeFromCustomerId? })— verify, lookup/create/upgrade, issue tokens (409 if google_sub already linked to another customer)signInWithApple({ idToken, deviceInfo, upgradeFromCustomerId? })— same as GooglesignInCcUser({ email, password, deviceInfo })— verify password, lockout check, issue tokenslogout({ sessionId })— delegate to token service
1.8 Password Service
New file: backend/src/services/password.service.js
Exports:
hashPassword(plain)— bcrypt 12verifyPassword(plain, hash)— bcrypt comparevalidateComplexity(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:
verifyFirebaseTokenusagerequest.firebaseUser.uidreferences
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/mitra.chat.routes.js
- backend/src/routes/public/shared.chat.routes.js
- backend/src/routes/public/mitra.status.routes.js
- backend/src/routes/internal/config.routes.js
- backend/src/routes/internal/roles.routes.js
- backend/src/routes/internal/mitra.routes.js
- backend/src/routes/internal/mitra-activity.routes.js
- backend/src/routes/internal/cc-user.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
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:
jsonwebtokenbcryptgoogle-auth-libraryapple-signin-authOR implement manual Apple JWKS verification withjsonwebtoken+jwks-rsa- HTTP client for Fazpass (can reuse
undiciif 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 callsignInAnonymous()signInAnonymous()— POST/api/shared/auth/anonymousrequestOtp(phone)— POST/api/client/auth/otp/requestverifyOtp(otpRequestId, code)— POST/api/client/auth/otp/verify, passes current anonymous customer session_id for upgradesignInWithGoogle()— nativegoogle_sign_in→ POST/api/client/auth/googlesignInWithApple()— nativesign_in_with_apple→ POST/api/client/auth/applelogout()— 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:
- New:
mitra_app/lib/core/auth/token_storage.dart - Rewrite: mitra_app/lib/core/auth/auth_notifier.dart — methods:
bootstrap,requestOtp,verifyOtp,logout - Update: mitra_app/lib/core/api/api_client.dart
- Update: mitra_app/lib/core/chat/mitra_chat_notifier.dart and 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
New state:
accessTokenin memory (React state)profileauthStatus=loading | authed | unauthenticated
New methods:
bootstrap()— on mount, call/internal/auth/refresh(uses existing httpOnly cookie if present); set statelogin(email, password)— POST/internal/auth/login, server sets cookie + returns access token in bodylogout()— POST/internal/auth/logout, clear staterefreshAccessToken()— 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-userwith 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 byfirebase_core+firebase_messagingfor 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 failspassword.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 rejectedotp.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
Authorizationheader on protected route → 401 - Customer calling mitra-only endpoint with valid customer JWT → 403