Careeroza
PKCE Step by Step
Verifier and Challenge
PKCE has only two values:
- code_verifier: a high-entropy random string, 43–128 characters from
[A-Z a-z 0-9 - . _ ~]. The client keeps it secret. - code_challenge:
BASE64URL(SHA-256(code_verifier))without padding. Sent in the authorization request.
Because SHA-256 is one-way, seeing the challenge does not reveal the verifier. At the token step, the server hashes the verifier it receives and compares.
Example
Example
Generate a verifier, derive the challenge, and verify it the way the authorization server does. The second part uses a fixed verifier (the same one as in the token request above), so the output never changes:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class Main {
static final Base64.Encoder B64URL = Base64.getUrlEncoder().withoutPadding();
static String newVerifier() {
byte[] random = new byte[32]; // 256 bits of entropy
new SecureRandom().nextBytes(random);
return B64URL.encodeToString(random); // 43 characters
}
static String challenge(String verifier) throws Exception {
byte[] hash = MessageDigest.getInstance("SHA-256")
.digest(verifier.getBytes(StandardCharsets.US_ASCII));
return B64URL.encodeToString(hash);
}
// What the authorization server does at POST /token
static boolean serverVerifies(String storedChallenge, String receivedVerifier) throws Exception {
byte[] expected = storedChallenge.getBytes(StandardCharsets.US_ASCII);
byte[] actual = challenge(receivedVerifier).getBytes(StandardCharsets.US_ASCII);
return MessageDigest.isEqual(expected, actual); // constant-time comparison
}
public static void main(String[] args) throws Exception {
System.out.println("fresh verifier length: " + newVerifier().length());
String verifier = "dBjftJeZ4CVP-mJ92K5qZvbMCTr9g5gtWfH5zR2Rr8Y";
String challenge = challenge(verifier);
System.out.println("code_verifier : " + verifier);
System.out.println("code_challenge: " + challenge);
System.out.println("real client -> " + serverVerifies(challenge, verifier));
System.out.println("attacker -> " + serverVerifies(challenge, "stolen-code-but-no-verifier-1234567890abcd"));
System.out.println("sent challenge instead of verifier -> " + serverVerifies(challenge, challenge));
}
}Output
fresh verifier length: 43 code_verifier : dBjftJeZ4CVP-mJ92K5qZvbMCTr9g5gtWfH5zR2Rr8Y code_challenge: fsvuGwC6jZ1S5fS6rRWxoYbRArpX776H68sqheFaMCg real client -> true attacker -> false sent challenge instead of verifier -> false
Practice
Exercise: Why is code_challenge_method=plain (challenge = verifier) weaker than S256? When is it still allowed?
Show answer
With "plain", the challenge sent in the browser URL IS the verifier.
Anyone who can read the authorization request (browser history, logs,
a malicious extension) can also send the verifier at the token step.
With S256 the URL only contains SHA-256(verifier), which cannot be reversed.
"plain" is only allowed when the client truly cannot compute SHA-256.
Every modern browser, Android, iOS and Java platform can, so always use S256.
Spring Authorization Server rejects "plain" by default.