TT Lab
Get started
Learn Learning paths Courses

Keycloak and Enterprise Identity

Completing the Authorization Code + PKCE Flow

Continue in TT Lab

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. /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.
  8. 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.

Notes

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.