Guides Integration
PKCE
PKCE (Proof Key for Code Exchange, pronounced “pixie”) stops an attacker who intercepts your authorization code from being able to redeem it. You generate a secret, send only a hash of it to /authorize, and prove you hold the original when you exchange the code. An intercepted code is useless without the secret, which never leaves your application.
PKCE is required on every AgeWallet authorization request, including server-side ones.
Use a library if you have one
Most OIDC libraries generate and store the pair for you, and getting it wrong is easy enough that this is the recommended path:
- Node —
openid-client - Browser / SPA —
oidc-client-ts - PHP —
jumbojett/OpenID-Connect-PHP - Python —
authlib - iOS / Android — AppAuth
If your stack has one of these, use it and skip to Exchanging the code. The rest of this section is for anyone generating the pair by hand.
Generating the pair yourself
- Generate a random code verifier — 32 random bytes, base64url-encoded. The specification allows 43 to 128 characters; 32 bytes encodes to exactly 43, which is the recommended length.
- Hash the verifier string with SHA-256 and base64url-encode the digest. That result is your code challenge.
- Send
code_challengeandcode_challenge_method=S256on the/authorizerequest. - Store the verifier, then send the original
code_verifierwhen you exchange the authorization code for a token.
import { randomBytes, createHash } from 'node:crypto';
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');const b64url = bytes => btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = b64url(digest);
sessionStorage.setItem('agewallet_verifier', verifier);function agewallet_b64url( $bytes ) {
return rtrim( strtr( base64_encode( $bytes ), '+/', '-_' ), '=' );
}
$verifier = agewallet_b64url( random_bytes( 32 ) );
$challenge = agewallet_b64url( hash( 'sha256', $verifier, true ) );
$_SESSION['agewallet_verifier'] = $verifier;import base64, hashlib, secrets
def b64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
verifier = b64url(secrets.token_bytes(32))
challenge = b64url(hashlib.sha256(verifier.encode("ascii")).digest())Check your implementation
Run your generator against this pair from the PKCE specification. Given this verifier, you must produce this challenge:
code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
If your code does not reproduce that exactly, the problem is in your PKCE implementation rather than in your request to AgeWallet. Fixing it here saves you debugging a failed token exchange later.