API Matrix β€” GIW POC Identity Platform

Status: POC Β· Date: 2026-09-21

Every API and integration in the platform, on one page.

PII column: none Β· personal Β· sensitive (sensitive personal data under Law 91/2025/QH15).


1. Portal API β€” the only thing the browser calls

Provider: Wi-Fi Portal. Consumer: the portal's own frontend. Auth: opaque sid cookie (HttpOnly, SameSite=Strict, 1 h).

API PII IdemΒ­potent Status POC / Production
POST /api/v1/auth/login personal (email) + secret (password, transient) no β€” each call is a fresh authentication IMPLEMENTED POC ONLY. Direct Grant, T-GRANT
POST /api/v1/auth/register personal (email, name) + secret (password) no β€” 409 on duplicate IMPLEMENTED POC. No email verification (no mail server)
POST /api/v1/auth/employee sensitive (CCCD in, never out) yes β€” verify is read-only IMPLEMENTED POC. T-CCCD
GET /api/v1/auth/session none beyond the subject yes β€” read-only IMPLEMENTED POC
POST /api/v1/auth/refresh none no β€” rotates the token IMPLEMENTED POC
POST /api/v1/auth/logout none yes β€” always 200 IMPLEMENTED POC
GET /api/v1/meta none yes IMPLEMENTED POC

The browser learns one hostname (the portal) and holds one opaque cookie. It never sees a token, a Keycloak URL, or the address of any downstream service.

2. Service APIs (Galaxy-built)

API Provider Consumer Auth PII IdemΒ­potent Status POC / Production
POST /api/v1/employees/verify HR (mocked) Keycloak authenticator X-API-Key sensitive (CCCD in, never out) yes β€” read-only IMPLEMENTED POC. Production contract TBC (T-HR-SOR)
POST /api/v1/entitlements/evaluate Entitlement Service Wi-Fi Portal Bearer JWT (RS256, iss+aud+exp verified) none β€” subject id only yes β€” Idempotency-Key IMPLEMENTED POC. Policy depends on ADR-011
GET /api/v1/entitlements/{id} Entitlement Service Wi-Fi Portal, Ops Bearer JWT none yes β€” read-only IMPLEMENTED POC
POST /api/v1/sessions Session Service Wi-Fi Portal X-API-Key none β€” opaque deviceRef yes β€” Idempotency-Key IMPLEMENTED POC. Real adapter per ADR-007
POST /api/v1/sessions/{id}/grant Session Service Wi-Fi Portal X-API-Key none yes β€” declarative IMPLEMENTED POC
POST /api/v1/sessions/{id}/revoke Session Service Wi-Fi Portal X-API-Key none yes β€” declarative IMPLEMENTED POC
GET /api/v1/sessions/{id} Session Service Wi-Fi Portal, Ops X-API-Key none yes β€” read-only IMPLEMENTED POC
GET /health all four services Ops, Docker none none yes IMPLEMENTED POC
POST /admin/fault mock-hr-api test harness none none yes IMPLEMENTED POC ONLY β€” must not exist in production
POST /admin/reset-limits mock-hr-api test harness none none yes IMPLEMENTED POC ONLY β€” must not exist in production
POST /api/v1/decisions/{model}/evaluate DMN Service Entitlement Service none none yes β€” pure function IMPLEMENTED POC. Rules in .dmn on disk
GET /api/v1/decisions DMN Service Ops none none yes IMPLEMENTED POC
POST /api/v1/members/lookup Loyalty (mocked) Entitlement Service X-API-Key personal (email in, never out) yes β€” read-only IMPLEMENTED POC. Enrichment, never a gate
POST /api/v1/decisions/reload DMN Service business owner / CI none none yes IMPLEMENTED ⚠️ unauthenticated β€” see DMN-DECISION-SPEC.md Β§7

2b. giw-admin module β€” member company integration

Provider: a Keycloak custom module (RealmResourceProvider, id giw-admin) served by Keycloak itself at /realms/{realm}/giw-admin. MVP POC scope: one module holds connector config, batch import, the inbound webhook and the decision-table editor. ADR-012 records what a later MVP splits out and what that costs today.

