Keycloak and Enterprise Identity
Parsing a JWT and Verifying Signature and Claims
Goal
By taking a JWT apart by hand, verifying its signature, and checking its claims, you understand exactly what the library was doing for you.
Why it matters
JWT verification is one line of library code, so it is easy to pass by without knowing the inside. But if you give that one line the wrong option, it is silently breached. A typical case is that if you do not turn on the aud check, a token for another API issued by the same authentication server passes at our API. If you do not verify alg, there was an actual incident where a token without a signature passed using the none algorithm. And the number you measure at the end of this lab matters — introspection is a network round trip on every request and local verification is a CPU computation. If ten microservices each ask, the authentication server becomes the bottleneck, so in practice you keep access tokens short at 5–15 minutes and use local verification. It is a trade that limits the revocation delay to the token lifetime.
Steps
- With
/root/kc/wait.sh, wait up to 180 seconds untilhttp://127.0.0.1:8080/realms/masterreturns 200. In/root/kc/ready.txt, writeready_seconds=<정수>(an integer). - Obtain a token using the values in
/opt/fixtures/kc/realm-info.envand save only the access token on one line to/root/kc/token.txt. It must be a string with 2 dots. - With
/root/kc/decode.py, save the header to/root/kc/header.jsonand the payload to/root/kc/claims.json. Both must be valid JSON. - In
/root/kc/claimcheck.txt, writeiss=<값> aud=<값> sub=<값> exp_in=<남은초>(the values, and the seconds remaining).exp_inmust be greater than 0. - From
http://127.0.0.1:8080/realms/labhub/protocol/openid-connect/certs, get the JWKS and save the key matching the header'skidto/root/kc/jwk.json.ktymust beRSA. - With
/root/kc/verify.py, verify the signature and in/root/kc/verify.outwritesignature=valid alg=<값>(the value). - Run the same verification with a token whose payload has been tampered with, and in
/root/kc/tamper.outwritesignature=invalid.
Notes
- Padding correction when decoding Base64URL:
s + '=' * (-len(s) % 4) - What is signed is exactly the string
<header_b64>.<payload_b64>. - An ID token and an access token are different. For API calls, use the access token.
- Common mistake 1: not checking
aud— a token for another API passes. - Common mistake 2: not checking the header's
alg— it becomes a route for thenonealgorithm bypass.
Wait until Keycloak is ready
With /root/kc/wait.sh, wait up to 180 seconds until http://127.0.0.1:8080/realms/master returns 200. In /root/kc/ready.txt, write ready_seconds=<정수> (an integer).
It is a JVM, so startup is slow. Use a loop that polls for readiness and allow a generous maximum wait time.
Obtain a token
Obtain a token using the values in /opt/fixtures/kc/realm-info.env and save only the access token on one line to /root/kc/token.txt. It must be a string with 2 dots.
The information for the pre-imported realm is in /opt/fixtures/kc/realm-info.env. Send form data to the token endpoint.
Decode the header and payload
With /root/kc/decode.py, save the header to /root/kc/header.json and the payload to /root/kc/claims.json. Both must be valid JSON.
They are the first two of the three pieces separated by dots. Base64URL may lack padding, so you need to correct it.
Check the required claims
In /root/kc/claimcheck.txt, write iss=<값> aud=<값> sub=<값> exp_in=<남은초> (the values, and the seconds remaining). exp_in must be greater than 0.
Check the issuer, audience, expiry, and subject each. If you do not look at even one of them, it becomes a hole.
Find the signing key in the JWKS
From http://127.0.0.1:8080/realms/labhub/protocol/openid-connect/certs, get the JWKS and save the key matching the header's kid to /root/kc/jwk.json. kty must be RSA.
Pick from the list by the key identifier in the header. During a rotation, several keys can be there together.
Verify the signature
With /root/kc/verify.py, verify the signature and in /root/kc/verify.out write signature=valid alg=<값> (the value).
What is signed is the string of the header and payload joined with a dot. It is not the decoded JSON.
Confirm that a tampered token is rejected
Run the same verification with a token whose payload has been tampered with, and in /root/kc/tamper.out write signature=invalid.
It is normal for the signature to break if you change even one character of the payload.