# PKCE (Proof Key for Code Exchange)

PKCE (pronounced "pixy") is a security extension to OAuth 2.0 for public clients that cannot securely store a client secret.

## When PKCE is Required

| Client Type | Can Store Secrets? | PKCE Required? | Examples |
|-------------|-------------------|----------------|----------|
| **Confidential** | ✅ Yes (server-side) | ❌ Optional | Backend servers, traditional web apps |
| **Public** | ❌ No (client-side) | ✅ **Required** | Mobile apps, SPAs, desktop apps |

###Why Public Clients Can't Keep Secrets

```javascript
// ❌ INSECURE: Client secret embedded in mobile/SPA code
const CLIENT_SECRET = "abc123"; // Anyone can decompile/inspect and find this!

// Attacker can:
// 1. Extract CLIENT_SECRET from app
// 2. Intercept authorization code
// 3. Exchange code for tokens using stolen secret
```

## How PKCE Works

PKCE prevents authorization code interception attacks **without requiring a client secret**.

### Flow Diagram

```
┌─────────────┐                 ┌──────────────┐                 ┌──────────────┐
│ Mobile App  │                 │ Zoom OAuth   │                 │   Attacker   │
│  (Public)   │                 │   Server     │                 │ (Intercepting│
└──────┬──────┘                 └──────┬───────┘                 └──────┬───────┘
       │                               │                                │
       │ 1. Generate code_verifier     │                                │
       │    (random 43-128 chars)      │                                │
       │                               │                                │
       │ 2. Create code_challenge       │                                │
       │    SHA256(code_verifier)      │                                │
       │                               │                                │
       │ 3. Authorize with challenge   │                                │
       │https://zoom.us/oauth/authorize?│                                │
       │    code_challenge={HASH}      │                                │
       │    code_challenge_method=S256 │                                │
       │──────────────────────────────>│                                │
       │                               │                                │
       │ 4. User authorizes            │                                │
       │──────────────────────────────>│                                │
       │                               │                                │
       │ 5. Return authorization code  │                                │
       │<──────────────────────────────│                                │
       │                               │                                │
       │                               │   Attacker intercepts code     │
       │                               │<───────────────────────────────│
       │                               │                                │
       │                               │   Attacker tries to exchange   │
       │                               │   (but doesn't have verifier!) │
       │                               │<───────────────────────────────│
       │                               │                                │
       │                               │   REJECTED: Missing verifier   │
       │                               │───────────────────────────────>│
       │                               │                                │
       │ 6. Exchange code with verifier│                                │
       │  POST /oauth/token            │                                │
       │    code={CODE}                │                                │
       │    code_verifier={ORIGINAL}   │                                │
       │──────────────────────────────>│                                │
       │                               │                                │
       │                               │ Verify:                        │
       │                               │ SHA256(code_verifier)          │
       │                               │   == code_challenge?           │
       │                               │                                │
       │ 7. Return tokens              │                                │
       │<──────────────────────────────│                                │
       │                               │                                │
```

### Key Concept

- **code_verifier:** Random secret generated by app (kept secret)
- **code_challenge:** SHA256 hash of code_verifier (sent to Zoom)
- **Zoom stores challenge:** During authorization
- **App proves possession:** By providing original verifier during token exchange
- **Attacker fails:** Even with intercepted code, can't generate matching verifier

---

## Implementation

### Step 1: Generate PKCE Parameters

```javascript
const crypto = require('crypto');

function generatePKCE() {
  // Generate random code_verifier (43-128 characters)
  const verifier = crypto
    .randomBytes(32)
    .toString('base64url'); // base64url encoding (no padding)
  
  // Create code_challenge: SHA256(code_verifier)
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
  
  return {
    code_verifier: verifier,
    code_challenge: challenge
  };
}

// Example output:
// {
//   code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
//   code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
// }
```

### Step 2: Store code_verifier Securely

```javascript
// Store in session (server-side) or secure storage (mobile)
req.session.pkce_verifier = code_verifier;

// For mobile apps, use secure storage:
// - iOS: Keychain
// - Android: EncryptedSharedPreferences
```

### Step 3: Redirect to Authorization with code_challenge

```javascript
app.get('/auth', (req, res) => {
  const { code_verifier, code_challenge } = generatePKCE();
  
  // Store verifier for later (Step 5)
  req.session.pkce_verifier = code_verifier;
  
  const authURL = new URL('https://zoom.us/oauth/authorize');
  authURL.searchParams.set('response_type', 'code');
  authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID);
  authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL);
  authURL.searchParams.set('code_challenge', code_challenge);
  authURL.searchParams.set('code_challenge_method', 'S256'); // SHA256
  
  res.redirect(authURL.toString());
});
```

### Step 4: Exchange Code with code_verifier

