API Overview β GIW POC Identity Platform
Status: POC Β· Version: 0.1.0 Β· Date: 2026-09-21
Spec: openapi.yaml (OpenAPI 3.0.3, validated)
1. What this platform proves
Four things, end to end, on a laptop:
- A customer can register, log in and log out against Keycloak, and receive an OIDC session.
- An employee can be authenticated by Employee ID + CCCD, where Keycloak β not the portal β calls the HR Verification API and grants a session only on MATCH + ACTIVE.
- After either path, the Wi-Fi Portal obtains a decision from the Entitlement Service and only then asks the Session Service to open access.
- The CCCD never reaches a token, a log, a database row or an error message.
2. API architecture β the portal owns the UI
There is no Keycloak-rendered page anywhere in this platform. The Wi-Fi Portal serves its own screens and calls Galaxy ID over the token and admin APIs.
Browser βββΆ Portal API βββ¬βββΆ Galaxy ID (Keycloak) authenticate
(sid cookie) ββββΆ Entitlement Service decide
ββββΆ Session Service enforce
β
Keycloak βββββββ΄βββΆ HR Verification API
The browser talks to exactly one host β the portal β and holds one opaque cookie. It never sees a token, a Keycloak URL, or the address of any downstream system.
Why not the redirect flow
An Authorization Code redirect is the textbook answer, and it is the wrong one here:
- The captive network assistant (the mini-browser iOS and macOS pop up on a captive Wi-Fi) handles redirects to a third-party origin badly and does not persist cookies reliably. A redirect-based login fails in exactly the place this product lives.
- Brand continuity: bouncing a Vietjet passenger to a Keycloak page, then back, is a visible seam in a 30-second journey.
What that costs, stated plainly
| Lost | Consequence |
|---|---|
| SSO across Galaxy properties | Each portal login is independent |
| MFA / step-up | No standard way to challenge inside a password grant |
| IdP federation (corporate, social) | Would need a redirect path added back |
| Credentials out of the portal's hands | The portal is now in credential-handling scope for audit |
This is not a blessed pattern. RFC 9700 (OAuth 2.0 Security Best Current Practice) states the password grant MUST NOT be used, and it records no first-party exception. Direct Grant is used only as a constrained POC workaround for captive-portal UX validation. Production authentication architecture remains TBC and should prefer modern browser-based OIDC/federation where operationally feasible.
What did not change, and must not:
- Keycloak still makes every authentication decision. The portal has no password store and no HR credential.
- Keycloak still calls HR for employee verification, through a custom direct-grant authenticator. The portal only relays two form fields.
- Tokens stay server-side.
Standard vs Galaxy-built
| Leg | Nature | Specified where |
|---|---|---|
| Browser β Wi-Fi Portal | Galaxy-built | openapi.yaml β /api/v1/auth/* |
| Wi-Fi Portal β Galaxy ID | STANDARD OAuth 2.0 / OIDC β RFC 6749 Β§4.3 password grant, RFC 6749 Β§4.4 client credentials, RFC 7009 logout, Keycloak Admin REST | Keycloak's discovery document. Not re-specified in openapi.yaml. |
| Keycloak β HR Verification API | Galaxy-built | openapi.yaml β POST /api/v1/employees/verify |
| Wi-Fi Portal β Entitlement Service | Galaxy-built | openapi.yaml β POST /api/v1/entitlements/evaluate |
| Wi-Fi Portal β Session Service | Galaxy-built | openapi.yaml β /api/v1/sessions* |
Standard endpoints in use, reference only:
| Purpose | Endpoint (realm galaxy-id-poc) |
Client |
|---|---|---|
| Customer login | POST /protocol/openid-connect/token grant_type=password |
wifi-portal-api |
| Employee verification | POST /protocol/openid-connect/token grant_type=password + employeeId + citizenId |
wifi-portal-employee-api |
| Refresh | POST /protocol/openid-connect/token grant_type=refresh_token |
either |
| Service token (registration) | POST /protocol/openid-connect/token grant_type=client_credentials |
wifi-portal-api |
| Create account | POST /admin/realms/{realm}/users |
service account, manage-users only |
| Back-channel logout | POST /protocol/openid-connect/logout |
either |
| JWKS | GET /protocol/openid-connect/certs |
Entitlement Service |
The employee flow has no special endpoint. It is the ordinary token endpoint on a second confidential client whose direct_grant flow is overridden to employee-direct-grant. Keycloak's authenticator reads employeeId and citizenId from the request and calls HR.
3. Separation of concerns
Keycloak identity and authentication WHO you are
HR Verification employment status ARE you staff, and active
Entitlement access decision WHAT you may have
Session / Network access enforcement TURN IT ON
Two rules follow, and both are enforced in code, not just prose:
- Keycloak never opens the network. It issues a token and stops. The portal must go and get an entitlement.
- Session Service never decides.
POST /sessions/{id}/grantreturns422 ENTITLEMENT_DENIEDif noentitlementIdis supplied (ADR-008).
4. Components
| Service | Port | Stack | Role |
|---|---|---|---|
keycloak |
8080 (9000 health) | Keycloak 26.7.4 + custom Java SPI | Galaxy ID. Both a browser flow and a direct-grant flow for employees |
postgres-keycloak |
β | Postgres 16 | Keycloak persistence |
mock-hr-api |
3001 | Node 22, zero deps | Stands in for the HR system of record |
entitlement-service |
3002 | Node 22, zero deps | Verifies the token, applies policy, issues entitlements |
session-service |
3003 | Node 22, zero deps | Models the Network Adapter verbs from ADR-007 |
wifi-portal |
3000 | Node 22, zero deps | Serves the UI, calls Galaxy ID, orchestrates entitlement and session |
bootstrap |
β | Node 22 | One-shot: client scope, flow binding, test users |
Node services carry no npm dependencies on purpose. A POC that handles a CCCD should be auditable line by line, without a transitive dependency tree.
5. Identity linking β POC strategy
When an employee authenticates, the custom authenticator resolves a Keycloak user in this order:
- A user carrying the attribute
employee_refequal to the HR reference. - Otherwise a user whose username is the lowercased reference (
emp-12345). - Otherwise a new user is provisioned.
If step 1 finds more than one user sharing an employee_ref, the flow fails with IDENTITY_LINK_CONFLICT rather than guessing.
Deliberately not implemented: automatic merge with a pre-existing CUSTOMER account matched by email or phone. Silently merging two identities is irreversible and is a production decision, not a POC convenience. Tracked as T-LINK in Β§8.
Production: TBC. Requires a decision on whether Galaxy ID holds one subject with two roles, or two linked subjects. Relates to ADR-002.
6. Correlation
Every request carries or generates X-Correlation-ID, and it is echoed on every response. One id spans:
Portal β Keycloak β Custom Authenticator β HR API β Entitlement β Session
It is generated as corr-<uuid4>. It is never derived from a CCCD, an employee id or any personal data β a correlation id ends up in logs and dashboards by design.
7. POC assumptions
Everything in this table is a guess made to get the stack running. None is a production SLA.
| Assumption | Value | Where set |
|---|---|---|
| HR verify timeout | 3000 ms | GIW_HR_TIMEOUT_MS |
| HR call attempts | 2 (one retry) | GIW_HR_MAX_ATTEMPTS |
| Employee entitlement TTL | 4 h | TTL_EMPLOYEE_SEC |
| Customer entitlement TTL | 1 h | TTL_CUSTOMER_SEC |
| Access token lifespan | 900 s | realm config |
| Portal β Galaxy ID timeout | 5000 ms | idp.js |
| Portal β Galaxy ID timeout, employee grant | 12000 ms | IDP_EMPLOYEE_TIMEOUT_MS. Must exceed hrTimeoutMs Γ hrMaxAttempts, or the portal times out first and blames Galaxy ID for an HR fault. |
| Portal session TTL | 3600 s, reaped every 5 min | server.js |
| SSO idle / max | 1800 s / 36000 s | realm config |
| Form submissions per auth session | 5 | authenticator config |
| HR rate limit | 30 req / 60 s per peer | RATE_LIMIT_MAX |
| Eligible companies | VIETJET, GALAXY, SOVICO | EMPLOYEE_COMPANIES |
8. Open questions
| Ref | Question | Blocks |
|---|---|---|
| T-CCCD | Is CCCD acceptable as a production verification input at all? ADR-003 and decision D3 currently say remove it; this POC implements it because the task order asked for it. | Production employee flow |
| T-LINK | One subject with two roles, or two linked subjects, when an employee is also a customer? | ADR-002, user model |
| T-TIER | Are BASIC/BOOST the real tiers, and is the lever duration or speed? | ADR-011, entitlement catalogue |
| T-HR-SOR | Which system is the real HR system of record, and what is its actual contract, auth model and SLA? | Everything in Keycloak β HR |
| T-AUD | Should each service get its own audience rather than accepting account? |
API-SECURITY.md Β§3 |
| T-GRANT | Is the password grant acceptable long term, or should the portal move to a device/CIBA flow, or add a redirect path for corporate federation? | Β§2 above, ADR-002 |
| T-MFA | If MFA is ever required, the password grant cannot carry a step-up. What replaces it? | Β§2 above |
| T-STORE | Entitlement and session state are in memory. What is the production store and its retention? | DATA-CLASSIFICATION.md |
9. Reading order for reviewers
- This document
API-MATRIX.mdβ everything on one pageAPI-SEQUENCES.mdβ the three flows as diagramsTOKEN-CLAIMS.mdβ the token contractDATA-CLASSIFICATION.mdβ where every field lives and diesAPI-SECURITY.mdβ controls, and what is missingERROR-CATALOG.mdAPI-REVIEW-CHECKLIST.mdβ bring your objections here