Keycloak and Enterprise Identity
Completing the Authorization Code + PKCE Flow
Goal
Carry out the authorization code + PKCE flow from start to finish without an SDK, and confirm through failure cases what PKCE and state each prevent.
Why it matters
With an OIDC library, this flow is two function calls. So you end up using it without knowing what happens, and when a problem arises you do not know where to look. If you do each step by hand in this lab, three things become clear. First, the authorization code is exposed in the browser URL, but why is it useless by itself. Second, why does the exchange fail if you do not know the code_verifier — you make it fail yourself in step 6. Third, why can an attacker log the victim in to their own account without state. And if you also see the refresh rotation at the end, you will understand why the combination recommended in practice (a short access token + refresh rotation) gives an invalidation effect even without a blacklist.
Keycloak takes 40–90 seconds to start, so it is usually ready by the time you finish the previous lab and come here.
Steps
- With
/root/pk/pkce.py, create acode_verifier(43–128 characters, URL-safe characters) and acode_challengeand save them, one line each, to/root/pk/verifier.txtand/root/pk/challenge.txt. - In
/root/pk/authz_url.txt, write the authorization request URL on one line. The target is the pre-imported realmlabhub's public clientweb-app, and the redirect URI ishttp://127.0.0.1:8161/callback. It must contain all ofresponse_type=code,client_id,redirect_uri,scope=openid,state,code_challenge, andcode_challenge_method=S256. - While keeping a cookie jar, GET that URL and save the login form's
actionURL to/root/pk/action.txt. It must start withhttp. - POST user
dev1/ passwordDev1!pass, extract the authorization code from theLocationof the redirect response, and save it to/root/pk/code.txt. It must be at least 20 characters. - Send
grant_type=authorization_code, the code,redirect_uri,client_id, andcode_verifierto the token endpoint to get tokens./root/pk/tokens.jsonmust containaccess_tokenandrefresh_token. - Get a new authorization code and try the exchange with a wrong
code_verifier. In/root/pk/pkce_fail.json, theerrormust beinvalid_grant. /root/pk/state_check.pytakes two arguments (the sent state and the received state) and exits with code 0 if they are the same and 1 if they differ. Write the results of the match and mismatch cases to/root/pk/state.outasmatch=ok mismatch=rejected.- Refresh with the refresh token to make
/root/pk/refresh.json. In/root/pk/rotation.txt, writerotated=<true|false>. The basis for the decision is whether the new refresh token differs from the previous one.
Notes
- Computing the challenge:
base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=') - Keeping cookies:
curl -c /root/pk/cookies -b /root/pk/cookies ... - You must not follow the redirect in order to see the Location header (
-iwithout-L). - Common mistake 1: making the hash result a hex string and then Base64-encoding it — you must encode the binary digest.
- Common mistake 2: the
redirect_uriin the token exchange differs from the one in the authorization request, causinginvalid_grant.
Create the code_verifier and challenge
With /root/pk/pkce.py, create a code_verifier (43–128 characters, URL-safe characters) and a code_challenge and save them, one line each, to /root/pk/verifier.txt and /root/pk/challenge.txt.
The length limit and character set are fixed by the specification. You must not Base64 the hash result as is; you need URL-safe encoding without padding.
Assemble the authorization request URL
In /root/pk/authz_url.txt, write the authorization request URL on one line. The target is the pre-imported realm labhub's public client web-app, and the redirect URI is http://127.0.0.1:8161/callback. It must contain all of response_type=code, client_id, redirect_uri, scope=openid, state, code_challenge, and code_challenge_method=S256.
There are six required parameters. If even one is missing, the authentication server gives an error.
Extract the login form's action
While keeping a cookie jar, GET that URL and save the login form's action URL to /root/pk/action.txt. It must start with http.
If you GET the authorization endpoint, HTML comes back. You must keep the cookies for the next step to work.
POST the credentials to get the code
POST user dev1 / password Dev1!pass, extract the authorization code from the Location of the redirect response, and save it to /root/pk/code.txt. It must be at least 20 characters.
Check the form field names in the HTML. On success, the code is in the location header of the redirect response.
Exchange the code for tokens
Send grant_type=authorization_code, the code, redirect_uri, client_id, and code_verifier to the token endpoint to get tokens. /root/pk/tokens.json must contain access_token and refresh_token.
Send the authorization code together with the original verifier. The redirect URI must also be exactly the same as the first time.
Confirm the failure with a wrong verifier
Get a new authorization code and try the exchange with a wrong code_verifier. In /root/pk/pkce_fail.json, the error must be invalid_grant.
This is the evidence that PKCE actually works. Record the error code.
Implement the state mismatch check
/root/pk/state_check.py takes two arguments (the sent state and the received state) and exits with code 0 if they are the same and 1 if they differ. Write the results of the match and mismatch cases to /root/pk/state.out as match=ok mismatch=rejected.
Compare the value sent in the authorization request with the value that came back in the callback. If they differ, you must reject.
Check refresh and rotation
Refresh with the refresh token to make /root/pk/refresh.json. In /root/pk/rotation.txt, write rotated=<true|false>. The basis for the decision is whether the new refresh token differs from the previous one.
When you refresh, a new refresh token comes. Whether it is the same as or different from the old one is whether rotation happened.