PKCE (Proof Key for Code Exchange)
PKCE ("pixie" diye okunur), OAuth 2.0 authorization code flow'una eklenen bir güvenlik katmanıdır. Authorization code interception saldırılarını engeller.
Neden PKCE Gerekli?
Problem: Authorization Code Interception
Klasik OAuth flow'da authorization code URL'de döner:
https://myapp.com/callback?code=SENSITIVE_CODE&state=xyz
Bu code yakalanabilir:
- 🔴 Mobil cihazlarda custom URL scheme hijacking
- 🔴 Browser history
- 🔴 Referrer header
- 🔴 Proxy logs
Saldırgan code'u ele geçirip kendi client_secret'ıyla token alabilir (confidential client'larda).
Çözüm: PKCE
PKCE, her authorization isteği için tek kullanımlık bir gizli anahtar oluşturur. Code intercept edilse bile, saldırgan bu anahtara sahip olmadığı için token alamaz.
PKCE Nasıl Çalışır?
┌──────────┐ ┌──────────────┐
│ Client │ │ Atlantic ID │
└────┬─────┘ └──────┬───────┘
│ │
│ 1. Generate Random Verifier │
│ verifier = random_43_128_chars() │
│ │
│ 2. Create Challenge │
│ challenge = base64url(sha256(verifier)) │
│ │
│ 3. Authorization Request │
│ + code_challenge │
│ + code_challenge_method=S256 │
│ ────────────────────────────────────────────▶
│ │
│ Atlantic ID stores │
│ challenge for this code │
│ │
│ 4. Returns authorization code │
│◀────────────────────────────────────────────│
│ │
│ 5. Token Request │
│ + code │
│ + code_verifier (original random) │
│ ────────────────────────────────────────────▶
│ │
│ Atlantic ID verifies: │
│ sha256(code_verifier) == stored_challenge
│ │
│ 6. Returns tokens (if verified) │
│◀────────────────────────────────────────────│
Implementation
Step 1: Generate Code Verifier
Gereksinimler:
- 43-128 karakter arası
[A-Z],[a-z],[0-9],-,.,_,~karakterleri- Kriptografik olarak güvenli rastgele
JavaScript/TypeScript
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
const verifier = generateCodeVerifier();
// Örnek: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Python
import secrets
import base64
def generate_code_verifier():
code_verifier = base64.urlsafe_b64encode(
secrets.token_bytes(32)
).decode('utf-8')
return code_verifier.rstrip('=')
verifier = generate_code_verifier()
PHP
function generateCodeVerifier(): string {
$randomBytes = random_bytes(32);
return rtrim(strtr(base64_encode($randomBytes), '+/', '-_'), '=');
}
$verifier = generateCodeVerifier();
Java
import java.security.SecureRandom;
import java.util.Base64;
public static String generateCodeVerifier() {
SecureRandom secureRandom = new SecureRandom();
byte[] code = new byte[32];
secureRandom.nextBytes(code);
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(code);
}
Go
import (
"crypto/rand"
"encoding/base64"
)
func generateCodeVerifier() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
.NET/C
using System;
using System.Security.Cryptography;
public static string GenerateCodeVerifier()
{
var bytes = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
Step 2: Generate Code Challenge
Code verifier'ın SHA256 hash'ini alın ve base64url encode edin.
JavaScript/TypeScript
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
const challenge = await generateCodeChallenge(verifier);
// Örnek: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Python
import hashlib
def generate_code_challenge(verifier):
digest = hashlib.sha256(verifier.encode('utf-8')).digest()
challenge = base64.urlsafe_b64encode(digest).decode('utf-8')
return challenge.rstrip('=')
challenge = generate_code_challenge(verifier)
PHP
function generateCodeChallenge(string $verifier): string {
$hash = hash('sha256', $verifier, true);
return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
}
$challenge = generateCodeChallenge($verifier);
Java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public static String generateCodeChallenge(String verifier)
throws NoSuchAlgorithmException {
byte[] bytes = verifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(bytes);
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(digest);
}
Go
import (
"crypto/sha256"
"encoding/base64"
)
func generateCodeChallenge(verifier string) string {
h := sha256.New()
h.Write([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}
.NET/C
using System.Security.Cryptography;
using System.Text;
public static string GenerateCodeChallenge(string verifier)
{
using (var sha256 = SHA256.Create())
{
var hash = sha256.ComputeHash(
Encoding.UTF8.GetBytes(verifier)
);
return Convert.ToBase64String(hash)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}
Step 3: Store Code Verifier
Authorization request yapmadan önce verifier'ı güvenli bir yere kaydedin:
Browser (Frontend)
// Session Storage (önerilen - tab kapanınca sil in ir)
sessionStorage.setItem('pkce_verifier', verifier);
// Local Storage (KULLANMAYIN - güvensiz)
// localStorage.setItem('pkce_verifier', verifier);
Backend (Server-Side)
// Express.js
req.session.pkceVerifier = verifier;
// PHP
$_SESSION['pkce_verifier'] = $verifier;
// Django
request.session['pkce_verifier'] = verifier
Mobile (Native Apps)
// iOS - Keychain
KeychainService.save(key: "pkce_verifier", value: verifier)
// Android - EncryptedSharedPreferences
EncryptedSharedPreferences.edit()
.putString("pkce_verifier", verifier)
.apply()
Step 4: Authorization Request
GET /oauth/authorize?
client_id=cli_abc123&
response_type=code&
scope=openid%20profile%20email&
redirect_uri=https://myapp.com/callback&
state=xyz789&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256 HTTP/1.1
Host: id.codeatlantis.com
Parametreler:
code_challenge: Hesaplanan challenge değericode_challenge_method:S256(SHA256) veyaplain(önerilmez)
Step 5: Token Exchange with Verifier
Authorization code aldıktan sonra, token exchange'de orijinal code verifier'ı gönderin:
POST /oauth/token HTTP/1.1
Host: id.codeatlantis.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AQCxxx...xxx&
redirect_uri=https://myapp.com/callback&
client_id=cli_abc123&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Atlantic ID Doğrulama:
// Pseudo-code
stored_challenge = db.get_challenge(code)
calculated_challenge = base64url(sha256(code_verifier))
if (calculated_challenge !== stored_challenge) {
return error('invalid_grant')
}
Code Challenge Methods
Atlantic ID iki method destekler:
S256 (Önerilen)
SHA256 hash kullanır:
challenge = base64url(sha256(verifier))
Avantajları:
- ✅ Kriptografik olarak güvenli
- ✅ Verifier'ı tersine çevirmek imkansız
- ✅ OAuth 2.1'de zorunlu
Plain (Önerilmez)
Verifier'ı direkt gönderir:
challenge = verifier
Dezavantajları:
- ❌ Challenge yakalanırsa verifier'da yakalanmış olur
- ❌ Ek güvenlik sağlamaz
- ❌ Sadece eski client'lar için backward compat
Uyarı: Production'da her zaman
S256kullanın!
PKCE Zorunluluğu
Atlantic ID'de PKCE kullanımı:
Public Clients
- ✅ ZORUNLU
- Client secret olmadığı için PKCE tek koruma
- PKCE olmadan request reddedilir
Confidential Clients
- 📝 ÖNERİLEN
- Client secret zaten var ama PKCE ekstra koruma sağlar
- Defense-in-depth prensibi
Güvenlik Faydaları
1. Authorization Code Interception Koruması
Code yakalanabilir ama verifier olmadan kullanılamaz:
Saldırgan:
✅ Authorization code'u ele geçirdi
❌ Code verifier'a erişemedi
❌ Token alamadı
2. Client Impersonation Koruması
Başka bir client'ın code'unu kullanamazsınız:
Meşru Client A:
code_challenge_A = sha256(verifier_A)
Saldırgan Client B:
Tries: code + verifier_B
Result: sha256(verifier_B) ≠ code_challenge_A
Error: invalid_grant
3. Replay Attack Koruması
Her verifier tek kullanımlık:
1st Request: code + verifier_1 → Success
2nd Request: code + verifier_1 → Error (code already used)
Common Mistakes
❌ Verifier'ı Tekrar Kullanma
// YANLIŞ
const STATIC_VERIFIER = "always_same_verifier";
Her authorization request için yeni verifier oluşturun!
❌ Verifier'ı URL'de Göndermek
// YANLIŞ
window.location.href = `/callback?code=${code}&verifier=${verifier}`;
Verifier asla URL'de olmamalı (query param, hash, vb.).
❌ Plain Method Kullanmak
// YANLIŞ
code_challenge_method: 'plain'
Her zaman S256 kullanın.
❌ Verifier'ı LocalStorage'da Saklamak
// YANLIŞ
localStorage.setItem('verifier', verifier);
XSS saldırılarına açık. sessionStorage veya backend session kullanın.
Testing PKCE
Online Tools
Generate PKCE Pair:
Manual Testing
# 1. Generate verifier
VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
echo "Verifier: $VERIFIER"
# 2. Generate challenge
CHALLENGE=$(echo -n $VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_')
echo "Challenge: $CHALLENGE"
# 3. Test authorization
curl "https://id.codeatlantis.com/oauth/authorize?\
client_id=cli_abc123&\
response_type=code&\
scope=openid&\
redirect_uri=http://localhost:3000/callback&\
code_challenge=$CHALLENGE&\
code_challenge_method=S256"
# 4. Exchange token
curl -X POST https://id.codeatlantis.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=$CODE" \
-d "client_id=cli_abc123" \
-d "redirect_uri=http://localhost:3000/callback" \
-d "code_verifier=$VERIFIER"
Troubleshooting
invalid_request - code_challenge required
Sorun: PKCE parametreleri eksik
Çözüm:
// Şunları eklediğinizden emin olun:
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
invalid_grant - code_verifier verification failed
Sorun: Verifier eşleşmiyor
Nedenleri:
- Challenge oluştururken farklı verifier kullanıldı
- Verifier storage'dan yanlış alındı
- Base64 encoding hatalı
Çözüm:
// Same verifier for challenge and exchange
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
sessionStorage.setItem('pkce_verifier', verifier); // Save it!
// Later...
const savedVerifier = sessionStorage.getItem('pkce_verifier');
// Use savedVerifier in token exchange