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

Cross-Cutting Concepts

Technical concepts and patterns applied across the Connected Trade Network

Authentication Methods (per ADR-00002 and ADR-00008):

Machine-to-Machine (M2M):
├── Keycloak OAuth 2.0 Client Credentials
│ ├── Self-hosted IdP (the chosen one)
│ ├── RS256 JWT signing
│ ├── JWKS-based validation
│ └── Participant-scoped authorisation (Keycloak Organizations)
└── mTLS (optional high security)
User Authentication (single IdP, outward federation per participant):
├── Keycloak realm `ctn`
│ ├── Federation → Entra ID (per participant, OIDC)
│ ├── Federation → eHerkenning (SAML→OIDC, for NL admin-representation)
│ └── Local Keycloak account (email + password + MFA) for participants without an own IdP
└── MFA enforced on admin surfaces

Authorisation Pattern (2026) — VAD-only:

Request Authorisation Flow:
1. Validate VAD token (participation proof) against ASR JWKS
2. Apply provider's local policy
3. Make authorisation decision
4. Audit the decision
HTTP Headers:
Authorization: Bearer <VAD-token>
X-Correlation-Id: <request-id>

Keycloak password policy (local accounts). Local Keycloak accounts enforce a password policy aligned with NIST SP 800-63B: minimum length 12, at least one each of upper/lower/digit/special, password history 10, max length 128 (DoS guard), PBKDF2-SHA256 ≥210 000 iterations, and no periodic forced expiry (change only on suspected breach). Federated users (Entra ID, eHerkenning) are governed by their own IdP’s policy.

VAD Token Structure:

{
"iss": "https://association.ctn.network",
"sub": "org-client-id",
"aud": "https://ctn.network",
"iat": 1704063600,
"exp": 1704150000,
"jti": "unique-token-id",
"https://schemas.ctn.network/claims/legal_entity/name": "Transport Corp",
"https://schemas.ctn.network/claims/legal_entity/kvk": "12345678",
"https://schemas.ctn.network/claims/compliance/verified": true,
"https://schemas.ctn.network/claims/participation/status": "active",
"https://schemas.ctn.network/claims/identity_assurance_level": "substantial",
"https://schemas.ctn.network/claims/participation/valid_until": "2026-12-31"
}

M2M Token Structure (Keycloak):

{
"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"]
}
}

M2M Authentication Flow (full detail in Keycloak M2M Authentication):

1. M2M Client → Keycloak: POST /realms/ctn/protocol/openid-connect/token
- grant_type=client_credentials
- client_id + client_secret (Basic Auth) — or client_assertion JWT
2. Keycloak → M2M Client: JWT access token (RS256, ~1h TTL)
3. M2M Client → API: HTTP request, Authorization: Bearer <JWT>
4. API (quarkus-oidc) → Keycloak: Retrieve JWKS (cached ~10 minutes)
5. API: Validate JWT signature, iss, aud, exp, nbf, scopes
6. API → ASR DB: Resolve participant_id from token's organization claim
7. API: Apply participant-scoped policy, serve the request

Data Protection:

ContextMethodKey Management
Data at RestAES-256-GCMAzure Key Vault
Data in TransitTLS 1.3Managed certificates
Token SigningRS256 (RSA + SHA-256)HSM-backed keys
Sensitive FieldsField-level encryptionApplication keys
BackupsAzure Storage encryptionMicrosoft-managed

RESTful Principles:

URL Structure:
Pattern: /{version}/{resource}/{id}/{sub-resource}
Example: /v1/participants/12345/endpoints
HTTP Methods:
GET: Retrieve resource(s)
POST: Create new resource
PUT: Full update (replace)
PATCH: Partial update
DELETE: Remove resource
Status Codes:
200: Success
201: Created
204: No Content
400: Bad Request
401: Unauthorized
403: Forbidden
404: Not Found
409: Conflict
429: Too Many Requests
500: Internal Server Error

REST Maturity - Level 3 (HATEOAS):

CTN implements REST Level 3, using hypermedia controls to guide API consumers through available operations. This means API responses include _links that tell clients what actions are possible based on the current resource state.

📖 A separate REST Level 3 / HATEOAS explainer is planned; until then the Quick Example below is the canonical reference.

Quick Example:

{
"memberId": "ITG001",
"status": "active",
"_links": {
"self": { "href": "/v1/participants/ITG001" },
"requestVadToken": {
"href": "/v1/participants/ITG001/tokens/vad",
"method": "POST"
},
"updateProfile": {
"href": "/v1/participants/ITG001",
"method": "PATCH"
}
}
}

Benefits:

  • Self-documenting APIs
  • Workflow guidance through state-dependent links
  • Loose coupling (clients follow links, don’t construct URLs)
  • Graceful evolution without breaking clients

