> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://developer-stage.shipbob.dev/2026-07/migration/pkce/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer-stage.shipbob.dev/_mcp/server. # Migrate to PKCE > Add PKCE (Proof Key for Code Exchange) to an existing OAuth app's authorization flow. > **Info** > > PKCE (Proof Key for Code Exchange) adds a one-time secret to the OAuth authorization code exchange, protecting against intercepted authorization codes. It is now part of ShipBob's OAuth flow. This guide walks through adding it to an OAuth app that was built before PKCE. ## What changes PKCE adds two parameters to the authorize request and one to the token exchange. Nothing else about your OAuth flow changes - the `client_id`, `client_secret`, `redirect_uri`, and scopes all stay the same, and the `client_secret` is still required. | Step | Before | After | | ----------------- | ----------------------------------------- | ---------------------------------------------------- | | Authorize request | `client_id`, `redirect_uri`, ... | adds `code_challenge` + `code_challenge_method=S256` | | Token exchange | `client_id`, `client_secret`, `code`, ... | adds `code_verifier` | ## Migration steps #### Generate a code verifier and challenge For each authorization request, generate a fresh PKCE pair: * **`code_verifier`** – a cryptographically random string, 43-128 characters, using only the URL-safe characters `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`. Keep it secret and store it for the token exchange (Step 3). * **`code_challenge`** – the Base64-URL-encoded (no padding) SHA-256 hash of the `code_verifier`. > **Warning** > > Always use the `S256` challenge method. Never use the `plain` method in production. #### JavaScript / Node ```javascript import crypto from "crypto"; function base64UrlEncode(buffer) { return buffer .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } // 32 random bytes -> 43-character verifier const codeVerifier = base64UrlEncode(crypto.randomBytes(32)); const codeChallenge = base64UrlEncode( crypto.createHash("sha256").update(codeVerifier).digest() ); console.log({ codeVerifier, codeChallenge }); ``` #### Python ```python import base64 import hashlib import os def base64_url_encode(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") # 32 random bytes -> 43-character verifier code_verifier = base64_url_encode(os.urandom(32)) code_challenge = base64_url_encode( hashlib.sha256(code_verifier.encode("ascii")).digest() ) print(code_verifier, code_challenge) ``` #### PowerShell ```powershell $bytes = New-Object byte[] 32 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) $codeVerifier = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+','-').Replace('/','_') $sha256 = [System.Security.Cryptography.SHA256]::Create() $hashBytes = $sha256.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($codeVerifier)) $codeChallenge = [Convert]::ToBase64String($hashBytes).TrimEnd('=').Replace('+','-').Replace('/','_') "$codeVerifier`n$codeChallenge" ``` #### Add the challenge to your authorize request Append `code_challenge` and `code_challenge_method=S256` to the authorize URL you already build: ``` https://auth.shipbob.com/connect/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fwww.myapp.com%2Fintegrate%2Fshipbob%2Fcallback &response_type=code &scope=openid%20offline_access%20channels_read &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 ``` #### Add the verifier to your token exchange Include the matching `code_verifier` in the token request. The `client_secret` is still required - PKCE is an additional layer, not a replacement. ```bash curl -X POST "https://auth.shipbob.com/connect/token" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "client_id=YOUR_CLIENT_ID" \ --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \ --data-urlencode "redirect_uri=https://www.myapp.com/integrate/shipbob/callback" \ --data-urlencode "code=AUTHORIZATION_CODE" \ --data-urlencode "code_verifier=code_verifier_from_step_1" ``` > **Note** > > The `code_verifier` must match the `code_challenge` you sent on the authorize request in Step 2. Generate a new pair for every authorization - never reuse a `code_verifier`. > **Note** > > PKCE applies only to the initial authorization code exchange. You do **not** need to send a `code_verifier` or `code_challenge` when refreshing tokens. For the full OAuth walkthrough, see the [Authentication](/auth) guide. > Add PKCE (Proof Key for Code Exchange) to an existing OAuth app's authorization flow.