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:

  1. A customer can register, log in and log out against Keycloak, and receive an OIDC session.
  2. 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.
  3. After either path, the Wi-Fi Portal obtains a decision from the Entitlement Service and only then asks the Session Service to open access.
  4. 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}/grant returns 422 ENTITLEMENT_DENIED if no entitlementId is 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:

  1. A user carrying the attribute employee_ref equal to the HR reference.
  2. Otherwise a user whose username is the lowercased reference (emp-12345).
  3. 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

  1. This document
  2. API-MATRIX.md β€” everything on one page
  3. API-SEQUENCES.md β€” the three flows as diagrams
  4. TOKEN-CLAIMS.md β€” the token contract
  5. DATA-CLASSIFICATION.md β€” where every field lives and dies
  6. API-SECURITY.md β€” controls, and what is missing
  7. ERROR-CATALOG.md
  8. API-REVIEW-CHECKLIST.md β€” bring your objections here

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