Skip to main content

Overview

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks by binding the authorization request to the token exchange. The Grantex SDK provides a generate_pkce() helper that creates a cryptographically random code verifier and its S256 challenge.

Usage

from grantex import generate_pkce

pkce = generate_pkce()

print(pkce.code_verifier)         # Random 43-character URL-safe string
print(pkce.code_challenge)        # SHA-256 hash of the verifier, base64url-encoded
print(pkce.code_challenge_method) # Always "S256"

Import

from grantex import generate_pkce, PkceChallenge

PkceChallenge

generate_pkce() returns a PkceChallenge frozen dataclass:
FieldTypeDescription
code_verifierstrA cryptographically random URL-safe string (32 bytes, base64url-encoded).
code_challengestrThe SHA-256 hash of the verifier, base64url-encoded.
code_challenge_methodstrAlways "S256".

Complete PKCE Flow

The PKCE flow has three steps: generate the challenge, authorize with the challenge, and exchange with the verifier.

Step 1: Generate the PKCE Pair

from grantex import generate_pkce

pkce = generate_pkce()
# Store pkce.code_verifier securely -- you will need it in Step 3

Step 2: Authorize with the Code Challenge

Pass the code_challenge and code_challenge_method in the authorization request:
from grantex import Grantex, AuthorizeParams

with Grantex(api_key="gx_live_...") as client:
    auth = client.authorize(AuthorizeParams(
        agent_id="agt_abc123",
        user_id="user_xyz",
        scopes=["files:read", "files:write"],
        redirect_uri="https://myapp.com/callback",
        code_challenge=pkce.code_challenge,
        code_challenge_method=pkce.code_challenge_method,
    ))

    # Redirect user to auth.consent_url

Step 3: Exchange with the Code Verifier

After the user approves and you receive the authorization code at your redirect URI, include the code_verifier in the token exchange:
from grantex import Grantex, ExchangeTokenParams

with Grantex(api_key="gx_live_...") as client:
    token_response = client.tokens.exchange(ExchangeTokenParams(
        code="received_auth_code",
        agent_id="agt_abc123",
        code_verifier=pkce.code_verifier,
    ))

    print(f"Grant token: {token_response.grant_token}")
The server verifies that SHA256(code_verifier) == code_challenge before issuing the token. If the verifier does not match, the exchange is rejected.

Full Example

from grantex import (
    Grantex,
    AuthorizeParams,
    ExchangeTokenParams,
    generate_pkce,
)

# 1. Generate PKCE pair
pkce = generate_pkce()

with Grantex(api_key="gx_live_...") as client:
    # 2. Start authorization with PKCE
    auth = client.authorize(AuthorizeParams(
        agent_id="agt_abc123",
        user_id="user_xyz",
        scopes=["calendar:read"],
        redirect_uri="https://myapp.com/callback",
        code_challenge=pkce.code_challenge,
        code_challenge_method=pkce.code_challenge_method,
    ))

    print(f"Send user to: {auth.consent_url}")

    # 3. User approves, you receive the code at your callback
    code = "code_from_redirect"

    # 4. Exchange with PKCE verifier
    result = client.tokens.exchange(ExchangeTokenParams(
        code=code,
        agent_id="agt_abc123",
        code_verifier=pkce.code_verifier,
    ))

    print(f"Grant token: {result.grant_token}")
    print(f"Grant ID: {result.grant_id}")
    print(f"Scopes: {result.scopes}")

How It Works

  1. generate_pkce() creates 32 random bytes and base64url-encodes them to produce the code_verifier.
  2. The code_challenge is the SHA-256 digest of the verifier, base64url-encoded (without padding).
  3. The challenge method is always S256 (plain is not supported).
  4. During token exchange, the server independently computes SHA256(code_verifier) and compares it to the stored code_challenge. A mismatch causes the exchange to fail.