```javascript
app.get('/callback', async (req, res) => {
  const { code } = req.query;
  const code_verifier = req.session.pkce_verifier; // Retrieve stored verifier
  
  try {
    const response = await axios.post(
      'https://zoom.us/oauth/token',
      qs.stringify({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: process.env.ZOOM_REDIRECT_URL,
        code_verifier: code_verifier // Prove possession of original verifier
      }),
      {
        headers: {
          'Authorization': `Basic ${Buffer.from(
            `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
          ).toString('base64')}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    const { access_token, refresh_token } = response.data;
    
    // Success! Store tokens
    await saveTokens({ access_token, refresh_token });
    
    // Clean up verifier
    delete req.session.pkce_verifier;
    
    res.send('Authorization successful!');
  } catch (error) {
    res.status(500).send('Token exchange failed');
  }
});
```

---

## PKCE Methods

Zoom supports two code_challenge_method values:

| Method | Description | Security | Support |
|--------|-------------|----------|---------|
| **S256** | SHA256 hash of verifier | ✅ **Recommended** | All OAuth 2.0 servers |
| **plain** | Verifier sent as-is (no hash) | ⚠️ Weaker | Legacy support only |

**Always use S256:**

```javascript
// ✅ RECOMMENDED
authURL.searchParams.set('code_challenge_method', 'S256');
```

```javascript
// ❌ AVOID (less secure)
authURL.searchParams.set('code_challenge_method', 'plain');
```

---

## Security Benefits

### Without PKCE (Vulnerable)

```
Attacker intercepts authorization code
   ↓
Attacker exchanges code with client_secret
   ↓
Attacker gets access_token and refresh_token
   ↓
Attacker has full account access
```

### With PKCE (Protected)

```
Attacker intercepts authorization code
   ↓
Attacker tries to exchange code
   ↓
Zoom: "Provide code_verifier"
   ↓
Attacker doesn't have original verifier
   ↓
Token exchange FAILS
   ↓
Legitimate app exchanges with correct verifier
   ↓
Legitimate app gets tokens
```

---

## Common Mistakes

### 1. Not Storing code_verifier

```javascript
// ❌ WRONG: Generating new verifier on callback
app.get('/callback', async (req, res) => {
  const { code_challenge } = generatePKCE(); // New verifier!
  // This won't match the original challenge
});
```

```javascript
// ✅ CORRECT: Retrieve stored verifier
app.get('/callback', async (req, res) => {
  const code_verifier = req.session.pkce_verifier; // Original verifier
});
```

### 2. Using 'plain' Method

```javascript
// ❌ AVOID
code_challenge_method: 'plain' // Less secure
```

```javascript
// ✅ RECOMMENDED
code_challenge_method: 'S256' // SHA256 hashing
```

### 3. Exposing code_verifier

```javascript
// ❌ WRONG: Including verifier in URL
authURL.searchParams.set('code_verifier', verifier); // Don't send verifier during auth!
```

```javascript
// ✅ CORRECT: Only send code_challenge
authURL.searchParams.set('code_challenge', challenge);
authURL.searchParams.set('code_challenge_method', 'S256');
```

---

## Mobile App Implementation

### iOS (Swift)

```swift
import CryptoKit

func generatePKCE() -> (verifier: String, challenge: String) {
    // Generate random verifier
    var buffer = [UInt8](repeating: 0, count: 32)
    _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
    let verifier = Data(buffer).base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
    
    // Create SHA256 challenge
    let data = verifier.data(using: .utf8)!
    let hash = SHA256.hash(data: data)
    let challenge = Data(hash).base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
    
    return (verifier, challenge)
}

// Store verifier in Keychain
KeychainWrapper.standard.set(verifier, forKey: "pkce_verifier")
```

### Android (Kotlin)

```kotlin
import java.security.MessageDigest
import java.security.SecureRandom
import android.util.Base64

fun generatePKCE(): Pair<String, String> {
    // Generate random verifier
    val bytes = ByteArray(32)
    SecureRandom().nextBytes(bytes)
    val verifier = Base64.encodeToString(bytes, 
        Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
    
    // Create SHA256 challenge
    val digest = MessageDigest.getInstance("SHA-256")
    val hash = digest.digest(verifier.toByteArray())
    val challenge = Base64.encodeToString(hash,
        Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
    
    return Pair(verifier, challenge)
}

// Store verifier in EncryptedSharedPreferences
val encryptedPrefs = EncryptedSharedPreferences.create(...)
encryptedPrefs.edit().putString("pkce_verifier", verifier).apply()
```

---

## Testing PKCE Implementation

### 1. Verify code_challenge Format

```javascript
const { code_verifier, code_challenge } = generatePKCE();

console.log('Verifier length:', code_verifier.length); // Should be 43-128
console.log('Challenge length:', code_challenge.length); // Should be 43 for S256
console.log('Verifier chars:', /^[A-Za-z0-9_-]+$/.test(code_verifier)); // true
console.log('Challenge chars:', /^[A-Za-z0-9_-]+$/.test(code_challenge)); // true
```

### 2. Verify SHA256 Hashing

```javascript
const crypto = require('crypto');

const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";

const computed_challenge = crypto
  .createHash('sha256')
  .update(verifier)
  .digest('base64url');

console.log(computed_challenge === expected_challenge); // Should be true
```

### 3. Test Token Exchange

```bash
curl -X POST https://zoom.us/oauth/token \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=YOUR_AUTH_CODE" \
  -d "redirect_uri=YOUR_REDIRECT_URI" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

# Should return { access_token, refresh_token, ... }
```

---

## PKCE Specification

PKCE follows **RFC 7636**:
- https://datatracker.ietf.org/doc/html/rfc7636

---

## Next Steps

- **Implement PKCE in your app** → [../examples/pkce-implementation.md](../examples/pkce-implementation.md)
- **Add state parameter** → [state-parameter.md](state-parameter.md)
- **Understand OAuth flows** → [oauth-flows.md](oauth-flows.md)
