Keycloak M2M Authentication — Architecture Documentation
1. Introduction and Goals
Section titled “1. Introduction and Goals”Business Context
Section titled “Business Context”The Keycloak M2M (Machine-to-Machine) Authentication component provides secure, self-hosted authentication for external organisations and systems to access CTN Association Register APIs without an interactive user login. It lets terminal operators, carriers, logistics portals, and IoT devices authenticate programmatically using the OAuth 2.0 client-credentials grant.
IdP choice. Keycloak is the chosen IdP for the entire CTN stack (see ADR-00002 — Preferred stack and ADR-00008 — External IdP federation strategy). The same Keycloak instance handles user authentication (with outward federation to Entra ID, eHerkenning or local accounts) and M2M client-credentials flows.
Primary Goals
Section titled “Primary Goals”- Self-hosted independence — full control over identity infrastructure; no dependency on US-based managed identity providers.
- Standard M2M authentication — OAuth 2.0 client-credentials, no custom flows.
- Multi-organisation support — one Keycloak realm serves all CTN participants; per-participant clients via Keycloak Organizations.
- Fine-grained API authorisation — scope- and role-based access control via Keycloak Authorization Services.
- Audit compliance — complete audit trail of M2M authentication and API access.
Quality Goals
Section titled “Quality Goals”- Security: RS256 JWT signature validation, token expiration enforcement, JWKS-based key management, HSM-backed signing keys.
- Performance: <200ms token issuance, <50ms token validation (with JWKS caching at the consumer).
- Reliability: 99.95% uptime for M2M authentication.
- Independence: self-hosted, EU-resident.
- Scalability: hundreds of concurrent M2M clients.
Stakeholders
Section titled “Stakeholders”- Terminal operators — accessing container tracking and ETA APIs.
- Carriers — updating transport status and arrival information.
- External portals — integrating CTN data into third-party platforms.
- System administrators — managing M2M client credentials and access control.
2. Architecture Constraints
Section titled “2. Architecture Constraints”Technical
Section titled “Technical”- OAuth 2.0 client-credentials — standard grant flow.
- JWT format — RS256-signed JSON Web Tokens (JWS).
- JWKS discovery — public-key retrieval for distributed token validation.
- Container deployment — Keycloak runs as an OCI image with a PostgreSQL backend.
Regulatory
Section titled “Regulatory”- Data sovereignty — self-hosted infrastructure for full data control.
- EU residency — Keycloak instance hosted in an EU region; no cross-border secret material.
- GDPR — audit logging and data-protection controls.
Integration
Section titled “Integration”- Quarkus compatibility — every CTN service uses
quarkus-oidcas resource server against this Keycloak. - Coexistence with user authentication — the same Keycloak realm hosts both human users (federated per ADR-00008) and M2M clients; flows do not interfere.
- Per-participant mapping — M2M clients are scoped to a Keycloak Organization, tying them to a specific participant in the ASR.
3. Solution Strategy
Section titled “3. Solution Strategy”Keycloak realm structure
Section titled “Keycloak realm structure”Realm: ctn├── Organizations (per participant — ADR-00003 / 00008)│ ├── acme-bv│ │ ├── users (federated from Acme's Entra, or local accounts)│ │ └── clients│ │ ├── acme-tms-m2m (M2M service account)│ │ └── acme-erp-m2m (M2M service account)│ ├── contargo│ │ └── ...│ └── ...├── Client scopes│ ├── api.access (basic API access)│ ├── endpoints.read│ ├── endpoints.register│ └── containers.read└── Roles ├── participant-admin └── participant-memberClient-credentials flow
Section titled “Client-credentials flow”1. M2M client → Keycloak: POST /realms/ctn/protocol/openid-connect/token grant_type=client_credentials Basic Auth: client_id : client_secret (or client_assertion JWT for higher assurance)
2. Keycloak → M2M client: access_token (JWT, RS256, ~1h TTL)
3. M2M client → CTN API: GET /v1/<resource> Authorization: Bearer <access_token>
4. CTN API (Quarkus + quarkus-oidc): - Fetch JWKS from Keycloak (cached ~10 min) - Validate signature (RS256) - Validate iss, aud, exp, nbf - Read scopes / roles from token
5. CTN API → ASR DB: resolve participant_id from token claims (the client's Keycloak Organization → ASR participant link, per ADR-00003)
6. CTN API: enforce participant-scoped authorisation, serve the request4. Building Block View
Section titled “4. Building Block View”Core components
Section titled “Core components”- Keycloak realm
ctn— single realm hosting users, clients and organisations. - Service accounts — one per M2M client, scoped to a participant via the Organization.
- Authorization Services — fine-grained permissions per resource / scope.
- JWKS endpoint —
https://<keycloak>/realms/ctn/protocol/openid-connect/certs. - Admin REST API — for ASR-fronted client lifecycle (create / rotate / revoke).
Supporting components
Section titled “Supporting components”- Quarkus
quarkus-oidcextension — resource-server validation in every CTN service. - Audit pipeline — Keycloak admin and login events shipped to the central log platform.
- Secret store — HSM-backed storage for Keycloak’s signing keys (HMAC + RSA).
5. Runtime View
Section titled “5. Runtime View”Token issuance
Section titled “Token issuance”sequenceDiagram
participant Client as M2M Client
participant KC as Keycloak
participant API as CTN API (Quarkus)
Client->>KC: POST /token (client_credentials, client_id+secret)
KC->>KC: Authenticate client
KC->>KC: Build access token (scopes, organisation claim)
KC-->>Client: access_token (JWT, RS256)
Resource access
Section titled “Resource access”sequenceDiagram
participant Client as M2M Client
participant API as CTN API (Quarkus)
participant KC as Keycloak (JWKS)
participant DB as ASR DB
Client->>API: GET /v1/<resource> (Bearer JWT)
API->>KC: GET /protocol/openid-connect/certs (cached)
KC-->>API: JWKS
API->>API: Validate signature, iss, aud, exp
API->>API: Read scopes + organisation claim
API->>DB: Resolve participant_id → check policy
DB-->>API: Allow / Deny
alt Allowed
API-->>Client: 200 OK + data
else Denied
API-->>Client: 403 Forbidden
end
Access-token structure (example)
Section titled “Access-token structure (example)”{ "iss": "https://keycloak.ctn.network/realms/ctn", "sub": "service-account-acme-tms-m2m", "aud": ["ctn-api"], "exp": 1730906400, "iat": 1730902800, "azp": "acme-tms-m2m", "client_id": "acme-tms-m2m", "scope": "openid api.access containers.read", "organization": { "acme-bv": { "id": "11111111-2222-3333-4444-555555555555" } }, "realm_access": { "roles": ["participant-member"] }}The organization claim’s exact JSON shape (alias-as-key vs. predictable schema) is the subject of ADR-00006 — Token Claim Composition. Until that ADR closes, consumers should treat the field as opaque and resolve to a participant ID via the ASR’s /participant/context endpoint when needed.
6. Deployment View
Section titled “6. Deployment View”Reference deployment (Azure)
Section titled “Reference deployment (Azure)”- Compute: Azure Kubernetes Service (AKS) running the
quay.io/keycloak/keycloak:<version>image. - Database: Azure Database for PostgreSQL — Flexible Server.
- Secret store: Azure Key Vault (Managed HSM tier for signing keys).
- Ingress: Azure Front Door with WAF.
Cloud-agnostic alternatives
Section titled “Cloud-agnostic alternatives”| Component | Azure (reference) | AWS | Self-hosted |
|---|---|---|---|
| Compute | AKS (Kubernetes) | EKS | Kubernetes / Nomad |
| Database | PostgreSQL Flexible Server | RDS for PostgreSQL | Patroni / Crunchy |
| Secret store | Key Vault Managed HSM | KMS + Secrets Manager | HashiCorp Vault |
| Ingress | Front Door | CloudFront + ALB | Traefik / NGINX |
7. Security Considerations
Section titled “7. Security Considerations”- Client secrets: minimum 256-bit entropy; rotated on a scheduled cadence (90 days target). Prefer
client_assertion(JWT-bearer) over static secrets where the M2M client can hold a private key. - Scopes: every M2M client is granted the minimum scope set needed for its data products. Default-deny; explicit grants per participant.
- Token lifetime: access tokens 1 hour; no refresh tokens for client-credentials clients (per OAuth 2.0).
- Audit logging: every token issuance and every admin action emits a Keycloak event; events ship to the central log platform with a
participant_idcorrelation. - JWKS hardening: signing keys live in the HSM-backed key vault; rotation through Keycloak’s key-management UI.
8. Operations
Section titled “8. Operations”Provisioning a new M2M client (ASR-fronted)
Section titled “Provisioning a new M2M client (ASR-fronted)”- Participant admin calls the ASR endpoint (
POST /v1/participants/{id}/m2m-clients) — see ADR-00007. - ASR validates the request (participant exists, requester is admin) and calls Keycloak Admin REST API with its service-account credentials.
- Keycloak creates the client under the participant’s Organization with the requested scopes.
- Keycloak returns the freshly-generated client secret.
- ASR returns the secret to the caller once (it is not retrievable afterwards).
- The participant admin distributes the secret to the M2M system through their own secure channel.
Rotating a client secret
Section titled “Rotating a client secret”POST /v1/participants/{id}/m2m-clients/{clientId}/rotate-secretReturns a new secret, valid immediately; the previous secret is invalidated after a grace period (default 24h) to allow rolling deploys.
Revoking a client
Section titled “Revoking a client”DELETE /v1/participants/{id}/m2m-clients/{clientId}The client is disabled in Keycloak; outstanding tokens remain valid until expiry (max 1h). Token revocation can be forced via the Keycloak admin event LOGOUT_ALL, but that affects every session.
Error contract
Section titled “Error contract”M2M error responses follow RFC 9457 (Problem Details), consistent with the coding standards. The three authentication/authorisation failures a client must handle:
401 — authentication failed. Returned with a WWW-Authenticate: Bearer header per OAuth 2.0. Causes: expired token, invalid RS256 signature, issuer mismatch (iss), or a malformed Authorization header. Action: request a new access token; if it persists, verify the configured issuer matches the token.
{ "type": "https://schemas.ctn.network/problems/unauthorized", "title": "Unauthorized", "status": 401, "detail": "Invalid or expired token"}403 — insufficient scope. The token is valid but lacks a required scope or role. Scope names are case-sensitive and must match exactly. Action: grant the missing scope to the client in Keycloak.
{ "type": "https://schemas.ctn.network/problems/insufficient-scope", "title": "Insufficient scope", "status": 403, "detail": "Missing required scope: containers.read", "required_scopes": ["containers.read"], "missing_scopes": ["containers.read"]}403 — client not linked to a participant. Authentication succeeded but the client’s Keycloak Organization is not linked to an active ASR participant, so no participant context can be resolved (see ADR-00003). A participant_id correlation is logged. Action: link the Organization to a participant in the ASR.
{ "type": "https://schemas.ctn.network/problems/participant-not-mapped", "title": "Participant not mapped", "status": 403, "detail": "No active ASR participant is linked to this client's Keycloak Organization"}Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Check |
|---|---|---|
| 401 “issuer mismatch” | Configured OIDC issuer ≠ token iss | Confirm quarkus.oidc.auth-server-url resolves to https://<keycloak>/realms/ctn; no trailing slash. |
| 401 “JWKS fetch failed” | API cannot reach Keycloak certs endpoint | Network/egress to /realms/ctn/protocol/openid-connect/certs; no firewall block on outbound HTTPS. |
| 403 after successful auth | Client’s Organization not linked to a participant | Resolve the Organization claim → ASR participant; verify the participant is active. |
| 403 “missing scope” | Scope/role not assigned, or case mismatch | Token scope / realm_access.roles contains the exact (case-sensitive) value; re-issue token after granting. |
9. Quality Requirements
Section titled “9. Quality Requirements”| Quality | Target |
|---|---|
| Token issuance latency | <200ms (P95) |
| Token validation latency | <50ms (P95) with JWKS cached |
| Availability | 99.95% |
| Audit completeness | 100% of token issuance and admin actions logged |
| Secret rotation cadence | ≤90 days |
10. Risks and Technical Debt
Section titled “10. Risks and Technical Debt”| Risk | Mitigation |
|---|---|
| Compromise of the Keycloak admin service account → full realm takeover | Least-privilege service-account role; secrets in HSM-backed store; quarterly access review. |
| Stolen client secret used elsewhere | Short token TTL; per-client audit; offer client_assertion (JWT-bearer) for high-value M2M flows. |
| JWKS cache TTL too long → revoked keys still accepted | Standard JWKS cache TTL of 10 minutes at consumers; emergency JWKS push via cache invalidation event. |
| Keycloak Admin API list calls are capped (≈100 organisations, 100 members, 1000 admin events per call) | Add pagination before scaling past these thresholds; log a truncation warning when a cap is hit. |
| Provisioning is best-effort compensation, not a durable saga (no idempotency keys, no retry/backoff on Admin API calls, no drift reconciliation) | Acceptable at current volume; track as TD — add idempotency keys, a retry/backoff wrapper, and a periodic Keycloak↔ASR reconciliation job when volume grows. |
11. Related Documents
Section titled “11. Related Documents”- ADR-00002 — Define a preferred stack
- ADR-00003 — Separate users and participants
- ADR-00006 — Token Claim Composition
- ADR-00007 — Identity Mutation Proxy
- ADR-00008 — External IdP Federation Strategy
- Centralised Authentication
- ASR Data Model — the ASR-side
m2m_client_refrecord links to the Keycloak client by ID; the secret stays in Keycloak. - Keycloak documentation: https://www.keycloak.org/documentation
In samenwerking met




