Authentication Mechanisms
Document ID: ACP-001.1 Version: 1.0 Classification: Internal / Regulator-Shareable Effective Date: March 2026 Next Review: September 2026 Parent Document: ACP-001 — Access Control & Permissions
1. Overview
Keshless employs multiple authentication mechanisms tailored to each access channel. All mechanisms enforce the principle of fail closed — unauthenticated requests are rejected by default.
| Channel | Primary Auth | Secondary Auth | Token Type |
|---|---|---|---|
| User App | OTP (WhatsApp/SMS) | — | JWT (Bearer) |
| Vendor App | Email + Password | Device ID verification | JWT (Bearer) |
| Admin Dashboard | Email + Password | — | JWT (Bearer) |
| USSD | Phone Number (telco) | 4-digit PIN | Session-based |
| Partner API | API Key (X-API-Key) | IP Whitelist | Stateless |
| Scheduled Jobs | Job Secret (X-Job-Secret) | — | Stateless |
2. JWT Token Architecture
2.1 Token Configuration
| Parameter | Access Token | Refresh Token |
|---|---|---|
| Algorithm | HS256 | HS256 |
| Default Expiry | 7 days | 30 days |
| Storage (Client) | localStorage (auth_token) | localStorage (refresh_token) |
| Transport | Authorization: Bearer <token> | X-Refresh-Token: <token> |
| Revocation | Expiry-based (stateless) | Expiry-based (stateless) |
2.2 JWT Payload Structure
The JWT payload carries identity and authorization data, eliminating the need for server-side sessions:
{
userId — Unique user/admin/vendor ID
email — Email address (admins, vendors)
phoneNumber — Phone number (end users)
role — Base role: USER, ADMIN, or SUPER_ADMIN
// Vendor-specific fields
userType — 'vendor' or 'sub-user'
vendorId — Parent vendor ID (for sub-users)
permissions — Vendor permission array
// Admin-specific fields
adminEmployeeId — Admin employee record ID
adminRole — Specialized role (9 types)
adminPermissions— Granular permission array (90+)
iat — Issued-at timestamp
exp — Expiry timestamp
}2.3 Token Lifecycle
Login (credentials validated)
│
▼
Generate Token Pair
├── Access Token (7-day expiry)
└── Refresh Token (30-day expiry)
│
▼
Client stores tokens in localStorage
│
▼
API Request with Authorization: Bearer <access_token>
│
├── Token valid → Proceed to authorization layer
│
└── Token expired → Client sends refresh token
│
├── Refresh valid → New token pair issued
└── Refresh expired → User must re-loginOn 401 Response (Dashboard): The frontend automatically clears all tokens and user data from localStorage and redirects to /login.
3. End User Authentication
3.1 OTP-Based Login
End users authenticate via One-Time Password delivered through WhatsApp (primary) or SMS (fallback).
Flow:
- User submits phone number (
+268XXXXXXXX) - Server generates 6-digit OTP
- OTP delivered via WhatsApp (falls back to SMS if delivery fails)
- User submits OTP within validity window
- Server validates OTP and issues JWT token pair
3.2 OTP Configuration
| Parameter | Value |
|---|---|
| Length | 6 digits |
| Expiry | 5 minutes |
| Max Attempts | 3 per OTP |
| Delivery | WhatsApp (primary), SMS (fallback) |
| Rate Limit | 5 requests per 15 minutes per phone |
| Cooldown | New OTP invalidates previous |
3.3 Security Controls
- Phone number normalization: All formats (
0XX,268XX,+268XX,0268XX) normalized to E.164 (+268XXXXXXXX) - OTP not logged: OTP codes are never written to application logs
- Brute force protection: After 3 failed attempts, the OTP is invalidated — user must request a new one
4. Admin Employee Authentication
4.1 Email + Password Login
Admin employees access the dashboard via email and password at /api/admin/employees/login.
Flow:
- Admin submits email + password
- Server validates credentials (bcrypt comparison, 12 salt rounds)
- Server checks account status (active, not locked, password not expired)
- If
mustChangePasswordis set, admin is forced to change password before proceeding - Server returns JWT with
adminRoleandadminPermissionsembedded - Dashboard stores token and user profile in localStorage
4.2 Password Policy
| Requirement | Value |
|---|---|
| Minimum Length | 8 characters |
| Complexity | Uppercase + lowercase + number + special character |
| Hashing | bcrypt with 12 salt rounds |
| History | Last 5 passwords tracked (no reuse) |
| Expiry | Configurable via passwordExpiresAt |
| First Login | mustChangePassword flag enforced |
4.3 Account Lockout
| Parameter | Value |
|---|---|
| Tracking | failedLoginAttempts counter per account |
| Lockout | lockedUntil timestamp set after threshold |
| Scope | Per-account |
| Reset | Successful login resets counter; SUPER_ADMIN can manually unlock |
4.4 Password Recovery Rate Limits
| Operation | Rate Limit |
|---|---|
| Forgot Password | 5 requests per 15 minutes (per IP + email) |
| OTP Verification | 3 attempts per 5 minutes (per IP + email) |
| Recovery Key | 3 attempts per 30 minutes (per IP + email) |
| Password Generator | 10 requests per minute |
4.5 Super Admin Recovery Key
SUPER_ADMIN accounts have an additional recovery mechanism via SuperAdminRecoveryKey. This is a one-time-use credential for scenarios where the primary authentication method is compromised. Recovery keys are generated at account creation and can only be used once.
5. Vendor Authentication
5.1 Email + Password + Device ID
Vendors authenticate with three factors: email, password, and a device identifier that binds sessions to known devices.
Flow:
- Vendor submits email + password + device ID
- Server validates credentials
- Server verifies device ID against registered devices
- Server returns JWT with
vendorId,userType(vendor), and full vendor permissions
5.2 Vendor Sub-User Authentication
Vendor sub-users (cashiers, managers, accountants) authenticate under the parent vendor context:
- JWT includes
vendorId(parent vendor) andpermissions(sub-user's granted permissions) - Sub-users cannot exceed the parent vendor's capabilities
- Sub-user access can be revoked by the vendor owner or any system admin
6. USSD Authentication
6.1 Session + PIN Model
USSD uses a two-factor approach: the telco-verified phone number (implicit) plus a user-set 4-digit PIN.
6.2 Session Configuration
| Parameter | Value |
|---|---|
| Storage | In-memory server-side Map |
| TTL | 5 minutes |
| Auto-cleanup | Every 60 seconds |
| Session Data | Phone, menu path, transaction state |
| Input Modes | Accumulated (1*2*500) and per-request (500) auto-detected |
6.3 PIN Verification
All financial operations on USSD require PIN verification before execution.
| Parameter | Value |
|---|---|
| PIN Length | 4 digits |
| Storage | bcrypt hashed in database |
| Max Attempts | 3 (configurable via USSD_PIN_MAX_ATTEMPTS) |
| Lockout Duration | 15 minutes (configurable via USSD_PIN_LOCK_MINUTES) |
| Lockout Tracking | In-memory per phone number |
Verification Flow:
User enters PIN
│
├── Check lockout → Locked? → "Account locked. Try again in X minutes."
│
├── Lookup user by normalized phone → Not found? → "Account not found."
│
├── Check account status
│ ├── Inactive? → "Account inactive. Visit a service center."
│ └── No PIN set? → "Set a PIN via the Keshless app."
│
└── bcrypt compare
├── Match → Reset attempt counter, proceed with operation
└── No match → Increment counter
├── Attempts < 3 → "Incorrect PIN. X attempts left."
└── Attempts = 3 → Lock account for 15 minutes6.4 Vendor USSD PIN
Vendors accessing USSD business features (Menu 8) use a separate PIN with independent lockout tracking keyed as vendor:{phone}. Same 3-attempt, 15-minute lockout rules apply.
6.5 Telco-Level Security
Before any USSD session reaches the application layer:
| Control | Implementation |
|---|---|
| IP Whitelist | USSD_ALLOWED_IPS — only telco gateway IPs accepted |
| Auth Token | Per-carrier tokens: USSD_AUTH_TOKEN_MTN, USSD_AUTH_TOKEN_EM |
| Rate Limiting | 10 requests per 60 seconds per phone number |
| Swazi Mobile | Separate IP whitelist + Swazi-Key header validation |
7. API Key Authentication
7.1 Application API Key
A shared API key (APP_API_KEY) validates requests from Keshless frontend applications:
- Passed via
X-API-Keyheader - Used for public-facing endpoints that need basic authentication
- Some endpoints accept either JWT Bearer OR API key via
validateBearerOrApiKeymiddleware
7.2 Vendor Integration API Keys
Third-party systems integrate via vendor-specific API keys with additional security controls. See Integration Security for full details.
7.3 Job/Scheduler Authentication
Cloud Scheduler triggers authenticate via X-Job-Secret header:
- Validated against
JOB_SECRETenvironment variable - Used exclusively for automated maintenance tasks (backups, reconciliation)
- No user context — operates with system-level access
8. Middleware Execution Order
Every API request passes through middleware in this exact sequence:
| Order | Middleware | Purpose |
|---|---|---|
| 1 | helmet | Security headers (CSP, HSTS, X-Frame-Options) |
| 2 | compression | Response compression |
| 3 | requestLoggerMiddleware | Request/response logging |
| 4 | performanceMonitor | Slow query detection |
| 5 | databaseCheckMiddleware | Reject if database disconnected |
| 6 | systemEmergencyCheckMiddleware | Check kill switches (CRITICAL) |
| 7 | Route-specific middleware | Auth + Permission + Ownership checks |
| 8 | Controller logic | Business logic execution |
| 9 | Error handlers | Catch and format errors |
Key property: Emergency controls (step 6) execute before authentication (step 7). Kill switches can block all requests system-wide regardless of authentication status.
9. Authentication Failure Responses
| Scenario | HTTP Status | Response |
|---|---|---|
| Missing token | 401 | Authentication required |
| Invalid/expired token | 401 | Invalid or expired token |
| Invalid API key | 401 | Invalid API key |
| IP not whitelisted | 403 | IP address not authorized |
| Account locked | 423 | Account locked until [timestamp] |
| Rate limit exceeded | 429 | Too many requests + Retry-After header |
| DB unavailable | 503 | Service temporarily unavailable |
| System shutdown | 503 | Maintenance message from emergency config |