------------------------------------------------------------------ == PKCE parameters (RFC 7636) ------------------------------------------------------------------ code_verifier RnM5322FJWXI9zUZDUkmRmK-x_rjKT6HHFQyIhSEqjtVkb527uqjZmfQCwrNmavk (64 chars) code_challenge rzSywj6JKmWWVVBuZsD26hYR4_3PK0zZRCXrn_4sU9M code_challenge_method S256 The verifier never leaves the client until the token request. The challenge is all the authorization request carries, and it is a one-way hash of the verifier. ------------------------------------------------------------------ == 1. Log in to the authorization server (browser session) ------------------------------------------------------------------ $ curl -c jar -d username=alice -d password=password -d _csrf= http://localhost:9000/login HTTP/1.1 302 Location: http://localhost:9000/ ------------------------------------------------------------------ == 2. GET /oauth2/authorize (client=demo-spa) ------------------------------------------------------------------ NO_CHALLENGE=1: the authorization request carries no code_challenge. $ curl -b jar 'http://localhost:9000/oauth2/authorize?response_type=code&client_id=demo-spa&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fauthorized&scope=openid%20orders.read&state=xyz123' -> 302 http://127.0.0.1:8080/authorized?code=ydxIVNbSbrjWRlj1YxX6LS4cnXgDdzxTIyZgqoqlmX6cD8q-AYnL_Sdy6m5gqheZ-yd8qKUgSrrgMtg331sYHjblO3IYqGb1i94OxMypdyLr6RMqm_O_XuVZX2g1FSeD&state=xyz123 ------------------------------------------------------------------ == 3. No consent page ------------------------------------------------------------------ The authorization endpoint went straight back to the client. Either consent is off for this client, or every requested scope was already approved. ------------------------------------------------------------------ == 5. The authorization code ------------------------------------------------------------------ code = ydxIVNbSbrjWRlj1YxX6LS4cnXgDdzxTIyZgqoqlmX6cD8q-AYnL_Sdy6m5gqheZ-yd8qKUgSrrgMtg331sYHjblO3IYqGb1i94OxMypdyLr6RMqm_O_XuVZX2g1FSeD state = xyz123 (the client's own value, returned untouched - compare it) ------------------------------------------------------------------ == 6a. Exchange the code WITHOUT the verifier ------------------------------------------------------------------ This is the request an attacker who stole the code can make. HTTP 401 (empty response body) >>> Rejected. invalid_grant is deliberately vague: the server will not tell >>> a caller whether the code was wrong, expired, already used, or missing a >>> verifier, because each of those is information an attacker can use. Note: this consumed the code. Authorization codes are single-use, so the successful exchange below needs a fresh one. ------------------------------------------------------------------ == 6b. A fresh code, exchanged properly ------------------------------------------------------------------ fresh code = uvr0H49ByWte7NfWDoDnZ61-11rMP0wxYFJMY8eM73Lm2dcc2Edl-oFdW3DwJNSqUBMyIRX8rY9JqPDZIvwnEo4grUU-D5Z-GTwYYV1B8Xvosdbg-vAjiss1wbRsvD03 $ curl -d grant_type=authorization_code -d code=... http://localhost:9000/oauth2/token (no code_verifier - there was no challenge to verify against) HTTP 401 (empty response body) >>> No token, even though the client is registered with requireProofKey(false) >>> and the authorization request carried no challenge. The reason is that a >>> public client has no other way to authenticate at the token endpoint: >>> PublicClientAuthenticationProvider delegates entirely to >>> CodeVerifierAuthenticator, and raises invalid_client when there is nothing >>> to verify. requireProofKey(false) relaxes the AUTHORIZATION endpoint only.