OpenAPI Specification:

openapi: 3.0.3
info:
title: CTN Association Register API
version: 1.0.0
paths:
/v1/participants/{participantId}/endpoints:
post:
summary: Register a new data endpoint for a participant
parameters:
- name: participantId
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterEndpointRequest'
responses:
201:
description: Endpoint registered
headers:
Location:
schema: { type: string }
description: URI of the created endpoint

Rate limiting is applied in two layers. The edge layer (Azure Front Door / WAF, see Front Door with WAF) absorbs volumetric abuse before it reaches the API. The application layer enforces finer, purpose-specific limits per caller.

Application-layer keys: authenticated requests are limited per caller identity (participant/user), anonymous requests per source IP. Limits are tiered by endpoint sensitivity:

TierLimit (reference)Applies to
API100 req / minGeneral read/write endpoints
Auth10 req / minInteractive authentication endpoints
Token5 req / hourToken issuance
Failed auth5 attempts / hour (per IP)Repeated authentication failures
Upload20 req / hourFile-upload endpoints

Failed authentication attempts carry an extra penalty so brute-force attempts exhaust their budget faster than the nominal limit suggests.

Responses advertise the budget via standard headers (IETF RateLimit headers): RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset. On exceedance the API returns 429 with Retry-After and an RFC 9457 problem body:

{
"type": "https://schemas.ctn.network/problems/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded; retry after 60 seconds",
"retry_after": 60
}

Consumers must honour Retry-After and back off exponentially. Limits are configurable per environment (relaxed in non-production). In-process counters do not share state across instances, so a multi-instance deployment uses a shared backing store (e.g. Redis) or delegates distributed limiting to the API gateway.

Standard Error Response:

{
"error": {
"code": "CTN_AUTH_002",
"message": "VAD token validation failed",
"details": {
"reason": "Token expired",
"expired_at": "2026-05-29T10:00:00Z"
},
"timestamp": "2026-05-29T11:00:00Z",
"correlation_id": "req-abc-123",
"documentation": "https://docs.ctn.network/errors/CTN_AUTH_002"
}
}

Error Categories:

Code RangeCategoryExamples
CTN_VAL_*ValidationInvalid input, missing fields
CTN_AUTH_*AuthenticationInvalid token, expired
CTN_AUTHZ_*AuthorizationInsufficient permissions
CTN_BUS_*Business LogicInvalid state, rule violation
CTN_INT_*IntegrationExternal service failure
CTN_SYS_*SystemInternal error, resource unavailable

Structured Logging:

{
"timestamp": "2026-05-29T10:30:45.123Z",
"level": "INFO",
"correlation_id": "req-abc-123",
"service": "association-register",
"operation": "registerEndpoint",
"duration_ms": 145,
"user_id": "user-456",
"participant_id": "PARTICIPANT-789",
"endpoint_id": "ENDPOINT-012",
"result": "SUCCESS",
"metadata": {
"participation_status": "active",
"verification_level": "FULL"
}
}

Logging Levels:

  • ERROR: System errors, failures requiring attention
  • WARN: Degraded performance, potential issues
  • INFO: Normal operations, key business events
  • DEBUG: Detailed diagnostic information

Sensitive data — never log. Logs are an exfiltration surface and fall under GDPR. The following must never appear in log output, in any field:

  • Passwords and password hashes
  • Full API tokens or JWTs (log at most a short prefix, e.g. first 8 chars + )
  • Client secrets and signing keys
  • Full personal data (BSN/SSN, payment details, health data)

Safe to log: participant IDs and user UUIDs (not emails in security contexts), token prefixes, hashed values, IP addresses (for security monitoring) and user-agent strings. PII that must be correlated is masked or tokenised before logging.

Security-event logging. Authentication and authorisation outcomes are logged as distinct, queryable security events — not folded into generic request logs — so they can drive alerting:

  • Authentication failure — reason (e.g. invalid signature, expired token), source IP, correlation ID. No token contents.
  • Authorisation failure — subject, required vs. granted scopes/roles, the resource, correlation ID.
  • Suspicious activity — repeated failures from one source, token reuse from multiple IPs, anomalous request rates.
{
"timestamp": "2026-05-29T10:31:02.500Z",
"level": "WARN",
"event": "security.authn.failure",
"correlation_id": "req-abc-123",
"reason": "invalid_token_signature",
"ip": "203.0.113.7"
}

These events feed the audit pipeline and the alerting rules in the Deployment View.

Metrics Collection:

