04.2 — SPA ve PWA Uygulamaları (Public Client + BFF Önerisi)
Kimin için: React, Vue, Svelte, Angular veya saf JavaScript ile geliştirilen Tek Sayfa Uygulamaları (SPA) ve Progresif Web Uygulamaları (PWA).
Ön koşul: Temel Kavramlar ve bir Public OAuth İstemcisi.
Tahmini süre: 15 dakika.
Ne Öğreneceksiniz
- Tarayıcıda çalışan uygulamalar için Public Client mimarisini,
- Güvenlik açısından Backend-for-Frontend (BFF) modelinin neden önerildiğini,
- Saf SPA senaryosunda Web Crypto API ile istemci taraflı PKCE S256 üretimini.
Mimari Seçenekleri: BFF vs. Saf SPA
Tarayıcı ortamında istemci gizli anahtarı (client_secret) saklanamaz. Çünkü kullanıcı veya saldırgan kaynak kodları inceleyerek anahtarı elde edebilir.
text
ÖNERİLEN: Backend-for-Frontend (BFF) Modeli
[Tarayıcı: React/Vue] <--- Şifreli HttpOnly Cookie ---> [BFF Sunucusu (Node/Next)] <--- Token / Secret ---> [TALOST ID]
Mimari Tavsiyesi
Mümkün olan her durumda bir BFF katmanı (örneğin Next.js API Routes veya hafif bir Node.js proxy) kullanmanız önerilir. Böylece tokenlar tarayıcı belleğine veya `localStorage`'a düşmez, XSS saldırılarına karşı tam koruma sağlanır.
Mümkün olan her durumda bir BFF katmanı (örneğin Next.js API Routes veya hafif bir Node.js proxy) kullanmanız önerilir. Böylece tokenlar tarayıcı belleğine veya `localStorage`'a düşmez, XSS saldırılarına karşı tam koruma sağlanır.
Saf SPA Akışı (Public Client + PKCE)
Eğer bir arka uç sunucunuz yoksa (örneğin tamamen statik bir S3/Cloudflare Pages sitesi):
- Portalde istemcinin Confidential seçeneğini kapalı tutun (Public Client).
- Portaldeki İzin Verilen Kaynaklar (Allowed Origins) alanına SPA sitenizin kök adresini (
https://app.example.com) ekleyin (CORS desteği için).
Kod Örneği: Tarayıcı Taraflı PKCE Üretimi (Web Crypto API)
Modern tarayıcılarda harici kütüphane gerektirmeden PKCE S256 üretimi:
typescript
// pkce.ts
export async function generatePKCE() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
const verifier = base64UrlEncode(array);
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await window.crypto.subtle.digest("SHA-256", data);
const challenge = base64UrlEncode(new Uint8Array(hash));
const state = Math.random().toString(36).substring(2) + Date.now().toString(36);
return { verifier, challenge, state };
}
function base64UrlEncode(buffer: Uint8Array): string {
let str = "";
for (let i = 0; i < buffer.byteLength; i++) {
str += String.fromCharCode(buffer[i]);
}
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// Giriş Başlatma
export async function startLogin(issuer: string, clientId: string, redirectUri: string) {
const { verifier, challenge, state } = await generatePKCE();
// Geçici olarak sessionStorage'da sakla
sessionStorage.setItem("talost_pkce_verifier", verifier);
sessionStorage.setItem("talost_oauth_state", state);
const authUrl = new URL(`${issuer}/oauth/authorize`);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", clientId);
authUrl.searchParams.set("redirect_uri", redirectUri);
authUrl.searchParams.set("scope", "openid profile email");
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("code_challenge", challenge);
authUrl.searchParams.set("code_challenge_method", "S256");
window.location.href = authUrl.toString();
}
// Callback Sayfasında Token Takası
export async function handleCallback(issuer: string, clientId: string, redirectUri: string) {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");
const savedState = sessionStorage.getItem("talost_oauth_state");
const verifier = sessionStorage.getItem("talost_pkce_verifier");
if (!code || !state || state !== savedState || !verifier) {
throw new Error("Geçersiz state veya PKCE verifier.");
}
// Token Takası (Public client olduğu için client_secret gönderilmez)
const tokenRes = await fetch(`${issuer}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: clientId,
code,
redirect_uri: redirectUri,
code_verifier: verifier,
}),
});
sessionStorage.removeItem("talost_pkce_verifier");
sessionStorage.removeItem("talost_oauth_state");
const tokens = await tokenRes.json();
if (!tokenRes.ok) throw new Error(tokens.error_description || "Token alınamadı.");
return tokens;
}
Sık Yapılan Hatalar
- Token'ları
localStorage'da saklamak: XSS saldırısı durumunda sitenizdeki herhangi bir üçüncü parti scriptlocalStorage.getItem("token")ile kullanıcının oturumunu çalabilir. Token'ları yalnızca bellekte (memory) veya oturum süresince geçerli güvenli depolama alanlarında tutun. - Allowed Origins Tanımlamamak: SPA'dan
POST /oauth/tokenisteği atıldığında tarayıcı konsolunda CORS hatası alıyorsanız, Geliştirici Portalından ilgili istemcinin İzin Verilen Kaynaklar (Allowed Origins) listesine adresinizi ekleyin.
Güvenlik Notları
- Public istemcilerde
client_secretkullanmayın. - Yetkilendirme kodu (
code) URL barında kalmamalıdır; token takası tamamlandığı andawindow.history.replaceState({}, document.title, window.location.pathname)ile temizlenmelidir.
İlgili Sayfalar
- 04.1. Web Sunucu Taraflı Giriş
- 05. Tokenlar ve Oturum Yönetimi
- 14. Güvenlik Kuralları (Asla Yapmayın)
Son doğrulama: 2026-10-04 · Kaynak: _envanter.md, src/app/oauth/token/route.ts
Son doğrulama: 2026-10-04 · Kaynak: `_envanter.md`, `src/app/oauth/token/route.ts`