Skip to content
Deze documentatie wordt actief uitgebreid — kom regelmatig terug.

Keycloak M2M Authentication — Architecture Documentation

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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • Quarkus compatibility — every CTN service uses quarkus-oidc as 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.
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-member
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 request
  • 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 endpointhttps://<keycloak>/realms/ctn/protocol/openid-connect/certs.
  • Admin REST API — for ASR-fronted client lifecycle (create / rotate / revoke).
  • Quarkus quarkus-oidc extension — 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).
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)
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
{
"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.

  • 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.
ComponentAzure (reference)AWSSelf-hosted
ComputeAKS (Kubernetes)EKSKubernetes / Nomad
DatabasePostgreSQL Flexible ServerRDS for PostgreSQLPatroni / Crunchy
Secret storeKey Vault Managed HSMKMS + Secrets ManagerHashiCorp Vault
IngressFront DoorCloudFront + ALBTraefik / NGINX
  • 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_id correlation.
  • JWKS hardening: signing keys live in the HSM-backed key vault; rotation through Keycloak’s key-management UI.

Provisioning a new M2M client (ASR-fronted)

Section titled “Provisioning a new M2M client (ASR-fronted)”
  1. Participant admin calls the ASR endpoint (POST /v1/participants/{id}/m2m-clients) — see ADR-00007.
  2. ASR validates the request (participant exists, requester is admin) and calls Keycloak Admin REST API with its service-account credentials.
  3. Keycloak creates the client under the participant’s Organization with the requested scopes.
  4. Keycloak returns the freshly-generated client secret.
  5. ASR returns the secret to the caller once (it is not retrievable afterwards).
  6. The participant admin distributes the secret to the M2M system through their own secure channel.
POST /v1/participants/{id}/m2m-clients/{clientId}/rotate-secret

Returns a new secret, valid immediately; the previous secret is invalidated after a grace period (default 24h) to allow rolling deploys.

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.

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"
}
SymptomLikely causeCheck
401 “issuer mismatch”Configured OIDC issuer ≠ token issConfirm quarkus.oidc.auth-server-url resolves to https://<keycloak>/realms/ctn; no trailing slash.
401 “JWKS fetch failed”API cannot reach Keycloak certs endpointNetwork/egress to /realms/ctn/protocol/openid-connect/certs; no firewall block on outbound HTTPS.
403 after successful authClient’s Organization not linked to a participantResolve the Organization claim → ASR participant; verify the participant is active.
403 “missing scope”Scope/role not assigned, or case mismatchToken scope / realm_access.roles contains the exact (case-sensitive) value; re-issue token after granting.
QualityTarget
Token issuance latency<200ms (P95)
Token validation latency<50ms (P95) with JWKS cached
Availability99.95%
Audit completeness100% of token issuance and admin actions logged
Secret rotation cadence≤90 days
RiskMitigation
Compromise of the Keycloak admin service account → full realm takeoverLeast-privilege service-account role; secrets in HSM-backed store; quarterly access review.
Stolen client secret used elsewhereShort token TTL; per-client audit; offer client_assertion (JWT-bearer) for high-value M2M flows.
JWKS cache TTL too long → revoked keys still acceptedStandard 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.

In samenwerking met

Connected Trade NetworkConclusionData in LogisticsContargoInland Terminals GroupVan Berkel