Skip to main content

SECURITY_MODEL — bff-backoffice-service

Sibling: API_CONTRACTS · APPLICATION_LOGIC · SYNC_CONTRACT · DEPLOYMENT_TOPOLOGY

Cross-cutting: 07 Security/Compliance/Tenancy · ADR-0003 Electron offline-first desktop

1. Posture

This BFF is the cloud-side counterpart to a device-installed line-of-business application that holds physical-world authority (issue room keys, post folio charges, change room status, decide AI suggestions). The threat model concentrates on:

  1. Device theft / token theft — DPoP (proof-of-possession) on every request.
  2. Insider abuse — full audit envelope on every mutation; lock revocations gated by MFA step-up; activity ledger 7-y retained.
  3. Cross-tenant access — JWT subject + Postgres RLS + Memorystore namespacing.
  4. Sync-protocol abuse — handshake mints time-boxed signed token; cursor advance audited.
  5. Schema drift / replay — Zod parsers, idempotency keys, audit log.

It is not the source of truth for authentication or authorization (that's iam-service); it enforces device-bound posture, MFA gates, audit, and rate limits.

2. AuthN / AuthZ

PathAuthNAuthZ
/auth/refreshRefresh token + DPoPDevice-bound; iam-service decision
/auth/sign-outBearer + DPoPSelf-only
/auth/mfa/step-upBearer + DPoPOperator self
/dashboard, /today, etc.Bearer + DPoPOperator scope on requested propertyId
/locks/*/issue-keyBearer + DPoPProperty-scoped + role-gated
/locks/*/revoke-keyBearer + DPoP + valid MfaAttestationProperty-scoped + role-gated
/reservations/*/check-in etc.Bearer + DPoPReservation-service final say; BFF passes role context
/folios/*/chargesBearer + DPoP; large amounts gatedProperty-scoped
/sync/handshakeBearer + DPoPDevice-bound to JWT
/sse/streamBearer + DPoPPer-device single connection
/health/*Openn/a
/internal/jobs/*Cloud Scheduler OIDCScheduler SA only

All cross-tenant boundary checks happen at:

  • JWT subject claim (operator + tenant resolved from iam-service)
  • Property scope in JWT or resolved from tenant-service.operatorRole
  • Postgres RLS (tenant_id predicate on every owned table)
  • Memorystore key namespacing ({tenantId}: prefix)

tenant-isolation.spec.ts proves cross-tenant attempts fail at every layer.

3. Device-bound JWT (DPoP)

Per RFC 9449.

3.1 Token shape

{
"iss": "https://iam.melmastoon.ghasi.io",
"aud": "bff-backoffice-service",
"sub": "opr_01H8Y...",
"tnt": "tnt_01H8Y...",
"rol": ["front_desk_supervisor"],
"psc": ["prop_01H8Y..."],
"cnf": { "jkt": "<base64url SHA-256 of device public key>", "kid": "dev_01H8Y..." },
"iat": 1714032862,
"exp": 1714033762,
"jti": "tk_..."
}

15 min TTL.

3.2 DPoP proof

Per request, the desktop signs a single-use DPoP JWT with the device private key. BFF verifies:

  1. Signature against the public key embedded in the proof header.
  2. SHA-256(public key) matches cnf.jkt of the access token.
  3. htm matches the request method.
  4. htu matches the request URL (without query string).
  5. iat within ±60 s of server time.
  6. jti not seen in the last 5 min (Memorystore single-use cache).
  7. ath (access token hash) matches SHA-256 of the access token (when present).

Failure → MELMASTOON.IAM.DPOP_INVALID.

3.3 Refresh

Refresh token is a long-lived (30 d) opaque token bound to the same device key (proof-of-possession again on /auth/refresh). Stolen refresh token without device key is unusable.

4. MFA step-up

Sensitive scopes:

ScopeRequired factor
lock_revokeOperator's primary MFA (TOTP / WebAuthn / SMS)
large_folio_adjustSame; threshold per tenant config
refundSame
force_checkoutSame
device_unenrollWebAuthn preferred; TOTP acceptable

Flow:

desktop ── POST /auth/mfa/step-up { scope, factor, code } ──► BFF
│ │── proxy iam-service.attestStepUp
│◄────── { attestationToken, expiresAt: now+5min, scope } ──│

├── perform sensitive action with mfaAttestationToken header

BFF ── consume(mfaAttestationToken) ── single-use, 5 min TTL
│── proceed with sensitive call (e.g., lock revoke)
│── audit log includes mfa_attestation_id

Replay → MELMASTOON.BFF.BACKOFFICE.MFA_INVALID_OR_USED.

5. Tenant + property boundary

Every request resolves:

  1. tenantId from JWT tnt.
  2. operatorId from JWT sub.
  3. propertyScope from JWT psc (cached) or tenant-service.operatorRole lookup.
  4. requestedPropertyId from header / query.
  5. Reject with MELMASTOON.BFF.BACKOFFICE.PROPERTY_OUT_OF_SCOPE if requestedPropertyId ∉ propertyScope.
  6. Set app.tenant_id on Postgres connection for RLS.
  7. Cache keys include tenantId.

6. Audit envelope

Every mutation proxy and lock-action proxy carries:

"_audit": {
"tenantId": "tnt_...",
"operatorId": "opr_...",
"deviceId": "dev_...",
"sessionId": "bos_...",
"requestId": "req_...",
"traceId": "00-...",
"appVersion": "1.4.2",
"appPlatform": "win32",
"mfaAttestationId": null,
"emittedAt": "..."
}

Plus X-Audit-* request headers carrying the same values to upstream services that prefer headers.

7. Activity ledger

bff_backoffice.operator_activity rows are immutable; cannot be deleted via API. Retention 90 d hot, 7 y cold (BigQuery export). Used for:

  • Investigations (incident replay).
  • Insider abuse detection (anomalous action volume; off-hours mutations).
  • Compliance audits (operator-by-operator activity reports).
  • AI training feedback loops (operator override patterns).

8. Lock-action proxy controls

Lock issue/revoke is the most physically consequential action. Controls:

  • Operator must have front_desk or front_desk_supervisor role (or per-tenant equivalent).
  • Operator's property scope must include the target property.
  • Revocation requires fresh MFA attestation.
  • Per-device rate limit (30 lock actions / hour); abuse triggers MELMASTOON.GENERAL.RATE_LIMITED.
  • Audit row written before vendor call; outcome updated after.
  • Failures escalate via melmastoon.alert.raised.v1 with category=lock_failed.
  • 7-y retention on lock_action_proxy_audit; never deletable.

9. CSP + Electron renderer hardening

Per ADR-0003 §3, the Electron renderer is sandboxed (nodeIntegration: false, contextIsolation: true, sandbox: true). The renderer reaches the BFF only through the contextBridge in main process; the BFF never serves any HTML to the Electron renderer.

For the debug build (web dev tools), the BFF returns:

Content-Security-Policy:
default-src 'none';
connect-src 'self' https://api.melmastoon.ghasi.io https://stage-backoffice.melmastoon.ghasi.io;
frame-ancestors 'none';
base-uri 'none';
object-src 'none';
upgrade-insecure-requests;

10. CORS

Allowed originEnvironment
app://melmastoon-backofficeElectron production
http://localhost:5173Dev renderer
https://stage-backoffice-debug.melmastoon.ghasi.ioDebug stage

All others 403. Mutating endpoints additionally require Origin header to be on allow-list.

11. Secrets

SecretStorageRotation
bff-backoffice-pepper (PII hashing)Secret Manager365 d, 30 d overlap
bff-backoffice-sse-signing-key (SSE event signature optional)Secret Manager180 d
Service account keyNOT used; workload identity to bff-backoffice-san/a
iam-service JWKS endpointPublic (per JWT spec)n/a

DPoP keys live on the device via keytar (OS keychain); never in this BFF.

12. Rate limiting

ScopeLimit
Per device per endpoint family60 / min reads, 30 / min mutations
/devices/*/heartbeat1 / min
/auth/refresh5 / 15 min / device
/auth/mfa/step-up10 / hour / device
/locks/*30 / hour / device
/sse/stream1 active connection / device
/sync/handshake12 / hour / device

429 carries Retry-After. Cloud Armor adds IP-level rules atop these for DDoS resilience.

13. PII inventory

FieldStored?FormRetention
Operator nameNo (BFF)n/an/a
Operator emailNon/an/a
Guest name in API responsesyes (transient; not persisted)resolved on dashboard render0 (response only)
Notes (alert ack, AI decision)yes (truncated)200-char truncation365 d / 7 y
IP, UAhashedsha256 with peppersession-bound
Device fingerprinthashedsha256 with peppersession-bound

telemetry-pii.spec.ts asserts no plain PII appears in any published event. Audit lake rows reference only resource ids (no PII).

14. Threat model (top 10)

#ThreatMitigation
1Stolen access token replayDPoP single-use; cnf.jkt thumbprint check
2Stolen refresh tokenDPoP on refresh; device-bound
3Compromised device keyOS keychain isolation; per-device revocation; alert on key-material-not-attested
4Cross-tenant cache key collisionTenant-scoped keys; tenant-isolation.spec.ts
5Insider lock-revoke abuseMFA gate; full audit; activity-ledger anomaly detector
6Insider folio adjustment fraudMFA gate above threshold; audit; nightly reconciliation
7Schema drift from upstreamZod parsers; SCHEMA_DRIFT alert; per-PR contract tests
8SSE connection abuse (long-poll DDoS)Per-device 1-conn cap; idle-timeout; Cloud Armor rules
9Idempotency key reuse with different bodiesrequest-hash check; PRECONDITION_FAILED
10Trace / log PII exfiltrationlog filter + nightly synthetic probe; audit lake export uses ids only

15. Container hardening

  • Distroless Node 20 base image.
  • Non-root node user.
  • Read-only root filesystem.
  • No shell.
  • Trivy scan in CI; high/critical CVE blocks promotion.
  • Cosign signing with Fulcio identity; binary authorization in prod cluster verifies.

16. Compliance

  • PCI: out of scope (no PAN here).
  • GDPR: in scope; operator PII via tenant-service; we hold hashed identifiers only.
  • Sharia compliance respected via complianceProfile propagation.
  • Audit retention 7 y for: lock actions, AI decisions, alert acks; satisfies most regulators.
  • BAA / DPA: tenant-by-tenant; this BFF is on the in-scope list for tenant agreements.

17. Penetration test

Annual pen test by external firm. Specific surfaces tested:

  • DPoP replay
  • MFA bypass
  • Cross-tenant via header injection
  • SSE flood
  • Lock-revoke without MFA
  • Idempotency key bypass
  • Refresh token theft simulation

Findings tracked in services/bff-backoffice-service/_pentest/<year>.md.

18. Cross-references