Auth on /api/*: a realm bearer token carrying realm-management/manage-users. Not realm-admin β€” this module provisions users, it does not administer the realm. A master-realm admin token is not accepted: the module authenticates against the realm it serves.

API PII IdemΒ­potent Status POC / Production
GET /giw-admin/ none yes IMPLEMENTED POC. GUI shell, public; every /api call below authenticates
GET /giw-admin/api/companies none yes β€” read-only IMPLEMENTED POC. Returns hasSecret, never the secret
PUT /giw-admin/api/companies/{code} none yes β€” full replace IMPLEMENTED POC. Stored as a realm attribute β€” no versioning, no review (ADR-012)
DELETE /giw-admin/api/companies/{code} none yes IMPLEMENTED POC. Removes config only; imported users stay
POST /giw-admin/api/companies/{code}/test none yes β€” read-only probe IMPLEMENTED POC. GET {endpoint-origin}/health, reports UP / HTTP_n / UNREACHABLE
POST /giw-admin/api/import?mode=preview sensitive (CCCD in, never out, never stored) yes β€” writes nothing IMPLEMENTED POC. Evaluates each row against the realm as it is now
POST /giw-admin/api/import?mode=commit sensitive (CCCD in, hashed on arrival) no β€” creates and links users IMPLEMENTED POC. T-USER-SYNC β€” copy vs federate is still open
POST /giw-admin/webhook/{company} personal (employee reference) yes β€” deduped by X-GIW-Event-Id IMPLEMENTED POC. In-memory dedupe; T-WEBHOOK-DURABILITY
GET /giw-admin/api/events personal (event payloads) yes β€” read-only IMPLEMENTED POC. Ring buffer of 200, lost on restart
GET /giw-admin/api/dmn/models none yes IMPLEMENTED POC. Proxies dmn-service
GET /giw-admin/api/dmn/models/{name} none yes IMPLEMENTED POC
PUT /giw-admin/api/dmn/models/{name} none yes β€” full replace IMPLEMENTED POC. No approval workflow β€” same gap as /reload, see DMN-DECISION-SPEC Β§7
POST /giw-admin/api/dmn/evaluate/{name} none yes β€” read-only IMPLEMENTED POC. For trying a rule change before saving it

⚠️ Connector config is not wired into the login path yet. The employee authenticator reads hrEndpoint from the flow's authenticatorConfig, not from giw.company.{code}. Editing an endpoint here does not change what Keycloak calls at login. Settling that needs T-HR-ROUTING; tracked as DEFERRED-TASKS D5.

What the module deliberately does not do

Not done Why
Store the CCCD Rows are matched on HMAC-SHA256(cccd, realm salt). The raw number is dropped after hashing. Cost: you cannot look a person up by CCCD, and rotating the salt orphans every link
Set employee_verified on import An import is a data load, not a verification. Only a live HR check at login may set it
Apply non-identity events MEMBER_TIER_CHANGED and anything like it is recorded, not applied. Writing a tier into Keycloak makes it a stale cache of the loyalty system
Overwrite on a CCCD collision Same CCCD under a different employee_ref returns CONFLICT. Could be a re-hire, could be a typo, could be a row about to take over someone else's account β€” silent overwrite is the one outcome that hides which
Return a webhook secret Write-only. The API reports whether one is set, never its value

Webhook contract

POST /realms/{realm}/giw-admin/webhook/{company}

Header Required Meaning
X-GIW-Timestamp yes Unix seconds. Rejected outside Β±5 minutes
X-GIW-Signature yes base64(HMAC-SHA256(secret, "{timestamp}.{rawBody}")). Signed over the bytes as sent β€” re-serialising first would verify our own serialiser, not the sender
X-GIW-Event-Id recommended Duplicate suppression. Without it a provider's retry is applied twice
Event type Effect
EMPLOYEE_TERMINATED Account disabled, employee_verified=false
EMPLOYEE_SUSPENDED Account disabled
EMPLOYEE_REINSTATED Account enabled. Verification not restored
EMPLOYEE_COMPANY_CHANGED company attribute updated
anything else RECORDED β€” logged, not applied
Result HTTP Meaning
APPLIED 200 Identity change made
RECORDED 200 Accepted, not an identity event
DUPLICATE 200 Event id already processed
NO_MATCH 200 No user carries that employeeRef β€” recorded for review
CONFLICT 409 More than one user matches
REJECTED 401 / 400 Bad signature, stale timestamp, no secret configured, or missing employeeRef

3. Standard OAuth / OIDC (not Galaxy-built β€” do not re-specify)

All server-to-server. No browser redirect exists in this platform.

Endpoint Grant / method Consumer Client PII Status
POST /protocol/openid-connect/token password Wi-Fi Portal wifi-portal-api (confidential) secret in transit STANDARD β€” RFC 6749 Β§4.3
POST /protocol/openid-connect/token password + employeeId + citizenId Wi-Fi Portal wifi-portal-employee-api (confidential) sensitive in transit STANDARD endpoint, custom direct-grant flow
POST /protocol/openid-connect/token refresh_token Wi-Fi Portal either none STANDARD β€” RFC 6749 Β§6
POST /protocol/openid-connect/token client_credentials Wi-Fi Portal wifi-portal-api service account none STANDARD β€” RFC 6749 Β§4.4
POST /admin/realms/{realm}/users Bearer (service account) Wi-Fi Portal scoped to manage-users personal STANDARD KEYCLOAK ADMIN
POST /protocol/openid-connect/logout back-channel, refresh_token Wi-Fi Portal either none STANDARD β€” RFC 7009 style
GET /protocol/openid-connect/userinfo Bearer Wi-Fi Portal either personal STANDARD OIDC
GET /protocol/openid-connect/certs none (public keys) Entitlement Service β€” none STANDARD OIDC
Keycloak Admin REST admin password grant bootstrap (one-shot) admin-cli personal (test users) STANDARD KEYCLOAK

Clients

Client Type Grants Flow override Used for
wifi-portal-api confidential password, client_credentials built-in direct grant Customer login, registration
wifi-portal-employee-api confidential password direct_grant β†’ employee-direct-grant Employee verification
wifi-portal public (standard flow, unused by the portal) β€” Retained for reference only
wifi-portal-employee public (standard flow, unused by the portal) browser β†’ employee-browser Retained for reference only

Confidential, not public: a public client with Direct Access Grants lets anyone on the network replay the grant with no client authentication at all. The portal is server-side, so it can hold a secret.

4. Internal calls without an HTTP contract

From To Mechanism Note
Custom authenticator Keycloak user store UserProvider SPI Writes user_type, employee_verified, employee_ref, company. Never writes the CCCD.
Keycloak event log Keycloak events Records giw_correlation_id and giw_employee_ref only.

5. Callback / internal integration

One, inbound: POST /giw-admin/webhook/{company} in Β§2b. No message queue, no outbound callback β€” every other hop is a synchronous request the portal initiates.

The architecture review (ADR-009) requires that a production success path be confirmed by a backend callback, never by a browser redirect alone. That requirement does not bite yet β€” no payment or external campaign is in scope here. The webhook already meets the fields below except retry buffering: it is handled inline by Keycloak, so an event that arrives during a restart is lost rather than queued (T-WEBHOOK-DURABILITY). Any callback added later must arrive with:

Field Purpose
correlationId Ties the callback to the originating journey
subjectId Who the callback is about
sessionId Which access session it affects
Idempotency-Key / X-Event-Id Duplicate suppression β€” providers retry
HMAC signature over the raw body + timestamp Authenticity; Β±5 min window
Retry policy Provider-side, with the consumer returning 200 before async processing

6. Timeouts and retries

Call Timeout Attempts Retry safe?
Authenticator β†’ HR verify 3000 ms 2 Yes β€” read-only, no state change
Portal β†’ Galaxy ID, customer grants 5000 ms 1 No β€” re-authenticating is the user's job
Portal β†’ Loyalty 2000 ms 1 Yes β€” read-only. Short on purpose: it sits on the login path and is optional. Every failure degrades to "no tier", never an error
Portal β†’ Galaxy ID, employee grant 12000 ms 1 Yes β€” read-only. Must exceed hrTimeout Γ— hrAttempts, or the portal times out first and misattributes an HR fault to Galaxy ID
Portal β†’ Entitlement 5000 ms 1 Yes β€” idempotency key
Portal β†’ Session create 5000 ms 1 Yes β€” idempotency key
Portal β†’ Session grant 5000 ms 1 Yes β€” declarative desired state
Entitlement β†’ JWKS 4000 ms 1 + 1 forced refresh on unknown kid Yes

All values are POC ASSUMPTION. See API-OVERVIEW.md Β§7.

7. Rate limiting

Surface Limit Mechanism
HR verify β€” per employee record 5 failed attempts / 60 s Keyed by employeeId, not by caller address: Keycloak is the only caller, so a per-IP limit would throttle a whole cabin through one bucket. Successful verifications are not counted β€” they are not guesses
HR verify β€” global ceiling 600 req / 60 s runaway-loop guard only
Employee form 5 submissions per authentication session authenticator config
Keycloak login 5 failures then backoff to 900 s realm brute-force protection
Quick-login burst 2 failures inside 200 ms β†’ 60 s hold realm quickLoginCheckMilliSeconds. Was 1000 ms, which locked out a passenger who simply double-tapped Login.
Portal API none Gap β€” the portal fronts the brute-force surface now
Entitlement, Session none Gap β€” see API-REVIEW-CHECKLIST.md

GIW POC Identity Platform Β· local demo Β· not production Β· generated from the repository