File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/oauth/concepts/pkce.md
# 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)