How the bank stays safe while customers play.
The Alfouz Gamification App is built so that an exposed mobile-app endpoint can never do anything destructive to the customer's bank account. The app is allowed to create an app login, read the customer's Alfouz balance and tier, and add money to the Alfouz account. Every other bank operation — withdrawals, transfers, payee management, card actions, change of personal data — is not exposed by this app and cannot be reached through it.
This document explains the security model in plain terms, for review by the Central Bank of Kuwait and ABK's own security team.
Overview
The Alfouz Gamification App is a customer-engagement layer that runs alongside the Al Ahli Bank of Kuwait core banking system. It rewards existing Alfouz account holders for keeping a balance in the bank, through in-app spins, tier levels, and savings missions.
The app is a read-mostly client of the bank. It reads the customer's identity (via PACI), their Alfouz balance, and their Alfouz draw chances. The only write operation against the bank's records is a top-up deposit initiated through KNET, which goes directly to the customer's existing Alfouz account.
Bank-held identity, balance, tier, draw chances
The app reads these values through bounded ESB endpoints and renders them in the UI.
App login, top-up, in-app spin economy
The app creates an app-login record, processes KNET top-up to the Alfouz account, and maintains its own internal spin wallet. None of these are bank-core writes other than the top-up payment.
Withdrawal, transfer, card or personal-data change
The app's API surface does not include any of these operations. They are not reachable through any code path or admin override.
API surface boundary
The app's backend exposes a fixed, audited list of HTTP endpoints. Anything not on this list is not implemented; there is no admin override, hidden flag, or feature toggle that can expose additional bank operations.
Allowed operations PERMITTED
| Group | Endpoints | What it does |
|---|---|---|
| Identity | POST /api/paci/initiate POST /api/paci/poll POST /api/wf2/finalize |
Verifies civilId through PACI; binds the verified identity to the new app login. |
| App login | POST /api/auth/login POST /api/auth/refresh POST /api/auth/forgot-password |
Authenticates an existing app login; refreshes JWT; password reset through the same OTP gates. |
| Read — profile | GET /api/profile GET /api/dashboard GET /api/tiers/me |
Returns the customer's bank-held name, masked phone, current balance, current tier. |
| Read — Alfouz draw | GET /api/draws GET /api/draws/next GET /api/chances |
Returns the read-only Alfouz draw schedule, winners, and the customer's chance counts. |
| Write — top-up | POST /api/topups POST /api/topups/{id}/confirm |
Initiates a KNET payment that credits the customer's existing Alfouz account. Money never enters the app's own accounts. |
| App-internal — spins | GET /api/spins POST /api/spins/{id}/reveal GET /api/missions |
Manages the in-app spin wallet and savings missions. No bank-core writes. |
Explicitly not exposed DENIED
The following bank operations exist in the core banking system, but the Alfouz Gamification App does not implement any endpoint or code path that can trigger them:
- Withdrawal from the Alfouz account, the main current account, or any savings account.
- Transfer to another customer, to an external account (local or international), or between the customer's own accounts.
- Payee management — adding, removing, or modifying a saved beneficiary.
- Card operations — issuing, blocking, unblocking, PIN change, contactless toggle, online-payment toggle, or limit change.
- Standing orders or scheduled payments — creating, modifying, or cancelling.
- Personal-data change — name, address, civilId, bank-held phone, bank-held email, profession, salary record, or any KYC field.
- Account opening or closure — no new account is opened through the app, and no account is closed through the app.
- Alfouz draw influence — the app cannot add or remove draw chances, change the customer's draw eligibility, or affect the draw shuffle in any way.
- Admin or service-desk operations — no impersonation, no balance override, no transaction reversal, no audit-log modification.
If an attacker fully compromised the mobile API server, the worst they could do is read the data listed in the Allowed table above, push KNET top-ups to customers' Alfouz accounts (which would add money to those accounts, not take any), and write to the app-internal spin wallet (which is not real money). They cannot withdraw, transfer, or change any bank record.
Identity & authentication
The app's identity model has two layers: an upstream PACI verification (one-time at registration and at password recovery) and an app-login secret (password) used for every session afterwards. Both are validated server-side; the device never asserts identity by itself.
The Civil ID is verified through Kuwait Mobile ID (Hawiyti)
At registration and at password recovery, the server initiates a PACI session and binds the resulting requestId to the submitted civilId server-side. The user authorises the disclosure inside the official Hawiyti app. The server never trusts a client-asserted “PACI passed” flag.
civilId + password, validated server-side
The app password is hashed with a per-user salt; the server never transmits or logs the plaintext beyond the single login request. JWT tokens are issued on successful login with a short lifetime; refresh requires a valid refresh token.
Anti-impersonation
A client cannot submit civilId = A, complete PACI for A, then submit civilId = B at finalize and inherit the verification. The civilId is recovered from the server-side requestId binding on every step, not from the client's request body. If the bodies disagree, the request fails closed.
Data protection
TLS 1.2+ end-to-end
All API traffic is HTTPS. The .app TLD is HSTS-preloaded by Google, so browsers and the Flutter HTTP client refuse plaintext connections to any alfouz.app hostname.
Encrypted database storage
Customer data lives in SQL Server with transparent data encryption. Password hashes use a strong per-user salt; OTPs are stored hashed; PACI requestIds live in a 10-minute TTL cache.
Masked PII only
The bank-held phone number is never returned to the device in full. The server returns a masked display string (e.g. +9655•••0002) as an opaque token. The same masking applies to email and any other identifier.
No PII in application logs
Application logs record event types and customer ids, never raw civilIds, phone numbers, emails, OTP values, or passwords. SQL parameter values are redacted before logging.
Audit & logging
Every gate in the registration and login flows emits a discrete server-side event tied to a session identifier. The full registration of any customer is reconstructable from the audit log alone, without consulting the production database.
| Event | Captured |
|---|---|
| PACI initiate | civilId hash, requestId, server timestamp, source IP |
| PACI verified | requestId, PACI response code, server timestamp |
| Phone OTP sent / validated | customerId, channel = Phone, attempt counter, timestamp |
| Email OTP sent / validated | customerId, channel = Email, attempt counter, timestamp |
| Password set | customerId, hash version, timestamp (plaintext never logged) |
| Registered | customerId, civilId hash, timestamp (closes the WF2 session) |
| Login success / failure | civilId hash, source IP, timestamp, failure reason if any |
| Top-up initiated / completed | customerId, KNET reference, amount, timestamp |
| Spin reveal | customerId, spinPoolId, prizeId (if any), shuffle audit id, timestamp |
Audit events are written synchronously with the operation they record. A spin reveal that fails to write its audit row also fails to commit the reveal — there is no path that produces an outcome without an audit trail.
Rate limiting
- OTP issuance: N codes per channel per hour per customer. Exceeding the limit returns a generic “please try again later” without disclosing the actual cap.
- OTP validation attempts: N attempts per code. On exhaustion the code is destroyed and a fresh one must be requested, subject to the issuance limit.
- Login attempts: N failed attempts per civilId per window. Continued failures escalate to a temporary lockout that requires recovery through the OTP flow.
- PACI initiate: N sessions per civilId per hour. Prevents PACI-side rate-limit consumption by abusive clients.
- Top-up: Bounded by KNET's own per-card and per-day limits, plus a server-side daily cap per customer to absorb retry storms.
Threat model
| Threat | Mitigation |
|---|---|
| civilId substitution — client submits civilId A, completes PACI for A, then sends civilId B at finalize hoping to inherit A's verification. | Server recovers the verified civilId from the PACI requestId binding, not the client's request body. Mismatch fails closed. |
| OTP brute-force — attacker tries all six-digit codes. | Server-side validation only, attempt-capped, channel-scoped, single-use, plus issuance rate limit prevents code-pool refill. |
| OTP replay — reuse of a previously seen valid OTP. | OTPs are marked consumed on first valid use; reuse fails. OTPs also expire on a short TTL. |
| Channel cross-consumption — using a phone OTP to validate the email gate or vice versa. | OTPs are issued with a Channel attribute; validation requires the code and matching channel. |
| JWT replay — stolen token used after the user logs out. | Short access-token lifetime; logout invalidates the refresh token server-side. |
| API host compromise — the mobile API server is breached. | API surface allowlist prevents the attacker from invoking any withdrawal, transfer, payee, card, or personal-data operation. Worst case: read profile data and push KNET top-ups that add money to customers' accounts. |
| Database compromise — SQL Server access by an attacker. | Passwords are hashed with a per-user salt; OTPs are short-TTL and stored hashed; civilIds are stored as full values for bank operations but never returned to the device in full; encryption at rest applies. |
| Man-in-the-middle — intercepting API traffic. | TLS 1.2+ end-to-end; HSTS preload via the .app TLD; certificate pinning in the Flutter HTTP client for the production hostname. |
| Social engineering — fake prize message asking the customer for card details. | The Diraya in-app financial-literacy module presents a daily anti-fraud scenario and rewards the customer for choosing the safe response. |
Bank-core isolation
The mobile API does not talk to the bank's core banking system directly. It communicates through a bounded ESB adapter that exposes only the operations the app needs:
- GetCustomerIdentity — read identity for a verified civilId
- GetAlfouzAccount — read Alfouz balance and tier eligibility
- GetAlfouzDrawChances — read the customer's draw chance counts
- CreditAlfouzAccount — credit the customer's Alfouz account from a verified KNET payment
The ESB adapter rejects any call that does not match this allowlist. A vulnerability in the mobile API server cannot reach SendMoney, WithdrawCash, UpdateCustomerData, OpenAccount, or CloseAccount — those operations exist in the core but are not on the ESB allowlist this app uses.
The credit operation is also bounded: it accepts only a positive amount, only the customer's own Alfouz account as destination (derived from the JWT, not the request body), and only after KNET has confirmed the payment at its end.
Operational posture
Runs inside the bank's production environment
The backend is hosted on the bank's own production infrastructure, behind the bank's existing network controls, firewalls, and monitoring. It is not operated on any third-party public cloud. The app server reaches the core only through the bounded ESB adapter described above.
Integration-ready, simulation for testing
During testing the app runs against deterministic in-process simulators for PACI and the bank Alfouz lookup. Connecting to the bank's live ESB endpoints flips a single configuration flag; no schema or code change is required.
Audited, out-of-band releases
Releases follow the bank's production change-management process. The application server exposes no interactive remote shell; each change is applied through an audited pipeline and attributed to an authenticated operator.
Health endpoints + web-server access logs
Health endpoints confirm service state on every release. Web-server request logs capture every API call with timestamp, source IP, status, and a customer hash — never raw PII.
Compliance summary
PACI as root of trust
Verification is server-resolved against a Kuwaiti government source on every registration and password recovery. The client never asserts identity.
No destructive bank operations
The app's API surface is an audited allowlist. Withdrawal, transfer, card, payee, and personal-data operations are not implemented and not reachable through any admin override.
Every gate is an event
Each step emits a server-side event with a session identifier. Audit and operation commit together — one without the other is not possible.
Masked to the device
Bank-held phone, email, and other identifiers are returned to the device only as masked opaque display strings. Application logs never record raw PII.
Daily anti-fraud education
The Diraya module surfaces a short anti-fraud scenario each day inside the app, in Arabic and English, with a small in-app reward for engagement.
Available for follow-up review
The Al Ahli Bank of Kuwait Alfouz team is available to walk the Central Bank of Kuwait's reviewers through any gate, endpoint, audit event, or threat-model entry in person.
Three points to remember. The bank account is read-mostly. There is no withdrawal, transfer, or destructive operation exposed by the app. Every operation is audited end-to-end.