Security
Request Security
- Signing algorithm
Every request is signed with HMAC-SHA256 over
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY_SHA256. The signature is verified server-side usingsubtle::ConstantTimeEqto prevent timing oracles. - Replay prevention
Each request carries a UUID nonce. The server enforces canonical hyphenated UUID format and stores valid nonces in Redis with a 5-minute TTL. Replayed requests are rejected before any business logic runs.
- Timestamp window
Requests with a timestamp outside a ±5-minute window are rejected. Combined with the nonce, this bounds the replay window to the intersection of the two constraints.
- Body limits
Unauthenticated webhook endpoints are capped at
DefaultBodyLimit::max(65_536)to prevent request-body DoS before signature verification runs.
Token Security
- JWT revocation
Tokens are revoked on logout via
SET fraise:revoked:{jti} 1 EX {ttl}in Redis. Every authenticated request checksEXISTS fraise:revoked:{jti}before proceeding. Falls back gracefully when Redis is unavailable. - Secret rotation
The server accepts
JWT_SECRET_PREVIOUSalongside the active secret. Tokens signed with the previous secret remain valid during rotation, preventing forced logouts. - Staff isolation
Staff JWTs are signed with a separate
STAFF_JWT_SECRETand carry business-scoped claims. A staff token cannot be used on customer endpoints, and vice versa.
Client Security
- Keychain storage
Access tokens are stored in the iOS Keychain with
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyand biometric protection (.biometryAny | .devicePasscode). Tokens cannot be extracted from a locked device. - Certificate pinning
PinningDelegateinAPIClient.swiftvalidates the server's certificate against a bundled public key hash. Active before production deployment. - Push notifications
APNs device tokens are registered per-user and used only for silent loyalty-update pushes. No marketing or tracking payloads are sent.
Platform Security
- HTML escaping
All user-supplied data (customer name, reward description, error messages) is HTML-escaped before rendering in server-side HTML pages. No framework escaping is relied upon for values that pass through
format!()strings. - Rate limiting
Per-endpoint rate limits are enforced via Redis INCR + EXPIRE with fixed-window counters. The HTML stamp path uses an IP-keyed counter independent of the business ID, preventing business-targeted DoS.
- QR stamp integrity
QR tokens encode both user ID and business ID. A Lua script atomically validates business ownership and consumes the token in a single Redis round-trip, preventing cross-business token abuse without a race window.
- Audit log
Security-relevant events (cross-business stamp attempts, NFC probes, rate limit hits, staff logins) are written to a permanent audit log with full context before any rejection response is returned.
- CSP + headers
This site sets
Strict-Transport-Security,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy, and aContent-Security-Policyon every response.X-Powered-Byis suppressed.