To start, simply run
docker compose up --build -d
Open Docker Desktop to look at the individual containers and click on their links to access their services.
Credentials to each services are in the docker-compose.yml file.
- Frontend (Nginx): http://localhost:8081
- Backend (FastAPI): http://localhost:8000
- Keycloak: http://localhost:8080 (admin/admin)
- Postgres: localhost:5432 (wallet / walletpass / walletdb)
Use these steps to create test users and exercise RBAC.
- Open Keycloak Admin Console: http://localhost:8080 → login as
admin/admin. - Select realm
wallet-realmfrom the top-left dropdown (notmaster).
Default customer role
- The realm is configured so newly registered users get the
customerrealm role by default. - This lets users signing up from the login/registration screen access the Wallets tab immediately.
Create a customer (two options)
- From the app login page → Register → fill details.
- Or Admin Console → Users → Add user → set Username and Email → Create → Credentials → Set Password (uncheck Temporary) → Save.
Create a merchant
- Admin Console → Users → Add user → Credentials set (Temp password OFF).
- Role mapping → Assign role → Filter by realm roles → select realm role
merchant→ Assign.
Create an admin
- Admin Console → Users → Add user → Credentials set (Temp password OFF).
- Role mapping → Assign role → Filter by realm roles → select realm role
admin→ Assign.
Create an External Auditor
- Admin Console -> Users -> Add user -> Credentials set (Temp password OFF).
- Role mapping -> Assign role -> Filter by realm roles -> select realm role
External Auditor-> Assign.
Notes on roles and UI
- The application recognises realm roles in priority order:
External Auditor>admin>merchant>customer, but retains all granted roles for access checks. - Security mode toggling remains restricted to the
adminrole; External Auditors only receive read access. - On first visit to the Wallets tab, customer/merchant/admin accounts trigger
/bootstrapto align wallet type and credit a one-time SGD 100 initial top-up. External Auditor accounts remain read-only, and their profile is tagged with theExternal Auditorwallet type for visibility.
Bootstrap (first login per user)
- When Wallets opens, the app calls
POST /bootstrapto:- Upsert the user in Postgres (merge by email) and ensure a wallet exists.
- Persist the user’s effective role for the Admin dashboard.
- Align wallet type with the role (
admin>merchant>customer).
Manual bootstrap (optional)
curl.exe -s -H "Authorization: Bearer $TOKEN" -X POST http://localhost:8000/bootstrap
Customer
- Login as a customer → Wallets shows “Welcome …”, “My Wallets” (SGD 100) and Transaction History.
- Send tab is visible to transfer funds to another wallet UUID.
Merchant
- Login as a merchant → Wallet type shows MERCHANT; Merchant tab shows total customers and total incoming.
Admin
- Login as an admin -> Admin tab shows aggregate totals, but wallet IDs and balances are blurred. Admins can toggle security mode in the navbar.
External Auditor
- Login as an External Auditor -> Admin tab displays the full Accounts Overview with wallet identifiers and balances.
API spot checks (PowerShell)
$KC = "http://localhost:8080"
$REALM = "wallet-realm"
$CID = "wallet-backend"
$CSECRET = "backend-secret"
# Get a token for a user (replace username/password)
$TOKEN = (Invoke-RestMethod -Method Post -Uri "$KC/realms/$REALM/protocol/openid-connect/token" -Body @{
grant_type='password';client_id=$CID;client_secret=$CSECRET;username='cust1';password='cust1pass'
}).access_token
Invoke-RestMethod -Headers @{Authorization="Bearer $TOKEN"} -Uri "http://localhost:8000/me"
curl.exe -s -H "Authorization: Bearer $TOKEN" -X POST http://localhost:8000/bootstrap
Invoke-RestMethod -Headers @{Authorization="Bearer $TOKEN"} -Uri "http://localhost:8000/wallets"
This fully resets Postgres and Keycloak volumes. You will need to recreate users.
docker compose down -v # stop and remove containers + named volumes
docker compose up --build -d # start fresh; Keycloak auto-imports wallet-realm
After it starts:
- Recreate users in Keycloak (or register from the app).
- Log into the app once per user and open Wallets to run bootstrap.
Security Mode is a runtime feature flag that switches core defenses on or off to demonstrate attacks and mitigations.
- Backend state and API
- State is managed in
backend/app/security_mode.pyand exposed atGET /security-modeandPUT /security-mode. - Reading the flag requires authentication; updating requires the
adminrole.
- State is managed in
- Frontend UI
- The navbar shows SECURITY ON/OFF. Admins see an Enable/Disable button.
- The Send page displays a banner reflecting the current mode and adds a memo preview area for the XSS demo.
SECURITY_MODE(default:true) initial state at startup.KEYCLOAK_AUDIENCE(e.g.account,wallet-frontend) accepted audiences/clients for tokens.KEYCLOAK_ALLOWED_ISSUERSoptional comma-separated list of accepted issuers to handle container/localhost differences.
UI (admin): click SECURITY Enable/Disable in the navbar.
API examples (PowerShell):
$TOKEN = '<admin-access-token>'
Invoke-RestMethod -Headers @{ Authorization = "Bearer $TOKEN" } \
-Uri http://localhost:8000/security-mode
Invoke-RestMethod -Method Put -Headers @{ Authorization = "Bearer $TOKEN" } \
-ContentType 'application/json' \
-Body '{"enabled": false}' \
-Uri http://localhost:8000/security-mode
- OFF (for demos)
- JWT verification: signature is not validated; claims are accepted as-is.
- Dst Wallet UUID: wallet lookups use raw string SQL (intentionally injectable); currency check relaxed; memo returned unescaped.
- Memo: Script execution in the user’s browser via memo field in the Send form; returns memo unchanged as HTML; Send page renders with
dangerouslySetInnerHTML
- ON (defended)
- JWT: RS/ES signature verification via JWKS; issuer and audience enforced.
- Dst Wallet UUID: UUID validation + parameterized queries; currency must match; memo escaped.
- Memo: Conditional escape controlled by
secure_modeand escape memo server-side
- XSS via Send memo (renders immediately on Send page):
<img src=x onerror="alert('wallet demo')">
- SQLi to redirect transfer (forces destination to an arbitrary wallet):
abc' UNION SELECT id, currency FROM wallet WHERE 1=1 --
- SQLi to exfiltrate data via error message (emails):
abc' UNION SELECT (string_agg(u.id::text || ':' || u.email, ', '))::uuid, 'SGD' FROM app_user u --
- SQLi to list wallet IDs via error message:
abc' UNION SELECT (string_agg(w.id::text, ', '))::uuid, 'SGD' FROM wallet w --
- Forged JWT (privilege escalation when OFF):
- Construct alg “none” token with
adminrole:
header: {"alg":"none","typ":"JWT"}
payload: {"sub":"11111111-1111-1111-1111-111111111111","preferred_username":"evil","realm_access":{"roles":["admin"]}}
token: base64url(header).base64url(payload).
Use as Authorization: Bearer <token> to access admin endpoints during the demo.
/admin/ping
/admin/dashboard
- Re-enable Security Mode when finished to restore defenses.
- The API helper in the frontend surfaces database error details to make exfiltration demos visible in the UI error panel.
- All of these payloads are intentionally effective only with Security Mode OFF; in ON mode they are blocked by validation and parameterized queries.
- Start Docker Desktop + Linux engine
Open Docker Desktop from Start menu.
Wait until it shows Engine running (green).
In the tray icon menu, ensure you’re on Linux containers (if you see “Switch to Windows containers…”, you’re already on Linux; if you see “Switch to Linux containers…”, click it).
- Make sure WSL2 is enabled (Docker’s Linux backend)
Run PowerShell as Administrator:
## Enable required Windows features (safe to re-run)
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
## Set WSL2 as default
wsl --set-default-version 2
Reboot if Windows asks.
Then check that Docker’s internal WSL distros exist:
wsl -l -v
You should see docker-desktop and docker-desktop-data listed and on Version 2.
If they’re missing:
Launch Docker Desktop again (it will create them), or
Run wsl --install (will install a default Ubuntu; still fine for Docker).
- Ensure the Docker service is actually running
Get-Service com.docker.service
# If not Running:
Start-Service com.docker.service
- Sanity checks
docker version # both Client and Server sections should show up
docker context ls # ensure you're on the 'default' context
docker run hello-world # test pulling/running a Linux image
- Run your stack
From the project folder (where docker-compose.yml lives):
docker compose up --build -d