Business Metrics:
├── participants.onboarded (counter)
├── endpoints.registered (counter)
├── vad.tokens.issued (counter)
├── verifications.completed (counter)
└── re_verifications.scheduled (counter)
Technical Metrics:
├── api.response.time (histogram)
├── database.query.time (histogram)
├── cache.hit.ratio (gauge)
├── external_api.latency (histogram, per provider: KvK / KBO / VIES / GLEIF)
└── error.rate (counter)

Multi-Level Cache:

Level 1: CDN Cache
├── Static content (Portal assets)
├── Public API responses
└── TTL: 24 hours
Level 2: Consumer-side JWKS cache
├── Public keys of the ASR signing certificate
├── Refreshed on key-rotation events
└── TTL: 10 minutes
Level 3: Application Cache (in-process)
├── KvK / KBO / VIES / GLEIF lookup results
├── Reference data
└── TTL: 5 minutes (lookups), 24 hours (reference)

Cache Invalidation:

  • Event-driven invalidation
  • TTL-based expiration
  • Manual purge capabilities
  • Cache-aside pattern for consistency

Event Schema (CloudEvents):

{
"specversion": "1.0",
"type": "org.ctn.participant.verified",
"source": "/association-register",
"id": "evt-123",
"time": "2026-05-29T10:30:00Z",
"datacontenttype": "application/json",
"data": {
"participant_id": "PARTICIPANT-789",
"verification_source": "KvK",
"verified_at": "2026-05-29T10:29:55Z"
}
}

Event Topics:

Participation Events:
├── participant.registered
├── participant.verified
├── participant.re_verification_due
├── participant.suspended
└── participant.offboarded
Endpoint Events:
├── endpoint.registered
├── endpoint.suspended
└── endpoint.retired
Authorisation Events (at data provider, optional):
├── access.granted
└── access.denied

Circuit Breaker (Quarkus / SmallRye Fault Tolerance):

@CircuitBreaker(
requestVolumeThreshold = 10,
failureRatio = 0.5,
delay = 30, delayUnit = ChronoUnit.SECONDS,
successThreshold = 5
)
public Uni<KvkProfile> verifyKvk(String kvkNumber) {
return kvkClient.basisprofiel(kvkNumber);
}

Retry Policy:

Retry Configuration:
MaxAttempts: 3
BackoffStrategy: Exponential
InitialDelay: 1s
MaxDelay: 30s
RetryableErrors:
- NetworkError
- TimeoutError
- ServiceUnavailable

Data Classification:

LevelDescriptionExamplesProtection
PublicOpen informationAPI docs, schemasNone required
InternalBusiness operationsParticipant records, endpoint metadataAccess control
ConfidentialSensitive businessPricing, contractsEncryption + audit
RestrictedRegulated dataPersonal data, UBOFull protection

Data Retention:

Retention Policies:
├── Operational Data: 90 days active + 1 year archive
├── Audit Logs: 7 years (regulatory requirement)
├── Tokens: Duration of validity + 30 days
├── Personal Data: As per GDPR requirements
└── Backups: 30 days rolling window

Validation Rules:

  • Input validation at API gateway
  • Schema validation for events
  • Business rule validation in services
  • Referential integrity in database

Data Consistency:

  • ACID transactions for critical operations
  • Eventual consistency for distributed data
  • Idempotency keys for duplicate prevention
  • Compensating transactions for rollbacks
-- Indexes for common queries
CREATE INDEX idx_participant_status ON participant(kvk, status) WHERE status = 'ACTIVE';
CREATE INDEX idx_endpoint_participant ON endpoint(participant_id);
CREATE INDEX idx_identifier_lookup ON legal_entity_identifier(identifier_type, identifier_value);
-- Partitioning for large tables (yearly)
CREATE TABLE audit_log_2026 PARTITION OF audit_log
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
  • Response compression (gzip)
  • Pagination for large datasets
  • Field filtering (?fields=id,name)
  • ETag support for caching
  • Connection pooling
{
"audit_record": {
"id": "audit-123",
"timestamp": "2026-05-29T10:30:00Z",
"actor": {
"id": "user-456",
"type": "SERVICE_ACCOUNT",
"ip_address": "10.0.2.15"
},
"action": "DATA_ACCESS",
"resource": {
"type": "CONTAINER_ETA",
"id": "CNT-ABC-123"
},
"authorisation": {
"vad_token_id": "token-789",
"decision": "ALLOW"
},
"metadata": {
"correlation_id": "req-abc-123",
"session_id": "sess-def-456"
}
}
}
  • Right to access (data export)
  • Right to rectification (data updates)
  • Right to erasure (data deletion)
  • Data portability (standard formats)
  • Privacy by design principles

These cross-cutting concepts ensure consistency and quality across all CTN components.

In samenwerking met

Connected Trade NetworkConclusionData in LogisticsContargoInland Terminals GroupVan Berkel