How does PKCE protect an OAuth sign-in in an app, how is the code challenge computed, and what are state and nonce for?

14.6.5 · L3 · Interview

Predict first

Does the RFC 7636 example challenge come out, and how does base64url differ?

import Foundation
import CryptoKit

extension Data {
    var base64URL: String { base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") }
}
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
let digest = Data(SHA256.hash(data: Data(verifier.utf8)))
print("S256 challenge:", digest.base64URL)
print("plain base64:  ", digest.base64EncodedString())
var bytes = [UInt8](repeating: 0, count: 32)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
print("random verifier ok:", status == errSecSuccess, "length:", Data(bytes).base64URL.count)
  • S256 challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM / plain base64: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw+cM= / random verifier ok: true length: 43
  • S256 challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw+cM=
  • random verifier ok: false length: 44

Mental model. PKCE is writing a secret word on a slip, locking its fingerprint in the bank’s box, and later showing the real slip: someone who only saw the claim ticket cannot produce it.

A native app cannot keep a client secret, and the redirect back to the app (a custom scheme or universal link) can be intercepted by another app or by a malicious page. PKCE (RFC 7636) makes a stolen authorization code useless. Before starting, the app creates a random code_verifier (43 to 128 characters from a URL-safe alphabet; 32 random bytes encoded as base64url is 43), and sends only its code_challenge = base64url(SHA-256(verifier)) with code_challenge_method=S256 in the authorization request. When it exchanges the code for tokens it sends the verifier itself, and the server recomputes the hash and compares. An attacker who captured the code does not have the verifier. The check reproduces the RFC’s own example: the verifier dBjftJeZ…EjXk gives the challenge E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM. Note the encoding: base64url without padding (- and _, no =); standard base64 of the same digest ends in +cM= and would be rejected. Generate the verifier with SecRandomCopyBytes (it returned errSecSuccess), never with a seeded or predictable generator, and never use the plain method.

Two other values are often confused with it. state is an unguessable value you generate per request and compare on return, which defends the callback against cross-site request forgery and mixing up sessions. nonce (OpenID Connect) is placed in the ID token, which you compare so a token minted for another request cannot be replayed. Run the flow in ASWebAuthenticationSession (Q383), not an embedded web view, keep the verifier in memory only for the one attempt, and use the exact registered redirect URI. Not run: a real authorization server; the check covers only the cryptographic step.

The challenge is the unpadded base64url SHA-256 of the verifier; 32 random bytes give a 43-character verifier.

Common traps

  • Using standard base64 or keeping the = padding. The server’s hash comparison fails.
  • Skipping state because PKCE exists. They protect different steps.
  • Generating the verifier from a timestamp or UUID alone. It must be unpredictable; use secure random bytes.

They might follow up with

  • Why is PKCE needed even with a universal link callback? Links can still be claimed or observed in some paths; PKCE removes the dependence on the redirect being private.
  • Where does the verifier live? In memory for the duration of the sign-in, never in logs or persistent storage.

Related questions

  • 14.6.6 What should code that reads a JWT check, why is a client usually not the one to trust it, and what is the…
  • 14.6.4 How does a time-based one-time password (TOTP) work, and how would you compute one with CryptoKit?
  • 17.5.1 How do passkeys, Sign in with Apple and ASWebAuthenticationSession work in AuthenticationServices?
  • 14.4.1 Why are deep links an attack surface, and how do you validate them?

← How does a time-based one-time password (TOTP) work, and how would…   What should code that reads a JWT check, why is a client usually not… →