Bind an authorization code to the client that initiated it using a high-entropy code verifier and its SHA-256 challenge.
The examples introduce the underlying control before showing a verification and an application-level safeguard.
What You Will Build
Bind an authorization code to the client that initiated it using a high-entropy code verifier and its SHA-256 challenge.
Prerequisites
Use PHP 8.4 or newer, HTTPS for all authentication operations, and an application session or database where required. Example configuration values are placeholders, never production secrets.
Example 1: Basic Operation
Create one-time state, PKCE verifier and S256 code challenge.
<?php
session_start();
$state = bin2hex(random_bytes(32));
$verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$_SESSION['oauth_attempt'] = ['state' => $state, 'verifier' => $verifier];
echo 'PKCE S256 challenge: ' . $challenge;
Example 2: A Different Use Case
Validate the callback state and consume the login attempt once.
<?php
session_start();
$attempt = $_SESSION['oauth_attempt'] ?? [];
$state = $_GET['state'] ?? null;
if (!is_string($state) || !isset($attempt['state']) || !hash_equals($attempt['state'], $state)) {
http_response_code(400); exit('Invalid OAuth state');
}
$code = $_GET['code'] ?? null;
if (!is_string($code) || $code === '') { http_response_code(400); exit; }
$_SESSION['validated_pkce_verifier'] = $attempt['verifier'];
unset($_SESSION['oauth_attempt']);
// Exchange the code at a fixed token endpoint; expire verifier after this attempt.
Practical Example: Checking the Result
Build an authorization request using fixed provider settings, not user-controlled URLs.
<?php
// Run only after the server has validated and consumed the callback state.
// These values must be kept from the same original authorization attempt.
$authorizationCode = $_GET['code'] ?? null;
$verifier = $_SESSION['validated_pkce_verifier'] ?? null;
if (!is_string($authorizationCode) || !is_string($verifier)) { http_response_code(400); exit; }
$tokenEndpoint = 'https://auth.example.test/token'; // Fixed trusted provider setting.
$payload = http_build_query(['grant_type' => 'authorization_code', 'code' => $authorizationCode,
'client_id' => getenv('OAUTH_CLIENT_ID'), 'redirect_uri' => 'https://app.example.test/callback',
'code_verifier' => $verifier], '', '&', PHP_QUERY_RFC3986);
$ch = curl_init($tokenEndpoint);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded']]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
unset($_SESSION['validated_pkce_verifier']);
if (!is_string($response) || $status !== 200) { throw new RuntimeException('Token exchange failed'); }
$tokens = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
// Validate scope, issuer and token type; store tokens server-side only.
// For login via OpenID Connect, also validate the ID token and nonce.
Important Behavior and Edge Cases
Persist verifier and CSRF state server-side only for that login attempt, then consume them exactly once.
Enforce controls on the server, test denial paths and do not assume that a snippet alone implements an entire production workflow.
Comparison with Related Functions
| Approach | Key distinction |
|---|---|
| PHP OAuth 2.0 Authorization Code with PKCE | Bind an authorization code to the client that initiated it using a high-entropy code verifier and its SHA-256 challenge. |
| Related technique | The S256 method is stronger than plain; PKCE supplements rather than replaces state validation. |
Common Beginner Mistakes
| Mistake | Correct approach |
|---|---|
| Confusing this operation with a neighboring cryptographic or security mechanism | The S256 method is stronger than plain; PKCE supplements rather than replaces state validation. |
| Ignoring a security prerequisite or failure case | Persist verifier and CSRF state server-side only for that login attempt, then consume them exactly once. |
Key Takeaways
- Bind an authorization code to the client that initiated it using a high-entropy code verifier and its SHA-256 challenge.
- Persist verifier and CSRF state server-side only for that login attempt, then consume them exactly once.
- The S256 method is stronger than plain; PKCE supplements rather than replaces state validation.
Apply this behavior to a real application only after confirming the required extension, configuration and failure handling on the deployment runtime.