New Authentication & Onboarding API
Overview
Hybrid Auth Strategy:
- Signup: Phone/Email + OTP (passwordless initially)
- After onboarding: Optional password setup
- Login: Password (if set) OR OTP OR Google/Apple
- Sensitive actions: Require OTP or password confirmation
Response Format Standard:
All responses follow GlobeSuccessResponseBuilder or GlobeFailureResponseBuilder format:
{
"success": true/false,
"httpStatus": "OK/BAD_REQUEST/etc",
"message": "Human readable message",
"action_time": "2025-01-11T15:20:00",
"data": { ... }
}
DEVICE SECURITY ARCHITECTURE
The Problem We're Solving
Without hardware-bound keys:
─────────────────────────────────────────────────────────
Attacker steals victim's password
↓
Attacker generates fake deviceId: "fake-device-123"
↓
Attacker logs in → Server asks for OTP
↓
Attacker does SIM swap / social engineering → gets OTP
↓
Attacker is now "trusted" forever 😱
↓
Victim can't kick attacker out (attacker has valid device)
With hardware-bound keys:
─────────────────────────────────────────────────────────
Attacker steals victim's password
↓
Attacker tries to login with fake deviceId
↓
Server: "Sign this challenge with your private key"
↓
Attacker: "I don't have the private key..." 😤
↓
Private key is locked inside victim's phone hardware
↓
Attack FAILS ✅
Asymmetric Cryptography (Key Pair Concept)
┌─────────────────────────────────────────────────────────────┐
│ ASYMMETRIC CRYPTOGRAPHY │
├─────────────────────────────────────────────────────────────┤
│ │
│ Private Key Public Key │
│ ─────────── ────────── │
│ • Secret • Shareable │
│ • Never leaves device • Stored on server │
│ • Used to SIGN • Used to VERIFY │
│ • Locked in hardware • Anyone can have it │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Private Key │──── generates ──▶│ Public Key │ │
│ │ 🔐 │ │ 🔓 │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ Sign("hello") Verify(signature) │
│ │ │ │
│ ▼ ▼ │
│ "MEUCIQC7..." true / false │
│ (signature) │
│ │
│ KEY POINT: You CANNOT derive private key from public key │
│ │
└─────────────────────────────────────────────────────────────┘
Platform Security Overview
┌─────────────────────────────────────────────────────────────┐
│ iOS DEVICE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Main Processor │ │
│ │ Your app lives here │ │
│ │ Can request signatures │ │
│ │ CANNOT access private key │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ "Please sign this" │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SECURE ENCLAVE (Separate Chip) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Private Key 🔐 │ │ │
│ │ │ • Generated HERE │ │ │
│ │ │ • Stored HERE │ │ │
│ │ │ • NEVER leaves │ │ │
│ │ │ • Cannot be read by main processor │ │ │
│ │ │ • Cannot be extracted even if jailbroken │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ Returns: signature (NOT the key) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ANDROID DEVICE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Option A: StrongBox (Dedicated Security Chip) │
│ • Separate hardware chip (like iOS Secure Enclave) │
│ • Best security │
│ • Available on Pixel 3+, Samsung S10+, etc. │
│ │
│ Option B: TEE (Trusted Execution Environment) │
│ • Isolated area within main processor │
│ • Very good security │
│ • Available on most Android 7+ devices │
│ │
│ Both use Android Keystore API │
│ Same result: Private key cannot be extracted │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ WEB BROWSER │
├─────────────────────────────────────────────────────────────┤
│ │
│ Problem: No dedicated hardware security │
│ Solution: PKCE-style ephemeral keys (explained below) │
│ │
│ • Generate keypair in MEMORY (not stored) │
│ • Session-bound (dies when tab closes) │
│ • Combined with fingerprint + token binding │
│ • NEVER fully trusted (always require OTP for sensitive) │
│ │
└─────────────────────────────────────────────────────────────┘
Platform Security Comparison
| Aspect | iOS | Android | Web |
|---|---|---|---|
| Key Storage | Secure Enclave | StrongBox / TEE | Memory only |
| Hardware Protected | ✅ Yes | ✅ Yes | ❌ No |
| Key Extractable | ❌ Never | ❌ Never | N/A (ephemeral) |
| Forgery Possible | ❌ No | ❌ No | ⚠️ Harder |
| Trust Level | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Trust Duration | 30 days | 30 days | 7 days |
| Sensitive Actions | Some without OTP | Some without OTP | ALWAYS OTP |
Why Attacker Cannot Forge (Mobile)
What attacker has access to:
──────────────────────────────────────────────────────────────
✅ Victim's email/username (public or leaked)
✅ Victim's password (phishing, data breach, etc.)
✅ Victim's deviceId (could intercept network traffic)
✅ Victim's publicKey (stored on server, not secret)
✅ Can request fresh nonce anytime
What attacker CANNOT get:
──────────────────────────────────────────────────────────────
❌ Victim's privateKey (locked in hardware)
• Cannot extract from Secure Enclave
• Cannot extract even with physical device access
• Cannot extract even if device is jailbroken
Without privateKey:
──────────────────────────────────────────────────────────────
❌ Cannot create valid signature
❌ Server rejects login
❌ Attack fails
Challenge/Nonce Purpose
Without nonce:
──────────────────────────────────────────────────────────────
1. Victim logs in legitimately
2. Attacker intercepts: { deviceId, signature }
3. Attacker replays exact same request
4. Server: "Signature valid!" ✅
5. Attacker is in 😱
Problem: Signature is always the same for same deviceId
With nonce:
──────────────────────────────────────────────────────────────
1. Victim logs in legitimately
- Gets nonce "ch_abc123"
- Signs "ch_abc123|timestamp|deviceId"
2. Attacker intercepts the request
3. Attacker replays exact same request 1 minute later
4. Server: "Nonce ch_abc123 already used/expired!" ❌
5. Attacker tries to get new nonce and replay
- Gets nonce "ch_xyz789"
- But old signature was for "ch_abc123"
- Signature doesn't match new nonce ❌
6. Attacker cannot create new signature
- Needs privateKey to sign "ch_xyz789|..."
- privateKey is in victim's phone ❌
7. Attack fails ✅
Key insight:
──────────────────────────────────────────────────────────────
Nonce makes each signature UNIQUE and TIME-LIMITED
Even captured valid signatures become useless after ~60 seconds
WEB SECURITY MODEL (PKCE-Style)
The Problem: Web Cannot Keep Secrets
Mobile App:
────────────────────────────────────────────────────────
✅ Compiled binary (hard to reverse engineer)
✅ Secure storage (Keychain, Keystore)
✅ Hardware protection (Secure Enclave, TEE)
✅ Can store secrets safely
Web Browser:
────────────────────────────────────────────────────────
❌ JavaScript is readable (View Source)
❌ localStorage/IndexedDB accessible via DevTools
❌ No hardware-protected storage
❌ Any "secret" can be extracted
If we store a private key in browser:
→ Open DevTools → Application → IndexedDB → Copy the key → Use anywhere 😱
The Solution: PKCE-Style Ephemeral Keys
OAuth2 had the same problem. Their solution: Don't store a secret. Generate a temporary one-time proof.
┌─────────────────────────────────────────────────────────────┐
│ WEB DEVICE AUTH (PKCE-Style) │
├─────────────────────────────────────────────────────────────┤
│ │
│ Instead of: │
│ Store private key → Sign challenges │
│ (Private key can be stolen from IndexedDB) │
│ │
│ We do: │
│ Each session: Generate fresh keypair in MEMORY │
│ Register public key with server for THIS SESSION │
│ Private key lives only in JavaScript memory │
│ When tab closes → key is gone → nothing to steal │
│ │
└─────────────────────────────────────────────────────────────┘
Web Security Layers
┌─────────────────────────────────────────────────────────────┐
│ WEB SECURITY MODEL │
├─────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: Session-Bound Cryptographic Proof │
│ • Generate keypair in memory (not stored) │
│ • Proves: "Same browser tab that started auth" │
│ • Prevents: Request interception/replay │
│ │
│ LAYER 2: Browser Fingerprint │
│ • Collect browser characteristics │
│ • Proves: "Likely same browser/device" │
│ • Prevents: Token theft to different browser │
│ │
│ LAYER 3: Bound Tokens │
│ • Tokens bound to fingerprint + IP range │
│ • Server validates on each request │
│ • Prevents: Token theft/export │
│ │
│ LAYER 4: Never Fully Trust │
│ • Web devices NEVER get "trusted" status │
│ • Sensitive actions ALWAYS require OTP │
│ • Shorter token lifetime than mobile │
│ │
└─────────────────────────────────────────────────────────────┘
Web vs Mobile Trust Levels
┌─────────────────────────────────────────────────────────────┐
│ TRUST LEVEL COMPARISON │
├─────────────────────────────────────────────────────────────┤
│ │
│ MOBILE (iOS/Android): │
│ After OTP verification: │
│ • Device becomes "TRUSTED" │
│ • Trust lasts 30 days │
│ • Password-only login allowed │
│ • Most actions without re-auth │
│ • Hardware key proves device identity │
│ │
│ WEB (Browser): │
│ After OTP verification: │
│ • Device is "RECOGNIZED" (not trusted) │
│ • Recognition lasts 7 days (shorter) │
│ • Password-only login allowed for basic actions │
│ • Sensitive actions ALWAYS need OTP: │
│ • Change password │
│ • Change email/phone │
│ • View payment methods │
│ • Delete account │
│ • Large purchases │
│ • Session-bound keys (weaker than hardware) │
│ │
│ WHY THE DIFFERENCE: │
│ Mobile: Hardware guarantees "this is THE device" │
│ Web: Software only guarantees "probably same browser" │
│ │
└─────────────────────────────────────────────────────────────┘
AUTHENTICATION FLOWS
Device Registration (First App Launch - Mobile)
┌──────────────┐ ┌──────────────┐
│ Device │ │ Server │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1. Generate key pair in hardware │
│ Private key → Secure Enclave │
│ Public key → exportable │
│ │
│ 2. GET /auth/challenge │
│────────────────────────────────────────────▶│
│ │
│ 3. { nonce: "ch_abc123", expiresIn: 60 } │
│◀────────────────────────────────────────────│
│ │
│ 4. Sign nonce with private key │
│ message = "ch_abc123|timestamp|deviceId"│
│ signature = sign(message, privateKey) │
│ │
│ 5. POST /auth/device/register │
│ { │
│ deviceId: "ios_xyz...", │
│ publicKey: "MFkw...", │
│ nonce: "ch_abc123", │
│ timestamp: 1736611200000, │
│ signature: "MEUC...", │
│ platform: "IOS" │
│ } │
│────────────────────────────────────────────▶│
│ │
│ 6. Verify signature │
│ using publicKey │
│ │
│ 7. Store: │
│ deviceId → publicKey│
│ │
│ 8. { success: true, deviceId: "ios_xyz" } │
│◀────────────────────────────────────────────│
│ │
Login Flow (Mobile - With Device Signature)
┌──────────────┐ ┌──────────────┐
│ Device │ │ Server │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1. GET /auth/challenge │
│────────────────────────────────────────────▶│
│ │
│ 2. { nonce: "ch_xyz789", expiresIn: 60 } │
│◀────────────────────────────────────────────│
│ │
│ 3. Sign: signature = sign( │
│ "ch_xyz789|1736611200000|ios_xyz...", │
│ privateKey │
│ ) │
│ │
│ 4. POST /auth/login │
│ { │
│ identifier: "alex@email.com", │
│ password: "***", │
│ deviceAuth: { │
│ deviceId: "ios_xyz...", │
│ nonce: "ch_xyz789", │
│ timestamp: 1736611200000, │
│ signature: "MEUC...", │
│ platform: "IOS" │
│ } │
│ } │
│────────────────────────────────────────────▶│
│ │
│ 5. Validate nonce │
│ (exists in Redis?) │
│ │
│ 6. Delete nonce │
│ (one-time use) │
│ │
│ 7. Get publicKey │
│ for deviceId │
│ │
│ 8. Verify signature │
│ │
│ 9. Check password │
│ │
│ 10. Check device trust │
│ status │
│ │
│ 11. { accessToken, refreshToken, ... } │
│◀────────────────────────────────────────────│
│ │
Web Session Flow (PKCE-Style)
┌──────────────┐ ┌──────────────┐
│ Browser │ │ Server │
└──────┬───────┘ └──────┬───────┘
│ │
│ User opens login page │
│ │
│ 1. Generate keypair IN MEMORY │
│ const keyPair = await crypto.subtle │
│ .generateKey(ECDSA, P-256) │
│ ⚠️ NOT stored anywhere │
│ ⚠️ Lives only in JS variable │
│ │
│ 2. Generate browser fingerprint │
│ • Screen size, timezone, language │
│ • Canvas hash, WebGL renderer │
│ • → Hash all into single ID │
│ │
│ 3. POST /auth/web/session │
│ { │
│ publicKey: "MFkw...", │
│ fingerprint: "fp_abc123..." │
│ } │
│────────────────────────────────────────────▶│
│ │
│ 4. Generate sessionId │
│ 5. Store in Redis: │
│ sessionId → │
│ publicKey, │
│ fingerprint, │
│ ipAddress │
│ TTL: 10 minutes │
│ │
│ 6. { sessionId: "ws_xyz...", │
│ nonce: "ch_abc..." } │
│◀────────────────────────────────────────────│
│ │
│ User enters email + password │
│ │
│ 7. Sign the nonce │
│ signature = sign( │
│ nonce + timestamp + sessionId, │
│ privateKey ← still in memory │
│ ) │
│ │
│ 8. POST /auth/login │
│ { │
│ identifier: "alex@email.com", │
│ password: "***", │
│ webAuth: { │
│ sessionId: "ws_xyz...", │
│ nonce: "ch_abc...", │
│ timestamp: 1736611200000, │
│ signature: "MEUC...", │
│ fingerprint: "fp_abc123..." │
│ } │
│ } │
│────────────────────────────────────────────▶│
│ │
│ 9. Get session from │
│ Redis │
│ │
│ 10. Verify: │
│ • Session exists │
│ • Not expired │
│ • Signature valid │
│ • Fingerprint match │
│ • IP in range │
│ │
│ 11. Delete session │
│ (one-time use) │
│ │
│ 12. Create BOUND tokens │
│ (bound to fp + IP) │
│ │
│ 13. { │
│ accessToken: "...", │
│ refreshToken: "...", │
│ device: { trusted: false } │
│ } │
│◀────────────────────────────────────────────│
│ │
SCALABLE ARCHITECTURE
┌─────────────────────────────────────────────────────────────────────────────────┐
│ NEXTGATE SECURITY ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ API GATEWAY / LOAD BALANCER │ │
│ │ (Rate Limiting, DDoS Protection) │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ AUTH SERVICE (Stateless) │ │
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Challenge/Nonce Service │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │ Node 1 │ │ Node 2 │ │ Node 3 │ │ Node N │ (Stateless) │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ └────────────┴─────┬──────┴────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌───────────────────────┐ │ │ │
│ │ │ │ Redis Cluster │ (Nonce storage, 60s TTL) │ │ │
│ │ │ │ (High Availability) │ │ │ │
│ │ │ └───────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Signature Verification Service │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │ Node 1 │ │ Node 2 │ │ Node 3 │ │ Node N │ (Stateless) │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ └────────────┴─────┬──────┴────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ PostgreSQL (Primary-Replica) │ │ │ │
│ │ │ │ device_keys table (deviceId → publicKey) │ │ │ │
│ │ │ │ Cached in Redis for fast lookups │ │ │ │
│ │ │ └───────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ SCALABILITY: │
│ • Challenge generation: < 5ms (any node can generate) │
│ • Signature verification: < 10ms (pure computation) │
│ • Public keys cached in Redis (1-hour TTL) │
│ • Can handle 10,000+ logins/second per node │
│ • Horizontal scaling: just add more nodes │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
TOKEN BINDING (Web Extra Protection)
┌─────────────────────────────────────────────────────────────┐
│ TOKEN BINDING │
├─────────────────────────────────────────────────────────────┤
│ │
│ Traditional Token: │
│ { │
│ "sub": "user_123", │
│ "exp": 1736614800 │
│ } │
│ Problem: Anyone with this token can use it │
│ │
│ │
│ Bound Token (What we do for web): │
│ { │
│ "sub": "user_123", │
│ "exp": 1736614800, │
│ "device_fp": "fp_abc123...", ← Must match │
│ "ip_hash": "a1b2c3...", ← Must be in range │
│ "platform": "WEB" ← Affects trust level │
│ } │
│ │
│ On every request, server checks: │
│ • Current fingerprint ≈ token's device_fp │
│ • Current IP in same /24 range as token's IP │
│ • If mismatch → reject OR require re-auth │
│ │
└─────────────────────────────────────────────────────────────┘
BROWSER FINGERPRINT
┌─────────────────────────────────────────────────────────────┐
│ BROWSER FINGERPRINT │
├─────────────────────────────────────────────────────────────┤
│ │
│ Components collected: │
│ 1. Screen: "1920x1080" │
│ 2. Color Depth: 24 │
│ 3. Timezone: "Africa/Dar_es_Salaam" │
│ 4. Language: "en-US" │
│ 5. Platform: "MacIntel" │
│ 6. CPU Cores: 8 │
│ 7. Memory: 8 (GB, approximate) │
│ 8. Canvas Hash: "a1b2c3..." (drawn image fingerprint) │
│ 9. WebGL Renderer: "Apple M1" │
│ 10. Audio Context fingerprint │
│ │
│ All combined → SHA256 → "fp_7f9a2b3c..." │
│ │
│ Stability: │
│ • ~90% of users have unique fingerprint │
│ • Changes if: browser update, OS update, new monitor │
│ • We allow ~15% variance (fuzzy matching) │
│ │
│ Privacy Note: │
│ • NOT used for tracking users │
│ • Only to verify "same browser" for security │
│ • Stored hashed, associated with user session │
│ │
└─────────────────────────────────────────────────────────────┘
ATTACK SCENARIO ANALYSIS
ATTACK 1: Steal the private key (Mobile)
────────────────────────────────────────────────────────────────
Attacker: Tries to extract key from device
Result: Key is in Secure Enclave / StrongBox
Cannot be extracted even with physical access
Verdict: ❌ FAILED
ATTACK 2: Steal the private key (Web)
────────────────────────────────────────────────────────────────
Attacker: Opens DevTools, looks for private key
Result: Key is in JavaScript memory, not in storage
When attacker opens DevTools, they're in THEIR session
with THEIR keypair, not victim's
Verdict: ❌ FAILED
ATTACK 3: Intercept and replay request
────────────────────────────────────────────────────────────────
Attacker: Captures { sessionId, nonce, signature }
Attacker: Replays the exact request
Server: "Nonce already used/expired"
Verdict: ❌ FAILED (nonce is one-time use)
ATTACK 4: Get new nonce, use old signature
────────────────────────────────────────────────────────────────
Attacker: Gets new nonce "ch_NEW..."
Attacker: Uses old signature (signed for "ch_OLD...")
Server: Signature doesn't match nonce "ch_NEW..."
Verdict: ❌ FAILED (signature bound to specific nonce)
ATTACK 5: Steal tokens from victim's browser
────────────────────────────────────────────────────────────────
Attacker: Uses XSS to steal accessToken
Attacker: Uses token from different browser/IP
Server: Fingerprint mismatch! IP range mismatch!
Verdict: ❌ FAILED (tokens bound to browser + IP)
ATTACK 6: Create fake session from different browser
────────────────────────────────────────────────────────────────
Attacker: Knows victim's email + password
Attacker: Creates own session (own keypair, own fingerprint)
Attacker: Logs in successfully... BUT
Server: "New device detected, OTP required"
Attacker: Doesn't have victim's phone
Verdict: ❌ FAILED (OTP still required for new device)
SUMMARY: WHAT WE STORE WHERE
┌─────────────────────────────────────────────────────────────┐
│ WHAT WE STORE WHERE (MOBILE) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ON DEVICE (Secure Storage): │
│ ✅ Private key (in Secure Enclave / Keystore) │
│ ✅ deviceId (identifier) │
│ ✅ accessToken (short-lived) │
│ ✅ refreshToken (long-lived) │
│ │
│ ON SERVER: │
│ ✅ Public key (for signature verification) │
│ ✅ deviceId → userId mapping │
│ ✅ Trust status and expiry │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ WHAT WE STORE WHERE (WEB) │
├─────────────────────────────────────────────────────────────┤
│ │
│ IN BROWSER: │
│ ✅ accessToken (short-lived, bound) │
│ ✅ refreshToken (medium-lived, bound) │
│ ✅ deviceId (just an identifier, not secret) │
│ ❌ NO private keys stored │
│ ❌ NO long-term secrets │
│ │
│ IN MEMORY ONLY (during session): │
│ ✅ Ephemeral keypair (dies when tab closes) │
│ │
│ ON SERVER: │
│ ✅ Session public key (Redis, 10-min TTL) │
│ ✅ Browser fingerprint hash │
│ ✅ IP range for token binding │
│ │
└─────────────────────────────────────────────────────────────┘
DEVICE SECURITY API ENDPOINTS
1. Get Challenge (All Platforms)
GET /api/v1/auth/challenge
Called before any authenticated action (login, register device, refresh token).
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Challenge generated",
"action_time": "2025-01-11T16:00:00Z",
"data": {
"nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
"expiresIn": 60,
"expiresAt": "2025-01-11T16:01:00Z"
}
}
2. Register Device (Mobile - First Launch)
POST /api/v1/auth/device/register
Called on first app launch to register the device's public key.
Request:
{
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
"nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
"timestamp": 1736611200000,
"signature": "MEUCIQC7...",
"platform": "IOS"
}
| Field | Description |
|---|---|
deviceId |
SHA256 hash of public key, prefixed with platform |
publicKey |
Base64-encoded ECDSA P-256 public key |
nonce |
Challenge from /auth/challenge |
timestamp |
Current time in milliseconds |
signature |
Sign(nonce|timestamp|deviceId, privateKey) |
platform |
IOS, ANDROID, or WEB |
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Device registered successfully",
"action_time": "2025-01-11T16:00:05Z",
"data": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"registered": true,
"securityLevel": "SECURE_ENCLAVE",
"note": "Device will be linked to user account on login/signup"
}
}
Response (Invalid Signature):
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Invalid device signature",
"action_time": "2025-01-11T16:00:05Z",
"data": {
"code": "INVALID_SIGNATURE",
"suggestion": "Ensure keys are generated correctly"
}
}
3. Initialize Web Session (Web Only)
POST /api/v1/auth/web/session
Called when user opens login page in browser.
Request:
{
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
"fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}
| Field | Description |
|---|---|
publicKey |
Ephemeral public key (generated in memory) |
fingerprint |
Browser fingerprint hash |
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Web session initialized",
"action_time": "2025-01-11T16:00:00Z",
"data": {
"sessionId": "ws_abc123xyz789",
"nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
"expiresIn": 600,
"expiresAt": "2025-01-11T16:10:00Z"
}
}
4. Login with Device Auth (Mobile)
POST /api/v1/auth/login
Request:
{
"identifier": "alex@example.com",
"password": "MySecurePass123!",
"deviceAuth": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
"timestamp": 1736611200000,
"signature": "MEUCIQC7...",
"platform": "IOS"
}
}
Response (Success - Trusted Device):
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2025-01-11T16:00:10Z",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"requiresOtp": false,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/..."
},
"device": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"trusted": true,
"securityLevel": "SECURE_ENCLAVE",
"trustExpiresAt": "2025-02-10T16:00:10Z"
}
}
}
Response (New Device - OTP Required):
{
"success": true,
"httpStatus": "OK",
"message": "New device detected. Please verify with OTP.",
"action_time": "2025-01-11T16:00:10Z",
"data": {
"requiresOtp": true,
"otpReason": "NEW_DEVICE",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpSentTo": "+255*****678",
"otpMethod": "SMS",
"expiresAt": "2025-01-11T16:10:00Z",
"device": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"isNew": true,
"securityLevel": "SECURE_ENCLAVE"
}
}
}
Response (Invalid Signature):
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Invalid device signature",
"action_time": "2025-01-11T16:00:10Z",
"data": {
"code": "INVALID_SIGNATURE",
"suggestion": "Please ensure your app is up to date"
}
}
Response (Nonce Expired/Used):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Challenge expired or already used",
"action_time": "2025-01-11T16:00:10Z",
"data": {
"code": "INVALID_NONCE",
"suggestion": "Request a new challenge and try again"
}
}
5. Login with Web Auth (Web Browser)
POST /api/v1/auth/login
Request:
{
"identifier": "alex@example.com",
"password": "MySecurePass123!",
"webAuth": {
"sessionId": "ws_abc123xyz789",
"nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
"timestamp": 1736611200000,
"signature": "MEUCIQC7...",
"fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}
}
Response (Success - Web is NEVER fully trusted):
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2025-01-11T16:00:10Z",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"requiresOtp": false,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson"
},
"device": {
"trusted": false,
"recognized": true,
"platform": "WEB",
"recognitionExpiresAt": "2025-01-18T16:00:10Z",
"restrictions": [
"OTP required for password change",
"OTP required for email change",
"OTP required for phone change",
"OTP required for payment actions",
"OTP required for account deletion"
]
},
"tokenBinding": {
"boundToFingerprint": true,
"boundToIpRange": true,
"note": "Token will be invalidated if used from different browser/network"
}
}
}
6. Verify Device OTP (After New Device Detected)
POST /api/v1/auth/device/verify-otp
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456",
"trustDevice": true,
"deviceAuth": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"nonce": "ch_newNonceForVerification",
"timestamp": 1736611260000,
"signature": "MEUCIQD8...",
"platform": "IOS"
}
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Device verified and trusted",
"action_time": "2025-01-11T16:01:00Z",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson"
},
"device": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"trusted": true,
"securityLevel": "SECURE_ENCLAVE",
"trustExpiresAt": "2025-02-10T16:01:00Z"
}
}
}
7. Refresh Token (With Device Signature)
POST /api/v1/auth/token/refresh
Refresh tokens also require device signature to prevent stolen refresh token usage.
Request (Mobile):
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"deviceAuth": {
"deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"nonce": "ch_refreshNonce123",
"timestamp": 1736614800000,
"signature": "MEUCIQDx...",
"platform": "IOS"
}
}
Request (Web):
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"webAuth": {
"sessionId": "ws_abc123xyz789",
"nonce": "ch_refreshNonce123",
"timestamp": 1736614800000,
"signature": "MEUCIQDx...",
"fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Token refreshed",
"action_time": "2025-01-11T17:00:00Z",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600
}
}
Response (Device Mismatch):
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token does not belong to this device",
"action_time": "2025-01-11T17:00:00Z",
"data": {
"code": "DEVICE_MISMATCH",
"suggestion": "Please login again"
}
}
DEVICE KEYS DATABASE SCHEMA
-- Device Keys Table (stores public keys for verification)
CREATE TABLE device_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
device_id VARCHAR(64) NOT NULL UNIQUE,
-- Cryptographic identity
public_key TEXT NOT NULL,
key_algorithm VARCHAR(20) DEFAULT 'EC_P256',
-- Device metadata (server-derived from User-Agent)
platform VARCHAR(20) NOT NULL, -- IOS, ANDROID, WEB
device_name VARCHAR(255),
security_level VARCHAR(20), -- SECURE_ENCLAVE, STRONGBOX, TEE, SOFTWARE
-- Trust status
is_trusted BOOLEAN DEFAULT FALSE,
trust_expires_at TIMESTAMP,
-- Activity tracking
last_active_at TIMESTAMP DEFAULT NOW(),
last_ip_address VARCHAR(45),
last_location VARCHAR(255),
-- Audit
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_user_device UNIQUE (user_id, device_id)
);
CREATE INDEX idx_device_keys_user_id ON device_keys(user_id);
CREATE INDEX idx_device_keys_device_id ON device_keys(device_id);
CREATE INDEX idx_device_keys_last_active ON device_keys(last_active_at);
-- Web Sessions Table (Redis is preferred, but DB fallback)
CREATE TABLE web_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id VARCHAR(64) NOT NULL UNIQUE,
-- Session data
public_key TEXT NOT NULL,
fingerprint_hash VARCHAR(64) NOT NULL,
ip_address VARCHAR(45),
-- Expiry
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP, -- NULL = not used yet
-- Audit
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_web_sessions_session_id ON web_sessions(session_id);
CREATE INDEX idx_web_sessions_expires ON web_sessions(expires_at);
REDIS KEYS STRUCTURE
# Nonce/Challenge storage (60 second TTL)
nonce:{nonce_value} = {ip_address}|{created_timestamp}
# Web session storage (10 minute TTL)
websession:{session_id} = {
"publicKey": "MFkw...",
"fingerprint": "fp_abc123",
"ipAddress": "196.41.xxx.xxx",
"createdAt": 1736611200000
}
# Public key cache (1 hour TTL)
pubkey:{device_id} = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
# Device trust cache (matches trust expiry)
devicetrust:{device_id} = {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"trusted": true,
"expiresAt": 1739203200000
}
Problem: If username is used in JWT tokens, changing username requires logout (bad UX).
Solution: Separate system identifier from display username.
| Field | Type | Purpose | Can Change? | Used In |
|---|---|---|---|---|
id |
UUID | Primary key | ❌ Never | DB relations |
systemUsername |
String | Internal identifier | ❌ Never | JWT tokens, internal APIs |
userName |
String | Public @handle | ✅ Yes | Profile URL, mentions, search, display |
How it works:
systemUsernameis auto-generated at signup (e.g.,usr_550e8400e29b41d4)userNameis user-chosen during onboarding (e.g.,alexvibes)- JWT tokens contain
systemUsername→ user can changeuserNamewithout logout - Profile URLs use
userName:app.com/@alexvibes - Mentions use
userName:@alexvibes
Username Change Flow:
User changes userName from "alex" to "alexnew"
│
▼
┌─────────────────────────────────┐
│ 1. Validate new userName │
│ 2. Check availability │
│ 3. Update userName in DB │
│ 4. Return success │
│ │
│ JWT stays valid (uses │
│ systemUsername, unchanged) │
│ │
│ NO LOGOUT REQUIRED ✅ │
└─────────────────────────────────┘
Database:
account_table:
id UUID PRIMARY KEY
system_username VARCHAR(50) UNIQUE NOT NULL -- "usr_550e8400e29b41d4"
user_name VARCHAR(30) UNIQUE NOT NULL -- "alexvibes"
...
Flow Diagram
┌─────────────────────────────────────────────────────────────────┐
│ SIGNUP FLOW (5 Screens) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Screen 1: Sign Up Method │
│ ┌─────────────────┐ │
│ │ Phone/Email/ │──► OTP Sent ──► Verify OTP ──► Account │
│ │ Google/Apple │ Created │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ Screen 2: Name & Birthdate │
│ ┌─────────────────┐ │
│ │ Display Name │──► Validate Age ──► Save │
│ │ Birthdate │ │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ Screen 3: Profile Setup │
│ ┌─────────────────┐ │
│ │ Profile Pic │──► Username Check ──► Save │
│ │ Username │ │
│ │ Bio │ │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ Screen 4: Interests │
│ ┌─────────────────┐ │
│ │ Select 5-10 │──► Save Preferences │
│ │ Categories │ │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ Screen 5: Complete! ──► Home Feed │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ LOGIN FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DEVICE CHECK FIRST │ │
│ │ ┌─────────────┐ │ │
│ │ │ Device Info │──► Known & Active? ──► YES ──► Continue │ │
│ │ └─────────────┘ │ │ │
│ │ NO │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ OTP Required First │ │
│ │ (New/Inactive Device) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Option A: Password Login (if password set & device trusted) │
│ ┌─────────────────┐ │
│ │ Identifier + │──► Validate ──► Access Token │
│ │ Password │ │
│ └─────────────────┘ │
│ │ │
│ └──► "Forgot Password?" ──► Password Reset Flow │
│ └──► "Login with OTP instead" ──► Option B │
│ │
│ Option B: Passwordless/OTP Login (always available) │
│ ┌─────────────────┐ │
│ │ Phone/Email │──► OTP Sent ──► Verify ──► Access Token │
│ └─────────────────┘ │
│ │ │
│ └──► "Lost access to phone/email?" ──► Account Recovery │
│ │
│ Option C: Social Login (Google/Apple) │
│ ┌─────────────────┐ │
│ │ Google/Apple │──► OAuth ──► Check Existing ──► Link/Create│
│ └─────────────────┘ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Email matches existing? │ │
│ │ YES → Link Account Flow │ │
│ │ NO → Create New │ │
│ └─────────────────────────┘ │
│ │
│ Account Recovery (Lost Access) │
│ ┌─────────────────┐ │
│ │ Verify Identity │──► Support Ticket ──► Manual Review │
│ │ (ID upload, │ │
│ │ selfie, etc.) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
SECURITY ARCHITECTURE
Why We Need Extra Protection for OAuth Users
The Problem:
Attacker hacks victim's Google account
│
▼
Attacker can access ALL apps linked to that Google
│
▼
😱 If we only rely on Google, attacker owns the account
Our Solution: Multi-Layer Security
┌─────────────────────────────────────────────────────────────────┐
│ NEXTGATE SECURITY LAYERS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Authentication (Who are you?) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Password / OTP / Google / Apple │ │
│ │ (Any of these can authenticate) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Layer 2: Device Trust (Is this your device?) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ New device? → OTP to PHONE required │ │
│ │ Inactive 30+ days? → OTP to PHONE required │ │
│ │ Trusted device? → Pass through │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Layer 3: Sensitive Actions (Extra verification) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Change password → OTP to PHONE │ │
│ │ Change email → OTP to PHONE │ │
│ │ Change phone → OTP to OLD phone + NEW phone │ │
│ │ Link/Unlink OAuth → OTP to PHONE │ │
│ │ Delete account → OTP to PHONE + Password (if set) │ │
│ │ Large purchases → OTP to PHONE (configurable) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 🔑 KEY INSIGHT: Phone is the ULTIMATE trust anchor │
│ Even if Google/Apple/Email is hacked, phone protects you │
│ │
└─────────────────────────────────────────────────────────────────┘
Phone as Ultimate Recovery Method
| Auth Method Compromised | Can Attacker Access Account? |
|---|---|
| Password leaked | ❌ No - needs device OTP or phone OTP |
| Email hacked | ❌ No - sensitive actions need phone OTP |
| Google hacked | ❌ No - new device needs phone OTP |
| Apple hacked | ❌ No - new device needs phone OTP |
| Phone stolen (unlocked) | ⚠️ Partial - but needs password for sensitive actions |
| Phone + Password both | ✅ Yes - full access (this is expected) |
Mandatory Phone Verification
During onboarding, we STRONGLY encourage phone verification:
┌─────────────────────────────────────────────────────────────────┐
│ ONBOARDING PHONE PROMPT │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "Add your phone number for account security" │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🔒 Recover your account if you lose access │ │
│ │ 🔒 Get alerts about suspicious activity │ │
│ │ 🔒 Verify sensitive actions │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [+255] [___________] [Verify] │
│ │
│ [Skip for now] ← Show warning: │
│ "Without a verified phone, you may lose access to your │
│ account if you forget your password or lose access to │
│ your Google/Apple account." │
│ │
└─────────────────────────────────────────────────────────────────┘
ACCOUNT LINKING & MERGING
When OAuth Email Matches Existing Account
Scenario Matrix:
| Existing Account State | OAuth Provider | Action |
|---|---|---|
| Email verified + password | Google (same email) | Prompt to link |
| Email verified + no password | Google (same email) | Prompt to link |
| Email unverified | Google (same email) | Auto-link (Google verified it) |
| Phone only (no email set) | Google (new email) | Prompt to add email or create new |
| Email verified but different | Google (different email) | Create new account |
Link Account Flow
POST /api/v1/auth/oauth/google
Request:
{
"idToken": "eyJhbGciOiJSUzI1NiIs...",
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS"
}
}
Response (Email Matches Existing - Link Required):
{
"success": true,
"httpStatus": "OK",
"message": "Account found with this email. Please verify to link.",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "LINK_REQUIRED",
"existingAccount": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"maskedEmail": "a***@example.com",
"maskedPhone": "+255*****678",
"hasPassword": true,
"authProviders": ["PHONE", "EMAIL"],
"createdAt": "2024-06-15T10:00:00"
},
"linkOptions": [
{
"method": "PASSWORD",
"available": true,
"description": "Enter your password to link"
},
{
"method": "OTP_PHONE",
"available": true,
"description": "Get OTP on +255*****678"
},
{
"method": "OTP_EMAIL",
"available": true,
"description": "Get OTP on a***@example.com"
}
],
"oauthProvider": "GOOGLE",
"oauthEmail": "alex@example.com",
"tempToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
Confirm Account Link (with Password)
POST /api/v1/auth/account/link/confirm
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"method": "PASSWORD",
"password": "MySecurePass123!"
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Google account linked successfully",
"action_time": "2025-01-11T16:01:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"email": "alex@example.com",
"authProviders": ["PHONE", "EMAIL", "GOOGLE"],
"onboardingComplete": true
},
"linkedProvider": {
"provider": "GOOGLE",
"email": "alex@example.com",
"linkedAt": "2025-01-11T16:01:00"
}
}
}
Confirm Account Link (with OTP)
POST /api/v1/auth/account/link/request-otp
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"method": "OTP_PHONE"
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your phone",
"action_time": "2025-01-11T16:01:00",
"data": {
"otpToken": "eyJhbGciOiJIUzI1NiIs...",
"sentTo": "+255*****678",
"method": "SMS",
"expiresAt": "2025-01-11T16:11:00"
}
}
POST /api/v1/auth/account/link/confirm
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"method": "OTP_PHONE",
"otpCode": "123456"
}
Auto-Link (Unverified Email)
When existing account has unverified email that matches Google email:
Response (Auto-Linked):
{
"success": true,
"httpStatus": "OK",
"message": "Account linked and email verified",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "AUTO_LINKED",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"email": "alex@example.com",
"isEmailVerified": true,
"authProviders": ["PHONE", "GOOGLE"]
},
"note": "Your Google account has been linked and email verified automatically"
}
}
Create New Account (No Match)
Response (New Account):
{
"success": true,
"httpStatus": "OK",
"message": "Account created successfully",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "CREATED",
"isNewUser": true,
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"systemUsername": "usr_660e8400e29b41d4",
"userName": null,
"email": "alex@example.com",
"firstName": "Alex",
"lastName": "Johnson",
"profilePictureUrl": "https://lh3.googleusercontent.com/...",
"isEmailVerified": true,
"hasPassword": false,
"authProviders": ["GOOGLE"],
"onboardingStep": "NAME_BIRTHDATE",
"onboardingComplete": false
},
"promptAddPhone": true,
"phonePromptMessage": "Add your phone number to secure your account and enable recovery options"
}
}
MANAGE LINKED ACCOUNTS
Get Linked Providers
GET /api/v1/auth/providers
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Linked providers retrieved",
"action_time": "2025-01-11T18:00:00",
"data": {
"providers": [
{
"provider": "PHONE",
"identifier": "+255*****678",
"verified": true,
"linkedAt": "2025-01-01T10:00:00",
"isPrimary": true,
"canUnlink": false,
"unlinkBlockedReason": "Phone is your primary recovery method"
},
{
"provider": "EMAIL",
"identifier": "a***@example.com",
"verified": true,
"linkedAt": "2025-01-01T10:00:00",
"isPrimary": false,
"canUnlink": true
},
{
"provider": "GOOGLE",
"identifier": "a***@gmail.com",
"verified": true,
"linkedAt": "2025-01-11T16:01:00",
"isPrimary": false,
"canUnlink": true
}
],
"availableToLink": [
{
"provider": "APPLE",
"description": "Sign in with Apple"
}
],
"hasPassword": true,
"securityNote": "You have 3 ways to access your account"
}
}
Unlink Provider
DELETE /api/v1/auth/providers/{provider}
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"verificationMethod": "OTP_PHONE",
"otpCode": "123456"
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Google account unlinked",
"action_time": "2025-01-11T18:10:00",
"data": {
"unlinkedProvider": "GOOGLE",
"remainingProviders": ["PHONE", "EMAIL"],
"securityNote": "You can no longer sign in with Google"
}
}
Response (Cannot Unlink - Only Provider):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot unlink your only sign-in method",
"action_time": "2025-01-11T18:10:00",
"data": {
"code": "CANNOT_UNLINK_ONLY_PROVIDER",
"suggestion": "Add another sign-in method before unlinking this one"
}
}
Link New Provider
POST /api/v1/auth/providers/link
Request:
{
"provider": "APPLE",
"identityToken": "eyJraWQiOiJXNldjT0...",
"authorizationCode": "c7e8a3f1b2d4..."
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Apple account linked successfully",
"action_time": "2025-01-11T18:15:00",
"data": {
"linkedProvider": {
"provider": "APPLE",
"identifier": "a***@privaterelay.appleid.com",
"linkedAt": "2025-01-11T18:15:00"
},
"totalProviders": 4
}
}
ACCOUNT RECOVERY (Lost Access)
When User Can't Access Any Auth Method
POST /api/v1/auth/recovery/request
Request:
{
"identifier": "alexvibes",
"recoveryReason": "LOST_PHONE",
"contactEmail": "backup@anotheremail.com"
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Recovery request submitted",
"action_time": "2025-01-11T20:00:00",
"data": {
"ticketId": "REC-2025-001234",
"status": "PENDING_REVIEW",
"estimatedResponseTime": "24-48 hours",
"nextSteps": [
"Check your backup email for instructions",
"Prepare identity verification documents",
"Our team will contact you within 48 hours"
],
"requiredDocuments": [
"Government-issued ID (passport, national ID)",
"Selfie holding ID",
"Proof of account ownership (screenshots, transaction history)"
]
}
}
SENSITIVE ACTIONS - OTP VERIFICATION
All sensitive actions require OTP to phone (regardless of how user logged in):
Sensitive Actions List
| Action | OTP Required? | Additional Verification |
|---|---|---|
| Change password | ✅ Phone OTP | Current password (if set) |
| Change email | ✅ Phone OTP | - |
| Change phone | ✅ Old phone OTP + New phone OTP | - |
| Link OAuth provider | ✅ Phone OTP | - |
| Unlink OAuth provider | ✅ Phone OTP | - |
| Delete account | ✅ Phone OTP | Password (if set) |
| View full payment methods | ✅ Phone OTP | - |
| Add payment method | ❌ No | - |
| Remove payment method | ✅ Phone OTP | - |
| Large purchase (>$100) | ⚙️ Configurable | - |
| Export account data | ✅ Phone OTP | - |
| Change security settings | ✅ Phone OTP | Password (if set) |
Request OTP for Sensitive Action
POST /api/v1/auth/sensitive-action/request-otp
Request:
{
"action": "CHANGE_EMAIL",
"newEmail": "newemail@example.com"
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your phone",
"action_time": "2025-01-11T19:00:00",
"data": {
"actionToken": "eyJhbGciOiJIUzI1NiIs...",
"action": "CHANGE_EMAIL",
"sentTo": "+255*****678",
"method": "SMS",
"expiresAt": "2025-01-11T19:10:00"
}
}
Confirm Sensitive Action
POST /api/v1/auth/sensitive-action/confirm
Request:
{
"actionToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456"
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Email updated successfully",
"action_time": "2025-01-11T19:01:00",
"data": {
"action": "CHANGE_EMAIL",
"completed": true,
"changes": {
"previousEmail": "a***@example.com",
"newEmail": "n***@example.com",
"emailVerified": false
},
"note": "Please verify your new email address"
}
}
NO PHONE? FALLBACK OPTIONS
If user didn't add phone during onboarding:
Prompt to Add Phone (Shown on sensitive actions)
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Phone verification required for this action",
"action_time": "2025-01-11T19:00:00",
"data": {
"code": "PHONE_REQUIRED",
"action": "CHANGE_EMAIL",
"options": [
{
"option": "ADD_PHONE",
"description": "Add and verify your phone number first",
"endpoint": "/api/v1/profile/add-phone"
},
{
"option": "USE_PASSWORD",
"available": true,
"description": "Use your password instead (less secure)"
},
{
"option": "CONTACT_SUPPORT",
"description": "Contact support for manual verification"
}
]
}
}
SUMMARY: AUTH METHODS & SECURITY
Complete Auth Provider Matrix
| Provider | Can Signup? | Can Login? | Provides Email? | Provides Phone? | Trust Level |
|---|---|---|---|---|---|
| Phone + OTP | ✅ | ✅ | ❌ | ✅ | HIGH |
| Email + OTP | ✅ | ✅ | ✅ | ❌ | MEDIUM |
| Email + Password | ❌ (need OTP first) | ✅ | ✅ | ❌ | MEDIUM |
| Google OAuth | ✅ | ✅ | ✅ | ❌ | MEDIUM |
| Apple OAuth | ✅ | ✅ | ✅ (may be relay) | ❌ | MEDIUM |
| Password only | ❌ | ✅ (trusted device) | ❌ | ❌ | LOW |
Security Recommendations for Users
{
"securityScore": 75,
"level": "GOOD",
"recommendations": [
{
"priority": "HIGH",
"action": "ADD_PHONE",
"title": "Verify your phone number",
"description": "Enables account recovery and secures sensitive actions",
"completed": false
},
{
"priority": "MEDIUM",
"action": "SET_PASSWORD",
"title": "Set a password",
"description": "Faster login on trusted devices",
"completed": true
},
{
"priority": "LOW",
"action": "LINK_BACKUP_EMAIL",
"title": "Add backup email",
"description": "Alternative recovery option",
"completed": false
}
]
}
---
## SCREEN 1: Sign Up
### 1.1 Initiate Signup (Phone)
**POST** `/api/v1/auth/signup/initiate`
**Request:**
```json
{
"method": "PHONE",
"phoneNumber": "+255712345678"
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your phone",
"action_time": "2025-01-11T15:20:00",
"data": {
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"method": "PHONE",
"maskedIdentifier": "+255*****678",
"expiresAt": "2025-01-11T15:30:00",
"resendAllowedAt": "2025-01-11T15:22:00",
"attemptsRemaining": 3
}
}
Response (Phone Already Registered):
{
"success": false,
"httpStatus": "CONFLICT",
"message": "This phone number is already registered",
"action_time": "2025-01-11T15:20:00",
"data": {
"code": "ACCOUNT_EXISTS",
"field": "phoneNumber",
"suggestion": "Please login instead"
}
}
1.2 Initiate Signup (Email)
POST /api/v1/auth/signup/initiate
Request:
{
"method": "EMAIL",
"email": "alex@example.com"
}
Response (Success):
{
"success": true,
"message": "OTP sent to your email",
"data": {
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"method": "EMAIL",
"maskedIdentifier": "a***@example.com",
"expiresAt": "2025-01-11T15:30:00Z",
"resendAllowedAt": "2025-01-11T15:22:00Z",
"attemptsRemaining": 3
},
"timestamp": "2025-01-11T15:20:00Z"
}
1.3 Verify Signup OTP
POST /api/v1/auth/signup/verify
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456"
}
Response (Success - Account Created):
{
"success": true,
"httpStatus": "OK",
"message": "Account created successfully",
"action_time": "2025-01-11T15:21:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": null,
"phoneNumber": "+255712345678",
"email": null,
"isPhoneVerified": true,
"isEmailVerified": false,
"hasPassword": false,
"onboardingStep": "NAME_BIRTHDATE",
"onboardingComplete": false,
"createdAt": "2025-01-11T15:20:00"
}
}
}
Response (Invalid OTP):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid OTP code",
"action_time": "2025-01-11T15:21:00",
"data": {
"code": "INVALID_OTP",
"attemptsRemaining": 2
}
}
Response (OTP Expired):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "OTP has expired",
"action_time": "2025-01-11T15:35:00",
"data": {
"code": "OTP_EXPIRED",
"suggestion": "Please request a new OTP"
}
}
1.4 Resend OTP
POST /api/v1/auth/otp/resend
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs..."
}
Response (Success):
{
"success": true,
"message": "OTP resent successfully",
"data": {
"newTempToken": "eyJhbGciOiJIUzI1NiIs...",
"maskedIdentifier": "+255*****678",
"expiresAt": "2025-01-11T15:40:00Z",
"resendAllowedAt": "2025-01-11T15:32:00Z",
"attemptsRemaining": 2
},
"timestamp": "2025-01-11T15:30:00Z"
}
Response (Too Soon):
{
"success": false,
"message": "Please wait before requesting another OTP",
"error": {
"code": "RESEND_COOLDOWN",
"resendAllowedAt": "2025-01-11T15:32:00Z",
"waitSeconds": 90
},
"timestamp": "2025-01-11T15:30:30Z"
}
1.5 Social Signup (Google)
POST /api/v1/auth/signup/google
Request:
{
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..."
}
Response (Success - New User):
{
"success": true,
"message": "Account created successfully",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"isNewUser": true,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"email": "alex@gmail.com",
"firstName": "Alex",
"lastName": "Johnson",
"profilePictureUrl": "https://lh3.googleusercontent.com/...",
"isEmailVerified": true,
"hasPassword": false,
"authProvider": "GOOGLE",
"onboardingStep": "NAME_BIRTHDATE",
"onboardingComplete": false,
"createdAt": "2025-01-11T15:20:00Z"
}
},
"timestamp": "2025-01-11T15:20:00Z"
}
Response (Existing User - Login):
{
"success": true,
"message": "Welcome back!",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"isNewUser": false,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"userName": "alexj",
"email": "alex@gmail.com",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"isEmailVerified": true,
"hasPassword": true,
"authProvider": "GOOGLE",
"onboardingComplete": true
}
},
"timestamp": "2025-01-11T15:20:00Z"
}
1.6 Social Signup (Apple)
POST /api/v1/auth/signup/apple
Request:
{
"identityToken": "eyJraWQiOiJXNldjT0...",
"authorizationCode": "c7e8a3f1b2d4...",
"firstName": "Alex",
"lastName": "Johnson"
}
Note: Apple only provides name on first authorization
Response: Same structure as Google response
SCREEN 2: Name & Birthdate
2.1 Save Name & Birthdate
PUT /api/v1/onboarding/name-birthdate
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"displayName": "Alex Johnson",
"firstName": "Alex",
"lastName": "Johnson",
"birthDate": "1995-06-15"
}
Response (Success):
{
"success": true,
"message": "Profile updated successfully",
"data": {
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"displayName": "Alex Johnson",
"firstName": "Alex",
"lastName": "Johnson",
"birthDate": "1995-06-15",
"age": 29,
"onboardingStep": "PROFILE_SETUP",
"onboardingComplete": false
}
},
"timestamp": "2025-01-11T15:22:00Z"
}
Response (Underage - Below 13):
{
"success": false,
"message": "You must be at least 13 years old to use this app",
"error": {
"code": "UNDERAGE",
"field": "birthDate",
"minimumAge": 13
},
"timestamp": "2025-01-11T15:22:00Z"
}
Response (Invalid Date):
{
"success": false,
"message": "Invalid birth date",
"error": {
"code": "INVALID_DATE",
"field": "birthDate",
"details": "Birth date cannot be in the future"
},
"timestamp": "2025-01-11T15:22:00Z"
}
SCREEN 3: Profile Setup
3.1 Check Username Availability
GET /api/v1/onboarding/username/check?username=alexvibes
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Note: Always returns suggestions (even when available) so user can pick alternatives
Response (Available):
{
"success": true,
"httpStatus": "OK",
"message": "Username is available",
"action_time": "2025-01-11T15:23:00",
"data": {
"username": "alexvibes",
"available": true,
"valid": true,
"suggestions": [
"alexvibes_",
"alexvibes1",
"thealexvibes",
"alexvibes_official",
"real_alexvibes"
]
}
}
Response (Taken):
{
"success": true,
"httpStatus": "OK",
"message": "Username is already taken",
"action_time": "2025-01-11T15:23:00",
"data": {
"username": "alex",
"available": false,
"valid": true,
"suggestions": [
"alex123",
"alex_vibes",
"alexcool",
"alex2025",
"thealex"
]
}
}
Response (Invalid Format):
{
"success": true,
"httpStatus": "OK",
"message": "Invalid username format",
"action_time": "2025-01-11T15:23:00",
"data": {
"username": "123alex",
"available": false,
"valid": false,
"validationError": "Username must start with a letter",
"suggestions": [
"alex123",
"alex_user",
"alexnew",
"user_alex",
"the_alex"
]
}
}
3.2 Upload Profile Picture
POST /api/v1/onboarding/profile-picture
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: multipart/form-data
Request (Form Data):
file: [binary image data]
Response (Success):
{
"success": true,
"message": "Profile picture uploaded successfully",
"data": {
"profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg",
"thumbnailUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380_thumb.jpg"
},
"timestamp": "2025-01-11T15:23:00Z"
}
Response (Invalid File):
{
"success": false,
"message": "Invalid file type",
"error": {
"code": "INVALID_FILE_TYPE",
"allowedTypes": ["image/jpeg", "image/png", "image/webp"],
"maxSizeMB": 5
},
"timestamp": "2025-01-11T15:23:00Z"
}
3.3 Save Profile Setup
PUT /api/v1/onboarding/profile-setup
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"userName": "alexvibes",
"bio": "Fashion lover | Shopaholic | Event creator 🔥",
"profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg"
}
Response (Success):
{
"success": true,
"message": "Profile setup completed",
"data": {
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"bio": "Fashion lover | Shopaholic | Event creator 🔥",
"profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg",
"onboardingStep": "INTERESTS",
"onboardingComplete": false
}
},
"timestamp": "2025-01-11T15:24:00Z"
}
Response (Username Taken - Race Condition):
{
"success": false,
"message": "Username was just taken by another user",
"error": {
"code": "USERNAME_TAKEN",
"field": "userName",
"suggestions": [
"alexvibes1",
"alexvibes_",
"thealexvibes"
]
},
"timestamp": "2025-01-11T15:24:00Z"
}
SCREEN 4: Interests
4.1 Get Available Interest Categories
GET /api/v1/onboarding/interests/categories
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
{
"success": true,
"message": "Categories retrieved successfully",
"data": {
"categories": [
{
"id": "cat_001",
"name": "Fashion",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion.png",
"color": "#FF6B6B"
},
{
"id": "cat_002",
"name": "Electronics",
"icon": "📱",
"iconUrl": "https://cdn.example.com/icons/electronics.png",
"color": "#4ECDC4"
},
{
"id": "cat_003",
"name": "Beauty & Cosmetics",
"icon": "💄",
"iconUrl": "https://cdn.example.com/icons/beauty.png",
"color": "#FF69B4"
},
{
"id": "cat_004",
"name": "Food & Drinks",
"icon": "🍔",
"iconUrl": "https://cdn.example.com/icons/food.png",
"color": "#F39C12"
},
{
"id": "cat_005",
"name": "Sports & Fitness",
"icon": "⚽",
"iconUrl": "https://cdn.example.com/icons/sports.png",
"color": "#2ECC71"
},
{
"id": "cat_006",
"name": "Music & Dance",
"icon": "🎵",
"iconUrl": "https://cdn.example.com/icons/music.png",
"color": "#9B59B6"
},
{
"id": "cat_007",
"name": "Home & Decor",
"icon": "🏠",
"iconUrl": "https://cdn.example.com/icons/home.png",
"color": "#E67E22"
},
{
"id": "cat_008",
"name": "Tech & Gadgets",
"icon": "💻",
"iconUrl": "https://cdn.example.com/icons/tech.png",
"color": "#3498DB"
},
{
"id": "cat_009",
"name": "Travel",
"icon": "✈️",
"iconUrl": "https://cdn.example.com/icons/travel.png",
"color": "#1ABC9C"
},
{
"id": "cat_010",
"name": "Gaming",
"icon": "🎮",
"iconUrl": "https://cdn.example.com/icons/gaming.png",
"color": "#8E44AD"
},
{
"id": "cat_011",
"name": "Books & Reading",
"icon": "📚",
"iconUrl": "https://cdn.example.com/icons/books.png",
"color": "#D35400"
},
{
"id": "cat_012",
"name": "Art & Design",
"icon": "🎨",
"iconUrl": "https://cdn.example.com/icons/art.png",
"color": "#E74C3C"
},
{
"id": "cat_013",
"name": "Health & Wellness",
"icon": "🧘",
"iconUrl": "https://cdn.example.com/icons/wellness.png",
"color": "#27AE60"
},
{
"id": "cat_014",
"name": "Automotive",
"icon": "🚗",
"iconUrl": "https://cdn.example.com/icons/auto.png",
"color": "#34495E"
},
{
"id": "cat_015",
"name": "Pets & Animals",
"icon": "🐾",
"iconUrl": "https://cdn.example.com/icons/pets.png",
"color": "#F1C40F"
},
{
"id": "cat_016",
"name": "Photography",
"icon": "📷",
"iconUrl": "https://cdn.example.com/icons/photo.png",
"color": "#7F8C8D"
},
{
"id": "cat_017",
"name": "Kids & Baby",
"icon": "👶",
"iconUrl": "https://cdn.example.com/icons/kids.png",
"color": "#FFB6C1"
},
{
"id": "cat_018",
"name": "Business & Finance",
"icon": "💼",
"iconUrl": "https://cdn.example.com/icons/business.png",
"color": "#2C3E50"
},
{
"id": "cat_019",
"name": "Entertainment",
"icon": "🎬",
"iconUrl": "https://cdn.example.com/icons/entertainment.png",
"color": "#C0392B"
},
{
"id": "cat_020",
"name": "DIY & Crafts",
"icon": "🛠️",
"iconUrl": "https://cdn.example.com/icons/diy.png",
"color": "#16A085"
}
],
"minimumSelection": 3,
"recommendedSelection": 5,
"maximumSelection": 15
},
"timestamp": "2025-01-11T15:25:00Z"
}
4.2 Save User Interests
POST /api/v1/onboarding/interests
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"categoryIds": [
"cat_001",
"cat_003",
"cat_006",
"cat_009",
"cat_012"
]
}
Response (Success):
{
"success": true,
"message": "Interests saved successfully",
"data": {
"selectedCount": 5,
"interests": [
{ "id": "cat_001", "name": "Fashion" },
{ "id": "cat_003", "name": "Beauty & Cosmetics" },
{ "id": "cat_006", "name": "Music & Dance" },
{ "id": "cat_009", "name": "Travel" },
{ "id": "cat_012", "name": "Art & Design" }
],
"onboardingStep": "COMPLETE",
"onboardingComplete": true
},
"timestamp": "2025-01-11T15:26:00Z"
}
Response (Too Few Selected):
{
"success": false,
"message": "Please select at least 3 interests",
"error": {
"code": "MINIMUM_NOT_MET",
"selectedCount": 1,
"minimumRequired": 3
},
"timestamp": "2025-01-11T15:26:00Z"
}
4.3 Skip Interests (Optional)
POST /api/v1/onboarding/interests/skip
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{}
Response:
{
"success": true,
"message": "Interests skipped. You can update them later in settings.",
"data": {
"onboardingStep": "COMPLETE",
"onboardingComplete": true,
"reminder": "We'll show you general content. Update your interests anytime for a personalized feed!"
},
"timestamp": "2025-01-11T15:26:00Z"
}
SCREEN 5: Complete Onboarding
5.1 Get Onboarding Summary & Suggestions
GET /api/v1/onboarding/complete
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
{
"success": true,
"message": "Welcome to the app! 🎉",
"data": {
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic.jpg",
"bio": "Fashion lover | Shopaholic | Event creator 🔥",
"onboardingComplete": true,
"hasPassword": false
},
"suggestions": {
"accountsToFollow": [
{
"id": "user_001",
"userName": "fashionista",
"displayName": "Fashion Hub",
"profilePictureUrl": "https://storage.example.com/...",
"bio": "Latest fashion trends",
"isVerified": true,
"followerCount": 12500,
"matchReason": "Based on your interest in Fashion"
},
{
"id": "user_002",
"userName": "beautyguru",
"displayName": "Beauty Tips Daily",
"profilePictureUrl": "https://storage.example.com/...",
"bio": "Makeup tutorials & reviews",
"isVerified": true,
"followerCount": 8300,
"matchReason": "Based on your interest in Beauty & Cosmetics"
}
],
"shopsToFollow": [
{
"id": "shop_001",
"name": "Style Studio",
"logoUrl": "https://storage.example.com/...",
"category": "Fashion",
"rating": 4.8,
"productCount": 156,
"matchReason": "Top rated in Fashion"
}
],
"upcomingEvents": [
{
"id": "event_001",
"title": "Summer Fashion Show",
"coverImageUrl": "https://storage.example.com/...",
"startDate": "2025-01-20T18:00:00Z",
"attendeeCount": 234,
"matchReason": "Fashion event near you"
}
]
},
"nextSteps": [
{
"action": "FOLLOW_ACCOUNTS",
"title": "Follow 5 accounts",
"description": "Get started by following accounts you like",
"reward": null
},
{
"action": "SET_PASSWORD",
"title": "Set a password",
"description": "For faster login next time",
"reward": null
},
{
"action": "FIRST_POST",
"title": "Create your first post",
"description": "Share something with the community",
"reward": null
}
]
},
"timestamp": "2025-01-11T15:27:00Z"
}
LOGIN FLOWS (With Device Trust)
Login Option A: Password Login
POST /api/v1/auth/login/password
Request:
{
"identifier": "alexvibes",
"password": "MySecurePass123!",
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"appVersion": "1.2.0"
}
}
identifier can be: username, email, or phone number
Response (Success - Trusted Device):
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2025-01-11T16:00:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"requiresOtp": false,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"email": "alex@example.com",
"phoneNumber": "+255712345678",
"isEmailVerified": true,
"isPhoneVerified": true,
"hasPassword": true,
"onboardingComplete": true
},
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:00:00"
}
}
}
Response (New Device - OTP Required):
{
"success": true,
"httpStatus": "OK",
"message": "New device detected. Please verify with OTP.",
"action_time": "2025-01-11T16:00:00",
"data": {
"requiresOtp": true,
"otpReason": "NEW_DEVICE",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpSentTo": "+255*****678",
"otpMethod": "SMS",
"expiresAt": "2025-01-11T16:10:00",
"device": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"isNew": true
},
"securityNote": "We detected a login from a new device. For your security, please verify with OTP."
}
}
Response (Inactive Device 30+ days - OTP Required):
{
"success": true,
"httpStatus": "OK",
"message": "Device inactive for 30+ days. Please verify with OTP.",
"action_time": "2025-01-11T16:00:00",
"data": {
"requiresOtp": true,
"otpReason": "INACTIVE_DEVICE",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpSentTo": "+255*****678",
"otpMethod": "SMS",
"expiresAt": "2025-01-11T16:10:00",
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"lastActiveAt": "2024-12-01T10:00:00",
"inactiveDays": 41
},
"securityNote": "This device hasn't been used in 41 days. Please verify it's you."
}
}
Response (Wrong Password):
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Invalid credentials",
"action_time": "2025-01-11T16:00:00",
"data": {
"code": "INVALID_CREDENTIALS",
"attemptsRemaining": 4,
"suggestion": "Forgot password? Use OTP login instead"
}
}
Response (Account Locked):
{
"success": false,
"httpStatus": "LOCKED",
"message": "Account temporarily locked due to multiple failed attempts",
"action_time": "2025-01-11T16:00:00",
"data": {
"code": "ACCOUNT_LOCKED",
"unlockAt": "2025-01-11T16:30:00",
"suggestion": "Try OTP login or wait 30 minutes"
}
}
Response (No Password Set):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "No password set for this account",
"action_time": "2025-01-11T16:00:00",
"data": {
"code": "NO_PASSWORD",
"suggestion": "Use OTP login or social login"
}
}
Login Option B: OTP Login (Passwordless)
B.1 Request OTP
POST /api/v1/auth/login/otp/request
Request:
{
"identifier": "+255712345678",
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"appVersion": "1.2.0"
}
}
identifier can be: email or phone number
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your phone",
"action_time": "2025-01-11T16:00:00",
"data": {
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"method": "SMS",
"maskedIdentifier": "+255*****678",
"expiresAt": "2025-01-11T16:10:00",
"resendAllowedAt": "2025-01-11T16:02:00",
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"isNew": false,
"lastActiveAt": "2025-01-10T14:30:00"
}
}
}
Response (User Not Found):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "No account found with this phone number",
"action_time": "2025-01-11T16:00:00",
"data": {
"code": "USER_NOT_FOUND",
"suggestion": "Would you like to create an account?",
"createAccountUrl": "/api/v1/auth/signup/initiate"
}
}
B.2 Verify OTP Login
POST /api/v1/auth/login/otp/verify
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456",
"trustDevice": true,
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"appVersion": "1.2.0"
}
}
Response (Success - Device Trusted):
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2025-01-11T16:01:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"hasPassword": false,
"onboardingComplete": true
},
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:01:00"
},
"promptSetPassword": true,
"passwordPromptMessage": "Set a password for faster login on trusted devices"
}
}
Response (Success - Device NOT Trusted by user choice):
{
"success": true,
"httpStatus": "OK",
"message": "Login successful (device not saved)",
"action_time": "2025-01-11T16:01:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"hasPassword": false,
"onboardingComplete": true
},
"device": {
"trusted": false,
"note": "This device will require OTP on next login"
}
}
}
Login Option C: Social Login (Google/Apple)
POST /api/v1/auth/oauth/google
Request:
{
"idToken": "eyJhbGciOiJSUzI1NiIs...",
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"appVersion": "1.2.0"
}
}
Response (Existing User - Trusted Device):
{
"success": true,
"httpStatus": "OK",
"message": "Welcome back!",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "LOGIN",
"isNewUser": false,
"requiresOtp": false,
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"email": "alex@gmail.com",
"profilePictureUrl": "https://storage.example.com/...",
"isEmailVerified": true,
"hasPassword": true,
"authProviders": ["PHONE", "EMAIL", "GOOGLE"],
"onboardingComplete": true
},
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:00:00"
}
}
}
Response (Existing User - New Device - OTP Required):
{
"success": true,
"httpStatus": "OK",
"message": "New device detected. Please verify with OTP.",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "DEVICE_VERIFICATION_REQUIRED",
"isNewUser": false,
"requiresOtp": true,
"otpReason": "NEW_DEVICE",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpSentTo": "+255*****678",
"otpMethod": "SMS",
"expiresAt": "2025-01-11T16:10:00",
"user": {
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/..."
},
"device": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"isNew": true
},
"securityNote": "Even though you signed in with Google, we need to verify this new device."
}
}
Response (Existing User - No Phone - Fallback Options):
{
"success": true,
"httpStatus": "OK",
"message": "New device detected. Please verify to continue.",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "VERIFICATION_REQUIRED_NO_PHONE",
"isNewUser": false,
"requiresVerification": true,
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"userName": "alexvibes",
"displayName": "Alex Johnson"
},
"device": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"isNew": true
},
"verificationOptions": [
{
"method": "ADD_PHONE",
"description": "Add phone number to receive OTP",
"recommended": true
},
{
"method": "OTP_EMAIL",
"description": "Send OTP to a***@gmail.com",
"available": true,
"lessSecure": true,
"note": "Email is less secure than phone"
},
{
"method": "PASSWORD",
"available": true,
"description": "Enter your password"
}
],
"securityNote": "For better security, we recommend adding a phone number."
}
}
Response (New User - Account Created):
{
"success": true,
"httpStatus": "OK",
"message": "Account created successfully",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "CREATED",
"isNewUser": true,
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"systemUsername": "usr_660e8400e29b41d4",
"userName": null,
"email": "alex@gmail.com",
"firstName": "Alex",
"lastName": "Johnson",
"profilePictureUrl": "https://lh3.googleusercontent.com/...",
"isEmailVerified": true,
"hasPassword": false,
"authProviders": ["GOOGLE"],
"onboardingStep": "NAME_BIRTHDATE",
"onboardingComplete": false
},
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:00:00",
"note": "First device is automatically trusted"
},
"promptAddPhone": true,
"phonePromptMessage": "Add your phone number to secure your account"
}
}
Response (Email Matches Existing - Link Required):
{
"success": true,
"httpStatus": "OK",
"message": "Account found with this email. Please verify to link.",
"action_time": "2025-01-11T16:00:00",
"data": {
"action": "LINK_REQUIRED",
"isNewUser": false,
"existingAccount": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"maskedEmail": "a***@example.com",
"maskedPhone": "+255*****678",
"hasPassword": true,
"authProviders": ["PHONE", "EMAIL"],
"createdAt": "2024-06-15T10:00:00"
},
"linkOptions": [
{
"method": "PASSWORD",
"available": true,
"description": "Enter your password to link"
},
{
"method": "OTP_PHONE",
"available": true,
"description": "Get OTP on +255*****678"
},
{
"method": "OTP_EMAIL",
"available": true,
"description": "Get OTP on a***@example.com"
}
],
"oauthProvider": "GOOGLE",
"oauthEmail": "alex@gmail.com",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"note": "Device will be trusted after account link"
}
}
}
Apple Sign In
POST /api/v1/auth/oauth/apple
Request:
{
"identityToken": "eyJraWQiOiJXNldjT0...",
"authorizationCode": "c7e8a3f1b2d4...",
"firstName": "Alex",
"lastName": "Johnson",
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"appVersion": "1.2.0"
}
}
Note: Apple only provides name on first authorization. Store it!
Responses: Same structure as Google OAuth responses above.
Verify Device OTP (After any login triggers device verification)
POST /api/v1/auth/login/verify-device
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456",
"trustDevice": true,
"deviceInfo": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"deviceType": "WEB_BROWSER"
}
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Device verified and trusted",
"action_time": "2025-01-11T16:02:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"onboardingComplete": true
},
"device": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:02:00"
}
}
}
POST-ONBOARDING: Set Password (Optional)
POST /api/v1/auth/password/set
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"newPassword": "MySecurePass123!",
"confirmPassword": "MySecurePass123!"
}
Response (Success):
{
"success": true,
"message": "Password set successfully. You can now use password login.",
"data": {
"hasPassword": true,
"passwordStrength": {
"score": 85,
"level": "STRONG",
"feedback": "Great password!"
}
},
"timestamp": "2025-01-11T16:05:00Z"
}
Response (Weak Password):
{
"success": false,
"message": "Password is too weak",
"error": {
"code": "WEAK_PASSWORD",
"passwordStrength": {
"score": 40,
"level": "WEAK",
"feedback": "Add uppercase letters and special characters"
},
"requirements": [
"At least 8 characters",
"At least one uppercase letter",
"At least one lowercase letter",
"At least one number",
"At least one special character (@$!%*?&#)"
]
},
"timestamp": "2025-01-11T16:05:00Z"
}
TOKEN MANAGEMENT
Refresh Token
POST /api/v1/auth/token/refresh
Request:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
Response (Success):
{
"success": true,
"message": "Token refreshed successfully",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600
},
"timestamp": "2025-01-11T17:00:00Z"
}
Response (Invalid/Expired Refresh Token):
{
"success": false,
"message": "Session expired. Please login again.",
"error": {
"code": "REFRESH_TOKEN_EXPIRED"
},
"timestamp": "2025-01-11T17:00:00Z"
}
Logout
POST /api/v1/auth/logout
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"logoutAllDevices": false
}
Response:
{
"success": true,
"message": "Logged out successfully",
"data": null,
"timestamp": "2025-01-11T18:00:00Z"
}
PASSWORD RESET
Request Password Reset
POST /api/v1/auth/password/reset/request
Request:
{
"identifier": "alex@example.com"
}
Response:
{
"success": true,
"message": "Password reset OTP sent to your email",
"data": {
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"maskedIdentifier": "a***@example.com",
"expiresAt": "2025-01-11T16:10:00Z"
},
"timestamp": "2025-01-11T16:00:00Z"
}
Verify Reset OTP & Set New Password
POST /api/v1/auth/password/reset/confirm
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456",
"newPassword": "MyNewSecurePass123!",
"confirmPassword": "MyNewSecurePass123!"
}
Response:
{
"success": true,
"message": "Password reset successfully. Please login with your new password.",
"data": null,
"timestamp": "2025-01-11T16:02:00Z"
}
GET CURRENT USER (Check Auth Status)
GET /api/v1/auth/me
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
{
"success": true,
"httpStatus": "OK",
"message": "User retrieved successfully",
"action_time": "2025-01-11T18:00:00",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"firstName": "Alex",
"lastName": "Johnson",
"email": "alex@example.com",
"phoneNumber": "+255712345678",
"profilePictureUrl": "https://storage.example.com/...",
"bio": "Fashion lover | Shopaholic | Event creator 🔥",
"birthDate": "1995-06-15",
"isEmailVerified": true,
"isPhoneVerified": true,
"hasPassword": true,
"authProviders": ["PHONE", "GOOGLE"],
"onboardingComplete": true,
"onboardingStep": "COMPLETE",
"interests": [
{ "id": "cat_001", "name": "Fashion" },
{ "id": "cat_003", "name": "Beauty & Cosmetics" }
],
"createdAt": "2025-01-11T15:20:00",
"updatedAt": "2025-01-11T15:27:00"
}
}
ONBOARDING STATUS CHECK
GET /api/v1/onboarding/status
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response (Incomplete):
{
"success": true,
"message": "Onboarding in progress",
"data": {
"onboardingComplete": false,
"currentStep": "PROFILE_SETUP",
"completedSteps": ["SIGNUP", "NAME_BIRTHDATE"],
"remainingSteps": ["PROFILE_SETUP", "INTERESTS"],
"progress": 50
},
"timestamp": "2025-01-11T15:23:00Z"
}
Response (Complete):
{
"success": true,
"message": "Onboarding complete",
"data": {
"onboardingComplete": true,
"currentStep": "COMPLETE",
"completedSteps": ["SIGNUP", "NAME_BIRTHDATE", "PROFILE_SETUP", "INTERESTS"],
"remainingSteps": [],
"progress": 100
},
"timestamp": "2025-01-11T15:27:00Z"
}
ERROR RESPONSE FORMAT (Standard)
All error responses follow GlobeFailureResponseBuilder structure:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Human readable error message",
"action_time": "2025-01-11T15:20:00",
"data": {
"code": "ERROR_CODE",
"field": "fieldName",
"details": "Additional details if any",
"suggestion": "What user can do"
}
}
Common Error Codes
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
BAD_REQUEST | Request validation failed |
INVALID_OTP |
BAD_REQUEST | OTP code is incorrect |
OTP_EXPIRED |
BAD_REQUEST | OTP has expired |
INVALID_CREDENTIALS |
UNAUTHORIZED | Wrong password |
TOKEN_EXPIRED |
UNAUTHORIZED | Access token expired |
REFRESH_TOKEN_EXPIRED |
UNAUTHORIZED | Refresh token expired |
UNAUTHORIZED |
UNAUTHORIZED | Not authenticated |
FORBIDDEN |
FORBIDDEN | Not allowed |
USER_NOT_FOUND |
NOT_FOUND | User doesn't exist |
ACCOUNT_EXISTS |
CONFLICT | Account already exists |
USERNAME_TAKEN |
CONFLICT | Username is taken |
PHONE_ALREADY_REGISTERED |
CONFLICT | Phone belongs to another account |
PHONE_TEMPORARILY_RESERVED |
CONFLICT | Phone claimed but unverified |
ACCOUNT_LOCKED |
LOCKED | Too many failed attempts |
RESEND_COOLDOWN |
TOO_MANY_REQUESTS | Wait before resending OTP |
RATE_LIMITED |
TOO_MANY_REQUESTS | Too many requests |
WEAK_PASSWORD |
UNPROCESSABLE_ENTITY | Password doesn't meet requirements |
UNDERAGE |
UNPROCESSABLE_ENTITY | User is under minimum age |
MIN_INTERESTS_REQUIRED |
UNPROCESSABLE_ENTITY | Must have minimum interests |
MAX_INTERESTS_REACHED |
UNPROCESSABLE_ENTITY | Maximum interests reached |
SERVER_ERROR |
INTERNAL_SERVER_ERROR | Internal server error |
ENUMS REFERENCE
OnboardingStep
SIGNUP
NAME_BIRTHDATE
PROFILE_SETUP
INTERESTS
COMPLETE
AuthProvider
PHONE
EMAIL
GOOGLE
APPLE
OTP Purpose
SIGNUP_VERIFICATION
LOGIN_OTP
PASSWORD_RESET
EMAIL_VERIFICATION
PHONE_VERIFICATION
SUMMARY: What's New vs Existing
New Endpoints
POST /api/v1/auth/signup/initiate(replaces/register)POST /api/v1/auth/signup/verify(replaces/verify-otp)POST /api/v1/auth/signup/googlePOST /api/v1/auth/signup/applePUT /api/v1/onboarding/name-birthdateGET /api/v1/onboarding/username/checkPOST /api/v1/onboarding/profile-picturePUT /api/v1/onboarding/profile-setupGET /api/v1/onboarding/interests/categoriesPOST /api/v1/onboarding/interestsPOST /api/v1/onboarding/interests/skipGET /api/v1/onboarding/completePOST /api/v1/auth/login/passwordPOST /api/v1/auth/login/otp/requestPOST /api/v1/auth/login/otp/verifyPOST /api/v1/auth/password/setGET /api/v1/auth/meGET /api/v1/onboarding/status
Modified Endpoints
POST /api/v1/auth/otp/resend(updated response)POST /api/v1/auth/token/refresh(same, just standardized response)POST /api/v1/auth/password/reset/request(updated from/psw-reset-otp)POST /api/v1/auth/password/reset/confirm(updated from/reset-password)
Deprecated/Removed
POST /api/v1/auth/register(replaced by/signup/initiate)POST /api/v1/auth/login(replaced by password/OTP specific endpoints)
INTEREST SYSTEM
The interest system tracks user preferences through two methods:
- Explicit: User picks during onboarding (visible to user)
- Implicit: Silent tracking based on user behavior (invisible to user)
Scores update silently in the background as users interact with content.
Interest Scoring Logic
| Action | Score | Notes |
|---|---|---|
| Explicitly selected (onboarding) | +100 | Base score for picked interests |
| View post | +1 | Quick glance |
| Like post | +3 | Shows interest |
| Comment on post | +5 | Higher engagement |
| Share post | +7 | Strong signal |
| Save/Bookmark | +5 | Wants to revisit |
| Follow user in category | +10 | Committed interest |
| View product | +2 | Shopping interest |
| Add to cart | +8 | Purchase intent |
| Purchase product | +15 | Strongest signal |
| View event | +2 | Curious |
| Attend event | +12 | Committed |
| Search term | +4 | Active seeking |
| Time spent (per 30sec) | +1 | Passive engagement |
| Scroll past quickly | -1 | Not interested |
| Hide/Not interested | -20 | Explicit dislike |
| Report content | -30 | Strong negative |
Score Decay: 10% reduction per week of no interaction (keeps recommendations fresh)
Max Score: 1000 per category
ADMIN: Interest Category Management
Create Category
POST /api/v1/admin/interests/categories
Headers:
Authorization: Bearer {admin_token}
Request:
{
"name": "Fashion",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion.png",
"color": "#FF6B6B",
"keywords": ["clothes", "style", "outfit", "wear", "dress"],
"displayOrder": 1,
"isActive": true
}
Response:
{
"success": true,
"message": "Category created successfully",
"data": {
"id": "cat_001",
"name": "Fashion",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion.png",
"color": "#FF6B6B",
"keywords": ["clothes", "style", "outfit", "wear", "dress"],
"displayOrder": 1,
"isActive": true,
"createdAt": "2025-01-11T16:00:00Z",
"updatedAt": "2025-01-11T16:00:00Z"
},
"timestamp": "2025-01-11T16:00:00Z"
}
List All Categories (Admin View)
GET /api/v1/admin/interests/categories?includeInactive=true
Response:
{
"success": true,
"message": "Categories retrieved successfully",
"data": {
"categories": [
{
"id": "cat_001",
"name": "Fashion",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion.png",
"color": "#FF6B6B",
"keywords": ["clothes", "style", "outfit"],
"displayOrder": 1,
"isActive": true,
"stats": {
"usersExplicit": 15420,
"usersImplicit": 28750,
"totalEngagements": 89500,
"postsTagged": 8920,
"productsTagged": 3450,
"shopsTagged": 234,
"eventsTagged": 89
},
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-10T14:30:00Z"
},
{
"id": "cat_002",
"name": "Electronics",
"icon": "📱",
"iconUrl": "https://cdn.example.com/icons/electronics.png",
"color": "#4ECDC4",
"keywords": ["phone", "laptop", "gadget", "tech"],
"displayOrder": 2,
"isActive": true,
"stats": {
"usersExplicit": 12300,
"usersImplicit": 31000,
"totalEngagements": 125000,
"postsTagged": 5600,
"productsTagged": 8900,
"shopsTagged": 456,
"eventsTagged": 23
},
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-10T14:30:00Z"
}
],
"totalCategories": 20,
"activeCategories": 18,
"inactiveCategories": 2
},
"timestamp": "2025-01-11T16:00:00Z"
}
Update Category
PUT /api/v1/admin/interests/categories/{categoryId}
Request:
{
"name": "Fashion & Style",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion-v2.png",
"color": "#FF5252",
"keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"],
"isActive": true
}
Response:
{
"success": true,
"message": "Category updated successfully",
"data": {
"id": "cat_001",
"name": "Fashion & Style",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion-v2.png",
"color": "#FF5252",
"keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"],
"displayOrder": 1,
"isActive": true,
"updatedAt": "2025-01-11T16:05:00Z"
},
"timestamp": "2025-01-11T16:05:00Z"
}
Reorder Categories
PUT /api/v1/admin/interests/categories/reorder
Request:
{
"order": [
{ "categoryId": "cat_003", "displayOrder": 1 },
{ "categoryId": "cat_001", "displayOrder": 2 },
{ "categoryId": "cat_002", "displayOrder": 3 }
]
}
Response:
{
"success": true,
"message": "Categories reordered successfully",
"data": {
"updated": 3
},
"timestamp": "2025-01-11T16:10:00Z"
}
Deactivate Category (Soft Delete)
DELETE /api/v1/admin/interests/categories/{categoryId}
Response:
{
"success": true,
"message": "Category deactivated successfully",
"data": {
"id": "cat_015",
"name": "Outdated Category",
"isActive": false,
"deactivatedAt": "2025-01-11T16:15:00Z",
"note": "Category hidden from users. Existing user data preserved."
},
"timestamp": "2025-01-11T16:15:00Z"
}
Get Category Analytics
GET /api/v1/admin/interests/categories/{categoryId}/analytics?period=30d
Response:
{
"success": true,
"message": "Analytics retrieved successfully",
"data": {
"categoryId": "cat_001",
"categoryName": "Fashion",
"period": "LAST_30_DAYS",
"overview": {
"totalUsers": 44170,
"explicitUsers": 15420,
"implicitUsers": 28750,
"averageScore": 67.5,
"totalEngagements": 89500
},
"trend": {
"direction": "UP",
"percentageChange": 12.5,
"newUsersThisPeriod": 2340
},
"engagement": {
"likes": 34000,
"comments": 12500,
"shares": 8900,
"saves": 15600,
"purchases": 3200
},
"topContent": {
"topPosts": [
{ "postId": "post_123", "engagements": 4500 },
{ "postId": "post_456", "engagements": 3200 }
],
"topProducts": [
{ "productId": "prod_789", "sales": 234 },
{ "productId": "prod_012", "sales": 189 }
]
},
"demographics": {
"ageGroups": [
{ "range": "13-17", "percentage": 15 },
{ "range": "18-24", "percentage": 35 },
{ "range": "25-34", "percentage": 30 },
{ "range": "35-44", "percentage": 12 },
{ "range": "45+", "percentage": 8 }
]
}
},
"timestamp": "2025-01-11T16:20:00Z"
}
PUBLIC: Interest Categories
Get Active Categories (Onboarding & Settings)
GET /api/v1/interests/categories
No auth required for onboarding, shows only active categories
Response:
{
"success": true,
"message": "Categories retrieved successfully",
"data": {
"categories": [
{
"id": "cat_001",
"name": "Fashion",
"icon": "👗",
"iconUrl": "https://cdn.example.com/icons/fashion.png",
"color": "#FF6B6B"
},
{
"id": "cat_002",
"name": "Electronics",
"icon": "📱",
"iconUrl": "https://cdn.example.com/icons/electronics.png",
"color": "#4ECDC4"
},
{
"id": "cat_003",
"name": "Beauty & Cosmetics",
"icon": "💄",
"iconUrl": "https://cdn.example.com/icons/beauty.png",
"color": "#FF69B4"
},
{
"id": "cat_004",
"name": "Food & Drinks",
"icon": "🍔",
"iconUrl": "https://cdn.example.com/icons/food.png",
"color": "#F39C12"
},
{
"id": "cat_005",
"name": "Sports & Fitness",
"icon": "⚽",
"iconUrl": "https://cdn.example.com/icons/sports.png",
"color": "#2ECC71"
},
{
"id": "cat_006",
"name": "Music & Dance",
"icon": "🎵",
"iconUrl": "https://cdn.example.com/icons/music.png",
"color": "#9B59B6"
},
{
"id": "cat_007",
"name": "Home & Decor",
"icon": "🏠",
"iconUrl": "https://cdn.example.com/icons/home.png",
"color": "#E67E22"
},
{
"id": "cat_008",
"name": "Tech & Gadgets",
"icon": "💻",
"iconUrl": "https://cdn.example.com/icons/tech.png",
"color": "#3498DB"
},
{
"id": "cat_009",
"name": "Travel",
"icon": "✈️",
"iconUrl": "https://cdn.example.com/icons/travel.png",
"color": "#1ABC9C"
},
{
"id": "cat_010",
"name": "Gaming",
"icon": "🎮",
"iconUrl": "https://cdn.example.com/icons/gaming.png",
"color": "#8E44AD"
},
{
"id": "cat_011",
"name": "Books & Reading",
"icon": "📚",
"iconUrl": "https://cdn.example.com/icons/books.png",
"color": "#D35400"
},
{
"id": "cat_012",
"name": "Art & Design",
"icon": "🎨",
"iconUrl": "https://cdn.example.com/icons/art.png",
"color": "#E74C3C"
},
{
"id": "cat_013",
"name": "Health & Wellness",
"icon": "🧘",
"iconUrl": "https://cdn.example.com/icons/wellness.png",
"color": "#27AE60"
},
{
"id": "cat_014",
"name": "Automotive",
"icon": "🚗",
"iconUrl": "https://cdn.example.com/icons/auto.png",
"color": "#34495E"
},
{
"id": "cat_015",
"name": "Pets & Animals",
"icon": "🐾",
"iconUrl": "https://cdn.example.com/icons/pets.png",
"color": "#F1C40F"
},
{
"id": "cat_016",
"name": "Photography",
"icon": "📷",
"iconUrl": "https://cdn.example.com/icons/photo.png",
"color": "#7F8C8D"
},
{
"id": "cat_017",
"name": "Kids & Baby",
"icon": "👶",
"iconUrl": "https://cdn.example.com/icons/kids.png",
"color": "#FFB6C1"
},
{
"id": "cat_018",
"name": "Business & Finance",
"icon": "💼",
"iconUrl": "https://cdn.example.com/icons/business.png",
"color": "#2C3E50"
},
{
"id": "cat_019",
"name": "Entertainment",
"icon": "🎬",
"iconUrl": "https://cdn.example.com/icons/entertainment.png",
"color": "#C0392B"
},
{
"id": "cat_020",
"name": "DIY & Crafts",
"icon": "🛠️",
"iconUrl": "https://cdn.example.com/icons/diy.png",
"color": "#16A085"
}
],
"selectionRules": {
"minimum": 3,
"recommended": 5,
"maximum": 15,
"canSkip": true
}
},
"timestamp": "2025-01-11T15:25:00Z"
}
USER: Interest Management
Get My Interests
GET /api/v1/interests/me
Headers:
Authorization: Bearer {access_token}
Response:
{
"success": true,
"message": "Interests retrieved successfully",
"data": {
"explicit": [
{
"categoryId": "cat_001",
"categoryName": "Fashion",
"icon": "👗",
"color": "#FF6B6B",
"source": "ONBOARDING",
"addedAt": "2025-01-11T15:26:00Z"
},
{
"categoryId": "cat_003",
"categoryName": "Beauty & Cosmetics",
"icon": "💄",
"color": "#FF69B4",
"source": "SETTINGS",
"addedAt": "2025-01-12T10:00:00Z"
}
],
"topImplicit": [
{
"categoryId": "cat_002",
"categoryName": "Electronics",
"icon": "📱",
"color": "#4ECDC4",
"score": 87,
"trend": "RISING"
},
{
"categoryId": "cat_006",
"categoryName": "Music & Dance",
"icon": "🎵",
"color": "#9B59B6",
"score": 65,
"trend": "STABLE"
},
{
"categoryId": "cat_009",
"categoryName": "Travel",
"icon": "✈️",
"color": "#1ABC9C",
"score": 42,
"trend": "FALLING"
}
],
"summary": {
"explicitCount": 2,
"implicitCount": 8,
"topCategory": "Fashion",
"feedPersonalization": "HIGH"
}
},
"timestamp": "2025-01-11T18:00:00Z"
}
Update My Explicit Interests
PUT /api/v1/interests/me
Headers:
Authorization: Bearer {access_token}
Request:
{
"categoryIds": ["cat_001", "cat_003", "cat_006", "cat_010", "cat_012"]
}
Response:
{
"success": true,
"message": "Interests updated successfully",
"data": {
"explicit": [
{ "categoryId": "cat_001", "categoryName": "Fashion" },
{ "categoryId": "cat_003", "categoryName": "Beauty & Cosmetics" },
{ "categoryId": "cat_006", "categoryName": "Music & Dance" },
{ "categoryId": "cat_010", "categoryName": "Gaming" },
{ "categoryId": "cat_012", "categoryName": "Art & Design" }
],
"changes": {
"added": ["cat_006", "cat_010", "cat_012"],
"removed": [],
"unchanged": ["cat_001", "cat_003"]
},
"feedImpact": "Your feed will now show more Music, Gaming, and Art content"
},
"timestamp": "2025-01-11T18:05:00Z"
}
Add Single Interest
POST /api/v1/interests/me/{categoryId}
Headers:
Authorization: Bearer {access_token}
Response:
{
"success": true,
"message": "Interest added successfully",
"data": {
"categoryId": "cat_015",
"categoryName": "Pets & Animals",
"icon": "🐾",
"addedAt": "2025-01-11T18:10:00Z",
"totalExplicit": 6
},
"timestamp": "2025-01-11T18:10:00Z"
}
Response (Max Reached):
{
"success": false,
"message": "Maximum interests reached",
"error": {
"code": "MAX_INTERESTS_REACHED",
"current": 15,
"maximum": 15,
"suggestion": "Remove an interest before adding a new one"
},
"timestamp": "2025-01-11T18:10:00Z"
}
Remove Single Interest
DELETE /api/v1/interests/me/{categoryId}
Headers:
Authorization: Bearer {access_token}
Response:
{
"success": true,
"message": "Interest removed successfully",
"data": {
"categoryId": "cat_015",
"categoryName": "Pets & Animals",
"removedAt": "2025-01-11T18:15:00Z",
"totalExplicit": 5,
"note": "You may still see some Pets & Animals content based on your activity"
},
"timestamp": "2025-01-11T18:15:00Z"
}
Response (Minimum Required):
{
"success": false,
"message": "Cannot remove interest",
"error": {
"code": "MIN_INTERESTS_REQUIRED",
"current": 3,
"minimum": 3,
"suggestion": "Add another interest before removing this one"
},
"timestamp": "2025-01-11T18:15:00Z"
}
Hide Content Category (Negative Signal)
POST /api/v1/interests/me/{categoryId}/hide
User explicitly says "not interested" - strong negative signal
Headers:
Authorization: Bearer {access_token}
Response:
{
"success": true,
"message": "You'll see less of this content",
"data": {
"categoryId": "cat_018",
"categoryName": "Business & Finance",
"action": "HIDDEN",
"feedImpact": "Business & Finance content will be significantly reduced in your feed",
"canUndo": true,
"undoExpiry": "2025-01-11T18:30:00Z"
},
"timestamp": "2025-01-11T18:20:00Z"
}
Unhide Content Category
DELETE /api/v1/interests/me/{categoryId}/hide
Response:
{
"success": true,
"message": "Category unhidden",
"data": {
"categoryId": "cat_018",
"categoryName": "Business & Finance",
"action": "UNHIDDEN",
"feedImpact": "Business & Finance content will appear normally based on your activity"
},
"timestamp": "2025-01-11T18:25:00Z"
}
Get Hidden Categories
GET /api/v1/interests/me/hidden
Response:
{
"success": true,
"message": "Hidden categories retrieved",
"data": {
"hidden": [
{
"categoryId": "cat_018",
"categoryName": "Business & Finance",
"icon": "💼",
"hiddenAt": "2025-01-11T18:20:00Z"
},
{
"categoryId": "cat_014",
"categoryName": "Automotive",
"icon": "🚗",
"hiddenAt": "2025-01-05T12:00:00Z"
}
],
"totalHidden": 2
},
"timestamp": "2025-01-11T18:30:00Z"
}
INTERNAL: Silent Interest Tracking
These are NOT public API endpoints. They are triggered internally when users interact with content.
How It Works
When user performs any action on content (post, product, shop, event), the system:
- Gets the category tags from that content
- Calculates score based on action type
- Updates user's implicit interest scores silently
- No API call from client needed
Actions That Trigger Tracking
| User Action | System Tracks |
|---|---|
| Views post | POST_VIEW on post's categories |
| Likes post | POST_LIKE on post's categories |
| Comments on post | POST_COMMENT on post's categories |
| Shares post | POST_SHARE on post's categories |
| Saves post | POST_SAVE on post's categories |
| Views product | PRODUCT_VIEW on product's category |
| Adds to cart | PRODUCT_CART on product's category |
| Purchases | PRODUCT_PURCHASE on product's category |
| Views shop | SHOP_VIEW on shop's categories |
| Follows shop | SHOP_FOLLOW on shop's categories |
| Views event | EVENT_VIEW on event's categories |
| RSVPs to event | EVENT_RSVP on event's categories |
| Follows user | USER_FOLLOW on user's primary categories |
| Searches | SEARCH on matched categories |
| Scrolls past quickly | SCROLL_PAST (negative) |
| Clicks "Not interested" | HIDE_CONTENT (strong negative) |
Content Must Have Category Tags
For tracking to work, all content must be tagged:
// Post
{
"id": "post_123",
"content": "Check out my new outfit!",
"categoryIds": ["cat_001"] // Fashion
}
// Product
{
"id": "prod_456",
"name": "Wireless Earbuds",
"categoryId": "cat_002" // Electronics
}
// Shop
{
"id": "shop_789",
"name": "StyleHub",
"categoryIds": ["cat_001", "cat_003"] // Fashion, Beauty
}
// Event
{
"id": "event_012",
"title": "Summer Music Festival",
"categoryIds": ["cat_006", "cat_019"] // Music, Entertainment
}
FEED: Using Interests for Recommendations
Get Personalized Feed
GET /api/v1/feed?page=1&size=20
Feed algorithm uses interest scores to rank content
Response includes personalization info:
{
"success": true,
"data": {
"posts": [
{
"id": "post_123",
"content": "...",
"author": { "...": "..." },
"categoryIds": ["cat_001"],
"relevanceScore": 0.95,
"relevanceReason": "EXPLICIT_INTEREST"
},
{
"id": "post_456",
"content": "...",
"author": { "...": "..." },
"categoryIds": ["cat_002"],
"relevanceScore": 0.87,
"relevanceReason": "HIGH_IMPLICIT_SCORE"
},
{
"id": "post_789",
"content": "...",
"author": { "...": "..." },
"categoryIds": ["cat_015"],
"relevanceScore": 0.45,
"relevanceReason": "TRENDING"
}
],
"personalization": {
"status": "ACTIVE",
"basedOn": {
"explicitInterests": 5,
"implicitInterests": 8,
"followedAccounts": 23
}
},
"pagination": { "...": "..." }
}
}
SUMMARY: Interest System Endpoints
Admin Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/admin/interests/categories |
Create category |
| GET | /api/v1/admin/interests/categories |
List all (with stats) |
| PUT | /api/v1/admin/interests/categories/{id} |
Update category |
| DELETE | /api/v1/admin/interests/categories/{id} |
Deactivate category |
| PUT | /api/v1/admin/interests/categories/reorder |
Reorder categories |
| GET | /api/v1/admin/interests/categories/{id}/analytics |
Get analytics |
Public Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/interests/categories |
Get active categories |
User Endpoints (Requires Auth)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/interests/me |
Get my interests |
| PUT | /api/v1/interests/me |
Update all explicit |
| POST | /api/v1/interests/me/{id} |
Add single interest |
| DELETE | /api/v1/interests/me/{id} |
Remove single interest |
| POST | /api/v1/interests/me/{id}/hide |
Hide category |
| DELETE | /api/v1/interests/me/{id}/hide |
Unhide category |
| GET | /api/v1/interests/me/hidden |
Get hidden list |
Internal (No Public API)
- Silent tracking triggered by user actions
- Score calculation and decay
- Feed personalization
New Database Fields Needed
AccountEntity
// Existing fields to keep
private UUID id;
private String userName; // Public @handle (changeable)
private String phoneNumber;
private String email;
private String password;
private String firstName;
private String lastName;
private String middleName;
private String bio;
private String location;
private Boolean isVerified;
private Boolean isEmailVerified;
private Boolean isPhoneVerified;
private boolean twoFactorEnabled;
private String twoFactorSecret;
private boolean locked;
private String lockedReason;
private LocalDateTime createdAt;
private LocalDateTime editedAt;
private Set<Roles> roles;
private List<String> profilePictureUrls;
private boolean isBucketCreated;
// NEW fields to add
private String systemUsername; // Internal identifier (never changes) - "usr_550e8400e29b41d4"
private LocalDate birthDate; // For age gating
private String displayName; // Full display name "Alex Johnson"
private String authProvider; // PHONE, EMAIL, GOOGLE, APPLE (primary signup method)
private String googleId; // Google OAuth ID
private String appleId; // Apple OAuth ID
private String onboardingStep; // SIGNUP, NAME_BIRTHDATE, PROFILE_SETUP, INTERESTS, COMPLETE
private Boolean onboardingComplete; // Quick check flag
private Boolean hasPassword; // Whether user set a password
private LocalDateTime phoneClaimedAt; // When phone was first set (for claim expiry)
private LocalDateTime phoneVerifiedAt;// When phone was verified
private LocalDateTime emailClaimedAt; // When email was first set
private LocalDateTime emailVerifiedAt;// When email was verified
New Entities
InterestCategory (Admin managed)
- id (UUID)
- name (String)
- icon (String) - emoji
- iconUrl (String) - image URL
- color (String) - hex color
- keywords (List<String>) - for auto-tagging
- displayOrder (Integer)
- isActive (Boolean)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)
UserInterest (User's interests)
- id (UUID)
- userId (UUID)
- categoryId (UUID)
- source (Enum: ONBOARDING, SETTINGS, IMPLICIT)
- score (Integer) - 0 to 1000
- isExplicit (Boolean)
- isHidden (Boolean)
- lastInteractionAt (LocalDateTime)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)
InterestEvent (Tracking log - optional, for analytics)
- id (UUID)
- userId (UUID)
- categoryId (UUID)
- actionType (Enum)
- weight (Integer)
- sourceType (Enum: POST, PRODUCT, SHOP, EVENT, USER, SEARCH)
- sourceId (UUID)
- createdAt (LocalDateTime)
JWT Token Structure
Access Token Payload:
{
"sub": "usr_550e8400e29b41d4",
"tokenType": "ACCESS",
"iat": 1736605200,
"exp": 1736608800
}
Refresh Token Payload:
{
"sub": "usr_550e8400e29b41d4",
"tokenType": "REFRESH",
"iat": 1736605200,
"exp": 1768141200
}
Note: sub (subject) uses systemUsername, NOT userName. This allows username changes without token invalidation.
Username Change Flow (No Logout Required)
PUT /api/v1/profile/update-basic-info
Request:
{
"userName": "newusername"
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Username updated successfully",
"action_time": "2025-01-11T16:00:00",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "newusername",
"previousUserName": "oldusername",
"note": "Your profile URL is now: app.com/@newusername"
}
}
JWT token remains valid because it uses systemUsername which hasn't changed.
System Username Generation
Generated automatically at account creation:
public String generateSystemUsername(UUID userId) {
// Take first 16 chars of UUID (without hyphens)
String shortId = userId.toString().replace("-", "").substring(0, 16);
return "usr_" + shortId;
}
// Example:
// UUID: 550e8400-e29b-41d4-a716-446655440000
// systemUsername: usr_550e8400e29b41d4
Rules:
- Prefix:
usr_ - Length: 20 characters total
- Characters: lowercase alphanumeric only
- Unique: derived from UUID
- Never displayed to users
- Never changeable
DEVICE TRUST & LOGIN SECURITY
Overview
The system tracks user devices and applies a sliding trust window:
- New device → OTP required before password login
- Inactive device (30+ days) → OTP required (re-verification)
- Trusted active device → Password login allowed
- Suspicious activity → OTP required
Trust Sliding Window
Device Activity Timeline:
─────────────────────────────────────────────────────────────────►
NOW
│ │ │
▼ ▼ ▼
Day 0 Day 25 Day 35
(Login) (Last use) (Login attempt)
│ │ │
│◄────── TRUSTED ────►│ │
│ │◄─── 30 DAY GAP ───►│
│ │
│ Device now UNTRUSTED
│ OTP required to re-trust
Login Decision Matrix
| Device Status | Last Activity | Password Set? | Action |
|---|---|---|---|
| New (unknown) | Never | Yes | OTP first → then password → trust device |
| New (unknown) | Never | No | OTP only → trust device |
| Known trusted | < 30 days | Yes | Password only ✅ |
| Known trusted | < 30 days | No | Auto-login or OTP |
| Known trusted | > 30 days | Yes | OTP first → then password → re-trust |
| Known trusted | > 30 days | No | OTP → re-trust |
| Known untrusted | Any | Any | OTP required |
| Any (suspicious) | Any | Any | OTP required + security alert |
Suspicious Activity Triggers
- 3+ failed password attempts
- Login from new country/region
- Multiple devices logging in simultaneously
- Unusual login time (if pattern established)
- IP address on blocklist
LOGIN FLOWS (Updated with Device Trust)
Password Login - Full Flow
POST /api/v1/auth/login/password
Request:
{
"identifier": "alexvibes",
"password": "MySecurePass123!",
"deviceInfo": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"appVersion": "1.2.0"
}
}
Response (Success - Trusted Device):
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2025-01-11T16:00:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"requiresOtp": false,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"onboardingComplete": true
},
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:00:00"
}
}
}
Response (New/Untrusted Device - OTP Required):
{
"success": true,
"httpStatus": "OK",
"message": "New device detected. Please verify with OTP.",
"action_time": "2025-01-11T16:00:00",
"data": {
"requiresOtp": true,
"otpReason": "NEW_DEVICE",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpSentTo": "+255*****678",
"otpMethod": "SMS",
"expiresAt": "2025-01-11T16:10:00",
"device": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"isNew": true
},
"securityNote": "We detected a login from a new device. For your security, please verify with OTP."
}
}
Response (Inactive Device - OTP Required):
{
"success": true,
"httpStatus": "OK",
"message": "Device inactive for 30+ days. Please verify with OTP.",
"action_time": "2025-01-11T16:00:00",
"data": {
"requiresOtp": true,
"otpReason": "INACTIVE_DEVICE",
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpSentTo": "a***@example.com",
"otpMethod": "EMAIL",
"expiresAt": "2025-01-11T16:10:00",
"device": {
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"lastActiveAt": "2024-12-01T10:00:00",
"inactiveDays": 41
},
"securityNote": "This device hasn't been used in 41 days. Please verify it's you."
}
}
Verify Device OTP (Step 2 for untrusted devices)
POST /api/v1/auth/login/verify-device
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456",
"trustDevice": true,
"deviceInfo": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"deviceType": "WEB_BROWSER"
}
}
Response (Success):
{
"success": true,
"httpStatus": "OK",
"message": "Device verified and trusted",
"action_time": "2025-01-11T16:02:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"systemUsername": "usr_550e8400e29b41d4",
"userName": "alexvibes",
"displayName": "Alex Johnson",
"profilePictureUrl": "https://storage.example.com/...",
"onboardingComplete": true
},
"device": {
"deviceId": "xyz789-new-device",
"deviceName": "Chrome on Windows",
"trusted": true,
"trustExpiresAt": "2025-02-10T16:02:00"
}
}
}
Login Without Trusting Device (One-time access)
Request:
{
"tempToken": "eyJhbGciOiJIUzI1NiIs...",
"otpCode": "123456",
"trustDevice": false
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Login successful (device not saved)",
"action_time": "2025-01-11T16:02:00",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": { ... },
"device": {
"trusted": false,
"note": "This device will require OTP on next login"
}
}
}
DEVICE MANAGEMENT
Get My Devices
GET /api/v1/auth/devices
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Devices retrieved successfully",
"action_time": "2025-01-11T18:00:00",
"data": {
"devices": [
{
"id": "device-uuid-1",
"deviceId": "abc123-device-fingerprint",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"isTrusted": true,
"isCurrentDevice": true,
"lastActiveAt": "2025-01-11T18:00:00",
"lastIpAddress": "196.41.xxx.xxx",
"lastLocation": "Dar es Salaam, Tanzania",
"createdAt": "2025-01-01T10:00:00"
},
{
"id": "device-uuid-2",
"deviceId": "xyz789-device-fingerprint",
"deviceName": "Chrome on MacBook",
"deviceType": "WEB_BROWSER",
"isTrusted": true,
"isCurrentDevice": false,
"lastActiveAt": "2025-01-10T14:30:00",
"lastIpAddress": "196.41.xxx.xxx",
"lastLocation": "Dar es Salaam, Tanzania",
"createdAt": "2024-12-15T08:00:00"
},
{
"id": "device-uuid-3",
"deviceId": "old-device-fingerprint",
"deviceName": "Samsung Galaxy S21",
"deviceType": "MOBILE_ANDROID",
"isTrusted": false,
"isCurrentDevice": false,
"lastActiveAt": "2024-11-20T09:00:00",
"lastIpAddress": "41.59.xxx.xxx",
"lastLocation": "Arusha, Tanzania",
"createdAt": "2024-06-01T12:00:00",
"untrustedReason": "Inactive for 52 days"
}
],
"totalDevices": 3,
"trustedDevices": 2
}
}
Remove/Logout Device
DELETE /api/v1/auth/devices/{deviceId}
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Device removed successfully",
"action_time": "2025-01-11T18:05:00",
"data": {
"removedDeviceId": "device-uuid-3",
"removedDeviceName": "Samsung Galaxy S21",
"note": "This device has been logged out and will require OTP to login again"
}
}
Logout All Other Devices
POST /api/v1/auth/devices/logout-all
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request:
{
"password": "MySecurePass123!",
"keepCurrentDevice": true
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "All other devices logged out",
"action_time": "2025-01-11T18:10:00",
"data": {
"devicesLoggedOut": 2,
"currentDeviceKept": true,
"note": "All other devices will require OTP to login again"
}
}
Rename Device
PUT /api/v1/auth/devices/{deviceId}
Request:
{
"deviceName": "My Work Laptop"
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Device renamed successfully",
"action_time": "2025-01-11T18:15:00",
"data": {
"deviceId": "device-uuid-2",
"deviceName": "My Work Laptop",
"previousName": "Chrome on MacBook"
}
}
LOGIN EVENTS/HISTORY
Get Login History
GET /api/v1/auth/login-history?page=1&size=20
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Login history retrieved",
"action_time": "2025-01-11T18:20:00",
"data": {
"events": [
{
"id": "event-uuid-1",
"loginMethod": "PASSWORD",
"deviceName": "iPhone 15 Pro",
"deviceType": "MOBILE_IOS",
"ipAddress": "196.41.xxx.xxx",
"location": "Dar es Salaam, Tanzania",
"status": "SUCCESS",
"requiresOtp": false,
"createdAt": "2025-01-11T16:00:00"
},
{
"id": "event-uuid-2",
"loginMethod": "OTP",
"deviceName": "Chrome on Windows",
"deviceType": "WEB_BROWSER",
"ipAddress": "196.41.xxx.xxx",
"location": "Dar es Salaam, Tanzania",
"status": "SUCCESS",
"requiresOtp": true,
"otpReason": "NEW_DEVICE",
"createdAt": "2025-01-11T14:30:00"
},
{
"id": "event-uuid-3",
"loginMethod": "PASSWORD",
"deviceName": "Unknown Device",
"deviceType": "WEB_BROWSER",
"ipAddress": "102.89.xxx.xxx",
"location": "Lagos, Nigeria",
"status": "FAILED_PASSWORD",
"requiresOtp": false,
"createdAt": "2025-01-10T23:45:00",
"securityAlert": true
}
],
"pagination": {
"page": 1,
"size": 20,
"totalElements": 45,
"totalPages": 3
},
"securitySummary": {
"totalLogins30Days": 28,
"failedAttempts30Days": 2,
"uniqueDevices30Days": 3,
"uniqueLocations30Days": 1
}
}
}
SECURITY ALERTS
Get Security Alerts
GET /api/v1/auth/security-alerts
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Security alerts retrieved",
"action_time": "2025-01-11T18:25:00",
"data": {
"alerts": [
{
"id": "alert-uuid-1",
"type": "SUSPICIOUS_LOGIN",
"severity": "HIGH",
"title": "Login attempt from new location",
"description": "Someone tried to login from Lagos, Nigeria",
"deviceName": "Unknown Device",
"ipAddress": "102.89.xxx.xxx",
"location": "Lagos, Nigeria",
"status": "UNREAD",
"actionTaken": "BLOCKED",
"createdAt": "2025-01-10T23:45:00",
"actions": [
{ "action": "DISMISS", "label": "This was me" },
{ "action": "SECURE_ACCOUNT", "label": "Secure my account" }
]
},
{
"id": "alert-uuid-2",
"type": "NEW_DEVICE_LOGIN",
"severity": "MEDIUM",
"title": "New device added",
"description": "Chrome on Windows was added to your account",
"deviceName": "Chrome on Windows",
"location": "Dar es Salaam, Tanzania",
"status": "READ",
"actionTaken": null,
"createdAt": "2025-01-11T14:30:00",
"actions": [
{ "action": "DISMISS", "label": "This was me" },
{ "action": "REMOVE_DEVICE", "label": "Remove this device" }
]
}
],
"unreadCount": 1,
"totalAlerts": 2
}
}
Dismiss Security Alert
POST /api/v1/auth/security-alerts/{alertId}/dismiss
Request:
{
"action": "DISMISS",
"feedback": "This was me logging in from a friend's device"
}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Alert dismissed",
"action_time": "2025-01-11T18:30:00",
"data": {
"alertId": "alert-uuid-1",
"status": "DISMISSED",
"unreadCount": 0
}
}
New Database Entities for Device Trust
UserDevice
- id (UUID)
- userId (UUID) FK → AccountEntity
- deviceId (String) - fingerprint from client (unique per user)
- deviceName (String) - "iPhone 15 Pro", "Chrome on Windows"
- deviceType (Enum) - MOBILE_IOS, MOBILE_ANDROID, WEB_BROWSER, DESKTOP_APP
- userAgent (String)
- lastIpAddress (String)
- lastLocation (String)
- isTrusted (Boolean)
- trustExpiresAt (LocalDateTime) - 30 days sliding window
- lastActiveAt (LocalDateTime)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)
Indexes:
- (userId, deviceId) UNIQUE
- (userId, isTrusted)
- (lastActiveAt)
LoginEvent
- id (UUID)
- userId (UUID) FK → AccountEntity
- deviceId (UUID) FK → UserDevice (nullable for failed attempts)
- loginMethod (Enum) - PASSWORD, OTP_SMS, OTP_EMAIL, GOOGLE, APPLE
- ipAddress (String)
- location (String)
- status (Enum) - SUCCESS, FAILED_PASSWORD, FAILED_OTP, BLOCKED, REQUIRES_OTP
- requiresOtp (Boolean)
- otpReason (Enum) - NEW_DEVICE, INACTIVE_DEVICE, SUSPICIOUS, MANUAL
- createdAt (LocalDateTime)
Indexes:
- (userId, createdAt)
- (userId, status)
- (ipAddress, createdAt) - for rate limiting
SecurityAlert
- id (UUID)
- userId (UUID) FK → AccountEntity
- loginEventId (UUID) FK → LoginEvent (nullable)
- type (Enum) - SUSPICIOUS_LOGIN, NEW_DEVICE_LOGIN, FAILED_ATTEMPTS, PASSWORD_CHANGED, etc.
- severity (Enum) - LOW, MEDIUM, HIGH, CRITICAL
- title (String)
- description (String)
- deviceName (String)
- ipAddress (String)
- location (String)
- status (Enum) - UNREAD, READ, DISMISSED, ACTIONED
- actionTaken (String)
- createdAt (LocalDateTime)
- readAt (LocalDateTime)
- dismissedAt (LocalDateTime)
Indexes:
- (userId, status)
- (userId, createdAt)
Device Trust Configuration
# application.yml
device:
trust:
enabled: true
window-days: 30 # Trust window duration
max-devices-per-user: 10 # Max trusted devices
auto-cleanup-days: 90 # Remove inactive devices after
login:
security:
max-failed-attempts: 5 # Before temporary lock
lock-duration-minutes: 30 # Temporary lock duration
suspicious-countries: [XX, YY] # Countries requiring extra verification
alert-on-new-country: true # Send alert on new country login
No comments to display
No comments to display