From standaarden
Guides implementation of Dutch government IAM standards: OAuth 2.0 NL profiles, OIDC NL GOV, AuthZEN, SAML for API authentication and authorization. Use for NL GOV-compliant auth.
npx claudepluginhub developer-overheid-nl/skills-marketplace --plugin standaardenThis skill is limited to using the following tools:
> **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](https://www.forumstandaardisatie.nl/open-standaarden) en [Logius](https://www.logius.nl) zijn altijd leidend. Overheidsorganisaties die generatieve AI inzetten dienen te voldoen aan het [Overh...
Monitors deployed URLs for regressions after deploys, merges, or upgrades by checking HTTP status, console errors, network failures, performance (LCP/CLS/INP), content, and API health.
Share bugs, ideas, or general feedback.
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.
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.
Net als andere Logius-standaarden kennen deze standaarden twee publicatiekanalen (vergelijkbaar met W3C):
gitdocumentatie.logius.nllogius-standaarden.github.ioDe 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).
| Repository | Beschrijving | Licentie | Vastgesteld | Draft |
|---|---|---|---|---|
| OAuth-NL-profiel | Nederlands profiel voor OAuth 2.0 | CC-BY-4.0 | v1.1.0 | Draft |
| OIDC-NLGOV | OpenID Connect profiel voor NL overheid | CC-BY-4.0 | v1.0.1 | Draft |
| OAuth-Inleiding | Introductie en achtergrond OAuth | CC-BY-4.0 | - | Draft |
| OAuth-Beheermodel | Beheermodel voor OAuth standaarden — gearchiveerd | CC-BY-4.0 | v1.0 | - |
| authzen-nlgov | AuthZEN NL GOV profiel (autorisatiebeslissingen) | CC-BY-4.0 | - | Draft |
| authorization-decision-log | Logging van autorisatiebeslissingen | CC-BY-4.0 | - | Draft |
| st-saml-spec | SAML specificatie — onderdeel van verplichte "Authenticatie-standaarden"; voor nieuwe implementaties wordt OIDC aanbevolen | CC-BY-4.0 | - | Draft |
| OIN-Stelsel | Organisatie Identificatie Nummer (ook in /ls-dk) | CC-BY-4.0 | v3.0.0 | Draft |
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.
private_key_jwt of mTLS zijn vrijgesteld. Dit voorkomt authorization code interception aanvallen. Clients genereren een code_verifier en sturen een code_challenge mee in het authorization request.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.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.authorization_code flow. Hiermee kunnen clients nieuwe access tokens verkrijgen zonder hernieuwde gebruikersinteractie. Refresh token rotation wordt aanbevolen.Het token endpoint vereist sterke 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)
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.
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.
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) |
Bij het valideren van een OIDC ID Token MOETEN de volgende stappen worden doorlopen:
jwks_uri)issuer uit het discovery documentclient_id van de relying party bevattenHet 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.
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 |
Het PDP biedt een gestandaardiseerd evaluation endpoint:
POST /access/v1/evaluation
Content-Type: application/json
Een autorisatieverzoek bestaat uit vier elementen:
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.
Het PDP retourneert een beslissing:
true voor toestaan, false voor weigeren{
"decision": true
}
Bij een weigering kan het PDP een reden meegeven:
{
"decision": false,
"context": {
"reason": {
"48": "No signing authority"
}
}
}
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.
| 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.
Vier niveaus van replayability (zie reference.md): (1) request+response, (2) + adl.core.policies, (3) + adl.core.information, (4) + adl.core.configuration.
{
"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"}}
}
}
}
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.
# Complete example using requests library
import hashlib, base64, secrets, requests
# 1. PKCE code_verifier en code_challenge genereren
code_verifier = secrets.token_urlsafe(64) # min 43, max 128 chars
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
# 2. Authorization request (redirect user to this URL)
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), # min 128 bits entropy
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"acr_values": "http://eidas.europa.eu/LoA/substantial", # eIDAS level
}
# redirect_url = f"{auth_url}?{'&'.join(f'{k}={v}' for k, v in params.items())}"
# 3. Token request (after receiving authorization code)
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, # PKCE proof
})
tokens = token_response.json()
# tokens = {"access_token": "eyJ...", "token_type": "Bearer", "id_token": "eyJ...", "expires_in": 3600}
import jwt, time, uuid, requests
# JWT client assertion aanmaken (private_key_jwt authenticatie)
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()), # uniek per request
"iat": now,
"exp": now + 300, # max 5 minuten geldig
}, private_key, algorithm="PS256", headers={"kid": "my-key-id"})
# Token request met client assertion
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"]
import jwt, requests
# JWKS ophalen van de authorization server
jwks_url = "https://auth.example.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
# Access token valideren
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", # verwachte audience
issuer="https://auth.example.com", # verwachte issuer
options={
"verify_exp": True,
"verify_iat": True,
"require": ["iss", "sub", "aud", "exp", "jti", "client_id"],
}
)
# Verplichte claims per NL GOV profiel
assert "client_id" in payload
assert "jti" in payload # uniek, niet herbruikbaar
except jwt.ExpiredSignatureError:
# Token verlopen - nieuwe token ophalen
pass
except jwt.InvalidTokenError as e:
# Token ongeldig - toegang weigeren
pass
# Discovery document ophalen
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
}'
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"}]
}
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 (voor resource servers die tokens niet zelf kunnen valideren)
curl -X POST https://auth.example.com/introspect \
-H "Authorization: Bearer $RESOURCE_SERVER_TOKEN" \
-d "token=$ACCESS_TOKEN" | jq
# Response bij geldig token:
# {"active": true, "scope": "api.read", "client_id": "...", "sub": "...", "exp": 1234567890}
# Response bij ongeldig token:
# {"active": false}
Zie reference.md voor SAML-details, OIN-stelsel, en gedetailleerde protocoldocumentatie. Zie conflicts.md voor bekende bronconflicten (GitHub-tags vs. publicatie, Forum Standaardisatie tegenstrijdigheden).