| name | ls-iam |
| description | OAuth, OpenID Connect, AuthZEN, SAML voor authenticatie en autorisatie bij de overheid. NL GOV profielen, JWT, PKCE, private_key_jwt, OIN authenticatie. |
| model | sonnet |
| allowed-tools | ["Bash(gh api *)","Bash(gh issue list *)","Bash(gh pr list *)","Bash(gh search *)","Bash(curl -s *)","WebFetch(*)"] |
| metadata | {"created-with-ai":"true","created-with-model":"claude-opus-4-20250514","created-date":"2025-02-12","status":"concept"} |
CONCEPT — Let op: Deze skill is geen officieel product van Logius. De beschrijvingen zijn informatieve samenvattingen — niet de officiële standaarden zelf. De definities op forumstandaardisatie.nl en Logius zijn altijd leidend. Overheidsorganisaties die generatieve AI inzetten dienen te voldoen aan het Overheidsbreed standpunt voor de inzet van generatieve AI. Zie DISCLAIMER.md en onze verantwoording.
Identity & Access Management (IAM)
Agent-instructie: Deze skill helpt bij het implementeren van authenticatie en autorisatie voor overheids-APIs. Gebruik de OAuth 2.0 NL en OIDC NL GOV profielen voor nieuwe implementaties. AuthZEN voor geëxternaliseerde autorisatie.
De IAM-standaarden van Logius definiëren profielen voor authenticatie en autorisatie bij Nederlandse overheids-APIs. Dit omvat OAuth 2.0, OpenID Connect, AuthZEN en aanvullende profielen specifiek voor de Nederlandse overheid. Deze standaarden waarborgen een consistent en veilig identiteits- en toegangsbeheer binnen het publieke domein.
Versiemodel
Net als andere Logius-standaarden kennen deze standaarden twee publicatiekanalen (vergelijkbaar met W3C):
- Vastgestelde versie (DEF): officieel goedgekeurd, gepubliceerd op
gitdocumentatie.logius.nl
- Werkversie (draft): werk-in-uitvoering, gepubliceerd op
logius-standaarden.github.io
De IAM-standaarden OAuth-NL-profiel, OIDC-NLGOV en OIN-Stelsel hebben vastgestelde versies op gitdocumentatie.logius.nl. OAuth-Beheermodel heeft eveneens een vastgestelde versie maar is gearchiveerd. De overige standaarden (AuthZEN, Authorization Decision Log, OAuth-Inleiding) hebben momenteel alleen werkversies.
OpenID.NLGov (v1.0.1) en SAML zijn beide verplicht ('pas-toe-of-leg-uit', sinds 21-09-2023). Voor nieuwe implementaties wordt OIDC aanbevolen; bestaande SAML-koppelingen blijven ondersteund. SAML-details staan in reference.md.
Op het Forum Standaardisatie staat het OAuth-NL-profiel v1.0 als verplicht ('pas-toe-of-leg-uit'). Versie v1.1.0 is vastgesteld door Logius (DEF) maar is op het Forum in procedure genomen (intake goedgekeurd 24-09-2025, verkort expertonderzoek loopt).
Repositories
OAuth 2.0 NL Profiel
Het Nederlandse OAuth 2.0 profiel scherpt de basisspecificatie (RFC 6749) aan met strikte beveiligingseisen voor gebruik binnen de overheid. Doel is het voorkomen van veelvoorkomende kwetsbaarheden en het garanderen van interoperabiliteit.
Verplichte beveiligingseisen
- PKCE (Proof Key for Code Exchange) is verplicht voor alle clients per het OIDC NL GOV profiel ("Clients MUST use PKCE to protect calls to the Token Endpoint"), inclusief confidential clients die zich met
private_key_jwt of mTLS authenticeren — er is geen vrijstelling. Dit voorkomt authorization code interception aanvallen. Clients genereren een code_verifier en sturen een code_challenge mee in het authorization request.
- Grant types:
authorization_code is verplicht (MUST); client_credentials is toegestaan (MAY) voor machine-to-machine communicatie. Implicit grant en Resource Owner Password Credentials zijn expliciet verboden vanwege bekende beveiligingsrisico's.
- Tokens als HTTP headers: access tokens worden als HTTP
Authorization header (Bearer scheme) meegegeven. Transport via query parameters is verboden (MUST NOT). Form-encoded body parameters (RFC 6750 Section 2.2) zijn wel toegestaan.
Token specificaties
- Access tokens: JWT-formaat (RFC 9068) is verplicht (MUST). Dit maakt lokale validatie mogelijk zonder een introspection endpoint aan te roepen, wat de prestaties verbetert.
- Refresh tokens: worden ondersteund (MAY) bij de
authorization_code flow. Hiermee kunnen clients nieuwe access tokens verkrijgen zonder hernieuwde gebruikersinteractie. Refresh token rotation wordt aanbevolen.
- Token levensduur: access tokens hebben een korte levensduur. De exacte duur wordt bepaald door de authorization server, maar korte TTL's (minuten) verdienen de voorkeur.
Client authenticatie
Het token endpoint vereist sterke client authenticatie:
- private_key_jwt (VERPLICHT per OAuth NL profiel): de client ondertekent een JWT-assertion met zijn private key en stuurt deze mee als client authenticatie. De authorization server valideert met de geregistreerde public key.
- mTLS (Mutual TLS) (MAY per OAuth NL profiel, gelijkwaardig alternatief per OIDC NL GOV profiel): de client presenteert een TLS-certificaat (bij voorkeur PKIoverheid) bij het aanroepen van het token endpoint. Dit biedt sterke cryptografische binding.
Keuzematrix: grant type en client authenticatie
Menselijke eindgebruiker betrokken?
JA → authorization_code + PKCE
NEE → client_credentials (machine-to-machine)
Client authenticatie bij token endpoint:
→ private_key_jwt (standaard, verplicht per OAuth NL profiel)
→ mTLS (gelijkwaardig alternatief per OIDC NL GOV profiel, bij PKIoverheid certificaat)
OpenID Connect NL GOV Profiel
Het OIDC NL GOV profiel bouwt voort op OpenID Connect Core 1.0 en het iGov-profiel, en specificeert eisen voor authenticatie binnen de Nederlandse overheid. Het profiel legt de nadruk op betrouwbaarheidsniveaus en gestructureerde identiteitsinformatie.
Betrouwbaarheidsniveaus (Levels of Assurance)
Het profiel definieert drie betrouwbaarheidsniveaus conform de eIDAS-verordening:
| Niveau | eIDAS Level | Toepassing |
|---|
| Laag | Low | Zelfregistratie, beperkte toegang |
| Substantieel | Substantial | DigiD Midden/Hoog, eHerkenning EH3 |
| Hoog | High | eHerkenning EH4, face-to-face verificatie |
De acr_values parameter in het authentication request mapt naar deze eIDAS-niveaus. De OpenID Provider geeft het daadwerkelijk bereikte niveau terug in de acr claim van het ID Token.
Verplichte claims in het ID Token
Het ID Token moet minimaal de volgende claims bevatten:
| Claim | Beschrijving |
|---|
sub | Unieke identifier van de gebruiker (subject) |
iss | Identifier van de OpenID Provider (issuer) |
aud | Identifier van de Relying Party (audience) |
exp | Verloopdatum van het token (expiration) |
iat | Tijdstip van uitgifte (issued at) |
nonce | Waarde uit het authorization request (voorkomt replay attacks) |
acr | Betrouwbaarheidsniveau (OPTIONAL per OIDC NL GOV; als gezet MOET de waarde minimaal het gevraagde betrouwbaarheidsniveau zijn) |
ID Token validatie
Bij het valideren van een OIDC ID Token MOETEN de volgende stappen worden doorlopen:
- Signature - Verifieer de JWS handtekening met de publieke sleutel van de provider (via
jwks_uri)
- iss - MOET exact overeenkomen met de
issuer uit het discovery document
- aud - MOET de
client_id van de relying party bevatten
- nonce - MOET exact overeenkomen met de nonce uit het authorization request
- exp - MOET in de toekomst liggen (token niet verlopen)
- iat - MOET in het verleden liggen; de acceptabele afstand is per Client te bepalen (OIDC NL GOV laat dit expliciet aan de Client; veelvoorkomende keuze in de praktijk: enkele minuten)
- acr - MOET minimaal het gevraagde betrouwbaarheidsniveau bevatten
- jti - MOET uniek zijn (bewaar voldoende lang om hergebruik te detecteren)
AuthZEN NL GOV
Het AuthZEN NL GOV profiel specificeert een architectuur voor geëxternaliseerde autorisatie. In plaats van autorisatielogica in applicaties te bouwen, worden autorisatiebeslissingen gedelegeerd aan een centraal beslispunt. Dit bevordert consistentie, herbruikbaarheid en auditeerbaarheid.
Architectuurcomponenten
Het profiel definieert PDP en PEP als kerncomponenten. PAP en PIP komen uit het XACML-referentiemodel en worden in de praktijk vaak samen ingezet:
| Component | Rol | Beschrijving |
|---|
| PDP | Policy Decision Point | Evalueert autorisatieverzoeken tegen het beleid en neemt een beslissing (permit/deny) |
| PEP | Policy Enforcement Point | Onderschept verzoeken en handhaaft de beslissing van het PDP |
| PAP | Policy Administration Point | Beheert autorisatiebeleid: aanmaken, wijzigen, verwijderen van policies |
| PIP | Policy Information Point | Levert aanvullende attributen en contextinformatie aan het PDP voor besluitvorming |
Evaluation endpoint
Het PDP biedt een gestandaardiseerd evaluation endpoint:
POST /access/v1/evaluation
Content-Type: application/json
Request formaat
Een autorisatieverzoek bestaat uit vier elementen:
- subject: de entiteit die toegang vraagt (gebruiker, systeem)
- action: de gevraagde actie
- resource: het object waarop de actie wordt uitgevoerd
- context: aanvullende omstandigheden (optioneel)
Voorbeeld van een autorisatieverzoek:
{
"subject": {
"type": "user",
"id": "alice"
},
"action": {
"name": "approve"
},
"resource": {
"type": "holiday-request",
"id": "446epbc8y7",
"properties": {
"employee": "bob"
}
}
}
In dit voorbeeld vraagt gebruiker Alice toestemming om een verlofaanvraag van Bob goed te keuren. Het PDP evalueert of Alice de juiste rol en bevoegdheid heeft om deze actie uit te voeren.
Response formaat
Het PDP retourneert een beslissing:
- decision (boolean):
true voor toestaan, false voor weigeren
- context (optioneel): aanvullende informatie, zoals de reden van weigering of referenties naar de toegepaste beleidsregels
{
"decision": true
}
Bij een weigering kan het PDP een reden meegeven:
{
"decision": false,
"context": {
"reason": {
"48": "No signing authority"
}
}
}
Authorization Decision Log
De ADL-standaard definieert een gestructureerd formaat voor het vastleggen van autorisatiebeslissingen voor audit, verantwoording en replay. De werkversie volgt sinds april 2026 een OpenTelemetry-vorm op basis van het AuthZEN-informatiemodel. Het record-model is transport-onafhankelijk; OTLP wordt aanbevolen, maar elke transport (REST, gRPC, messaging) is toegestaan zolang het record de gespecificeerde velden draagt.
Record-velden
| Veld | Type | Verplicht? | Beschrijving |
|---|
trace_id | 16 byte hex (32 chars) | Ja | Trace-id conform W3C Trace Context, cryptografisch random |
span_id | 8 byte hex (16 chars) | Ja | Span-id voor deze beslissing |
parent_span_id | 8 byte hex (16 chars) | Conditioneel | Verplicht bij upstream traceparent; alleen weglaatbaar bij root-span |
event_name | string | Ja | Een van vijf vaste waarden (zie hieronder) |
timestamp | uint64 | Ja | Milliseconden sinds Unix epoch |
status | enum | Ja | Unset (default, ook bij denial), Ok, of Error (PDP kon geen beslissing produceren) |
attributes | object | Optioneel | Source-referenties en metadata (adl.core.*) |
resource | object | Optioneel | OpenTelemetry resource-object (component-context) |
body | object | Optioneel | Raw payload (adl.core.request, adl.core.response, etc.) |
Een denial (decision: false) is Unset, niet Error.
event_name waarden
| AuthZEN API | event_name |
|---|
| Access Evaluation API | adl.access_evaluation |
| Access Evaluations API | adl.access_evaluations |
| Subject Search API | adl.search_subject |
| Action Search API | adl.search_action |
| Resource Search API | adl.search_resource |
attributes.adl.core.*
Source-referenties — geen raw data, die hoort in body. Een veld MOET in precies één van beide locaties staan.
| Attribute | Beschrijving |
|---|
adl.core.request | Input van de beslissing (AuthZEN-formaat) |
adl.core.response | Output (verplicht retrieveerbaar bij status: Unset/Ok) |
adl.core.policies | Object met per policy-source een versie-identifier (timestamp, hash, semver) |
adl.core.information | Referenties naar PIP-data |
adl.core.configuration | Configuratie van PDP/PIP/PAP |
adl.fsc.transaction_id | FSC transaction-id voor correlatie met FSC logs |
Aanvullende keys volgen <vendor>.<area>.<name>. Onbekende keys MOETEN door consumers worden genegeerd.
Levels of detail
Vier niveaus van replayability (zie reference.md): (1) request+response, (2) + adl.core.policies, (3) + adl.core.information, (4) + adl.core.configuration.
Voorbeeld log record (Level 1)
{
"trace_id": "28dbeec32e77635cc19bc3204ec56c41",
"span_id": "5e3c8a4f9b2d1e07",
"parent_span_id": "893e1b2ac52d712f",
"event_name": "adl.access_evaluation",
"timestamp": 1757240058042,
"status": "Unset",
"body": {
"adl.core.request": {
"subject": {"type": "user", "id": "alice"},
"action": {"name": "approve"},
"resource": {"type": "holiday-request", "id": "446epbc8y7"}
},
"adl.core.response": {
"decision": false,
"context": {"reason": {"48": "No signing authority"}}
}
}
}
Trace context en ingestion
Alle componenten (PEP, PDP, PIP, PAP) MOETEN W3C Trace Context propageren; trace_id blijft ongewijzigd over organisatiegrenzen, ook bij sampling=0 (records worden altijd geproduceerd). Ingestion MOET idempotent zijn met (trace_id, span_id) of content-hash als sleutel. Zie reference.md voor details.
Implementatievoorbeelden
OAuth 2.0 Authorization Code Flow met PKCE (Python)
import hashlib, base64, secrets, requests
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
auth_url = "https://auth.example.com/authorize"
params = {
"client_id": "my-client-id",
"response_type": "code",
"scope": "openid profile",
"redirect_uri": "https://myapp.example.com/callback",
"state": secrets.token_urlsafe(32),
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"acr_values": "http://eidas.europa.eu/LoA/substantial",
}
token_response = requests.post("https://auth.example.com/token", data={
"grant_type": "authorization_code",
"code": "received_auth_code",
"redirect_uri": "https://myapp.example.com/callback",
"client_id": "my-client-id",
"code_verifier": code_verifier,
})
tokens = token_response.json()
Client Credentials Flow met private_key_jwt (Python)
import jwt, time, uuid, requests
private_key = open("client_private_key.pem").read()
now = int(time.time())
client_assertion = jwt.encode({
"iss": "my-client-id",
"sub": "my-client-id",
"aud": "https://auth.example.com/token",
"jti": str(uuid.uuid4()),
"iat": now,
"exp": now + 300,
}, private_key, algorithm="PS256", headers={"kid": "my-key-id"})
response = requests.post("https://auth.example.com/token", data={
"grant_type": "client_credentials",
"scope": "api.read api.write",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": client_assertion,
})
access_token = response.json()["access_token"]
JWT Access Token Validatie (Python)
import jwt, requests
jwks_url = "https://auth.example.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
try:
payload = jwt.decode(
access_token,
jwt.PyJWKClient(jwks_url).get_signing_key_from_jwt(access_token),
algorithms=["RS256", "PS256"],
audience="https://api.example.com",
issuer="https://auth.example.com",
options={
"verify_exp": True,
"verify_iat": True,
"require": ["iss", "sub", "aud", "exp", "jti", "client_id"],
}
)
assert "client_id" in payload
assert "jti" in payload
except jwt.ExpiredSignatureError:
pass
except jwt.InvalidTokenError as e:
pass
OIDC Discovery Endpoint Ophalen (curl)
curl -s https://auth.example.com/.well-known/openid-configuration | jq '{
issuer,
authorization_endpoint,
token_endpoint,
jwks_uri,
scopes_supported,
response_types_supported,
grant_types_supported,
acr_values_supported,
subject_types_supported,
token_endpoint_auth_methods_supported
}'
FastAPI Resource Server met Token Validatie
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt, requests
app = FastAPI(title="NL GOV Protected Resource", version="1.0.0")
security = HTTPBearer()
JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
ISSUER = "https://auth.example.com"
AUDIENCE = "https://api.example.com"
jwks_client = jwt.PyJWKClient(JWKS_URL)
async def validate_token(credentials: HTTPAuthorizationCredentials = Security(security)):
"""Valideer OAuth 2.0 NL GOV access token."""
try:
signing_key = jwks_client.get_signing_key_from_jwt(credentials.credentials)
payload = jwt.decode(
credentials.credentials,
signing_key,
algorithms=["RS256", "PS256"],
audience=AUDIENCE,
issuer=ISSUER,
)
return payload
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid or expired token")
@app.get("/v1/resources")
async def get_resources(token: dict = Depends(validate_token)):
"""Beschermd endpoint - vereist geldig NL GOV OAuth token."""
return {
"client_id": token.get("client_id"),
"sub": token.get("sub"),
"scope": token.get("scope"),
"data": [{"id": 1, "name": "Resource"}]
}
Foutafhandeling
OAuth 2.0 Error Responses
Het NL GOV profiel volgt RFC 6749 voor error responses:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}
| Error Code | HTTP Status | Beschrijving |
|---|
invalid_request | 400 | Verplichte parameter ontbreekt of ongeldig |
invalid_client | 401 | Client authenticatie mislukt (verkeerde client_assertion) |
invalid_grant | 400 | Authorization code verlopen of al gebruikt |
unauthorized_client | 400 | Client niet geautoriseerd voor dit grant type |
unsupported_grant_type | 400 | Grant type niet ondersteund |
invalid_scope | 400 | Scope ongeldig of niet beschikbaar |
Token Introspection Response
curl -X POST https://auth.example.com/introspect \
-H "Authorization: Bearer $RESOURCE_SERVER_TOKEN" \
-d "token=$ACCESS_TOKEN" | jq
Achtergrondinfo
Zie reference.md voor SAML-details, OIN-stelsel, en gedetailleerde protocoldocumentatie. Zie conflicts.md voor bekende bronconflicten (GitHub-tags vs. publicatie, Forum Standaardisatie tegenstrijdigheden).