Normal tarayıcı yalıtımını zayıflatmadan birden fazla bağımsız web sitesinin tek bir hesap sistemi gibi davranmasına ihtiyacım vardı. Kullanıcı bir etki alanında oturum açabilmeli, daha sonra başka bir etki alanını doğrudan açıp Giriş Yap düğmesine tıklayarak kimlik bilgilerini yeniden girmeden orada devam edebilmeliydi.
Önemli kısıt, bunların bağımsız kök etki alanları olmasıydı. Bir kök etki alanı tarafından oluşturulan oturum çerezi, ilgisiz başka bir kök etki alanını kapsayacak şekilde ayarlanamaz. Sıkça birbirine karıştırılan iki kavramı ayırdığımda tasarımı anlamlandırmak çok daha kolaylaştı: global kimlik ve yerel tarayıcı oturumu.
Ortaya çıkan model basit: backend tek bir ortak hesap kimliğini tanır, her etki alanı kendi host'una bağlı oturuma sahip olur ve kısa ömürlü, tek kullanımlık bir aktarım, kimliği doğrulanmış güvenilir bir origin'in başka bir güvenilir origin üzerinde yeni bir oturum oluşturulmasına yetki vermesini sağlar.
Global kimlik, ortak bir çerez gerektirmez
Tarayıcı oturumları yerel olsa bile hesap global olabilir.
Her site yalnızca kendi host'una ait bir oturum çerezi tutar. Bir kök etki alanının verdiği çerez, Domain niteliğiyle ilgisiz başka bir kök etki alanına genişletilemez. Bu nitelik, bir çerezin kapsamını yalnızca çerezi veren host'un kullanmasına izin verilen etki alanı hiyerarşisi içinde genişletebilir.
Tek bir host'a bağlı kalması gereken bir oturum için __Host- önekli bir çerez uygun bir seçimdir. Kavramsal olarak:
Set-Cookie: __Host-<session-cookie>=<opaque-token>; Path=/; Secure; HttpOnly; SameSite=LaxÖnemli olan sınırların şu şekilde olmasıdır:
- Site A, Site B'nin yalnızca host'una ait oturum çerezini okuyamaz.
- Site B, Site A'nın yalnızca host'una ait oturum çerezini okuyamaz.
- Her iki oturum da yine aynı backend kimliğine çözümlenebilir.
Dolayısıyla etki alanları arası SSO, bir çerezi bir siteden diğerine taşımaya çalışmamalıdır. Bunun yerine, güvenilir bir kaynağın kullanıcıyı zaten doğruladığını gösteren geçici bir kanıt aktarılmalıdır.
Tek kullanımlık aktarım kalıbı
Kimliği doğrulanmış kaynak, küçük bir aktarım işlemi sunar. Tarayıcı bunu yalnızca siteler arası bir kimlik doğrulama geçişi gerektiğinde çağırır.
Backend kriptografik olarak rastgele bir ticket üretir, ham değeri tarayıcıya yalnızca bir kez döndürür ve aktarımı doğrulamak için gereken en az bilgiyle birlikte yalnızca hash değerini saklar:
{
tokenHash,
userId,
sourceOrigin,
targetSite,
returnPath,
expiresAt
}Ticket kısa ömürlü olmalı ve yalnızca tek bir hedef için geçerli olmalıdır. Kaynak taraftaki üretim isteği zaten kimliği doğrulanmış olmalı ve CSRF'ye karşı korunmalıdır. İstenen hedef ve dönüş adresi herhangi bir şey üretilmeden önce doğrulanmalıdır.
Tarayıcı daha sonra hedefe üst düzey bir POST gerçekleştirir. Hedef ticket'ı tüketir, ortak kullanıcı kimliğini çözümler, kendi yerel oturumunu oluşturur ve tarayıcıyı amaçlanan yola yönlendirir.
Kavramsal olarak:
record = consumeOnce({
tokenHash: hash(ticket),
targetSite: currentSite,
sourceOrigin: requestOrigin,
notExpired: true
})
if (!record) deny()
user = loadSharedUser(record.userId)
createLocalSession(user)
redirect(record.returnPath)Ticket hedef oturumun kendisi değildir. O hedef oturumu oluşturmak için tek kullanımlık bir yetkilendirmedir.
Atomik tüketim replay penceresini kapatır
Daha zayıf bir uygulama ticket'ı yükler, doğrular, bir oturum oluşturur ve ticket'ı daha sonra silerdi. Bu, iki isteğin aynı hâlâ geçerli kaydı görebileceği bir pencere oluşturur.
Daha güvenli tasarım, doğrulama ile geçersiz kılmayı tek bir atomik tüketim işleminde birleştirmektir. Eşleşen kayıt yoksa kimlik doğrulama başarısız olur. Bir kayıt dönerse, oturum oluşturulmadan önce o ticket zaten kullanılabilir kümeden çıkarılmıştır.
Bu ayrıca hedefin aynı işleme birkaç koşulu bağlamasını sağlar: beklenen hedef, beklenen kaynak origin, token hash'i ve son kullanma zamanı.
Origin doğrulaması fail-closed olmalıdır
Origin'e bağlama ek bir yararlı özellik kazandırır: güvenilir bir kaynak üzerinden üretilen ticket, ilgisiz bir origin üzerinden tüketilememelidir.
Origin kontrolü tüketim koşulunun kendisinin parçası olmalıdır. Böylece yanlış origin'den gelen bir istek geçerli ticket'ı tüketmez; meşru kaynak ise daha sonra aktarımı tamamlayabilir.
Önemli bir istisna vardır: Origin başlığının her tarayıcı bağlamında normal bir origin dizesi içermesi garanti edilmez. Bazı istekler kullanılabilir bir değer olmadan gelebilir; opak bağlamlar ise Origin: null üretebilir.
Benim kuralım, uygulamada açıkça tanımlanmış alternatif bir doğrulama mekanizması yoksa eksik veya opak bir Origin değerini başarısız origin kontrolü saymaktır. Gerçek null değeri sırf uyumluluk için güvenilir origin hâline gelmemelidir.
Bu, fail-closed bir tercihtir. Meşru bir akışın kullanılabilir origin bulunmayan bir bağlamı desteklemesi gerekiyorsa, genel kontrol sessizce zayıflatılmak yerine bu istisna açıkça tasarlanmalıdır.
Aktarım kimlik bilgisini URL'nin dışında tutun
Geçici ticket'ı sorgu dizesi içine koymak yerine üst düzey bir form POST ile göndermeyi tercih ediyorum.
Genelleştirilmiş bir istemci tarafı sürümü şöyledir:
const form = document.createElement('form');
form.method = 'POST';
form.action = targetOrigin + '/auth/consume';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'ticket';
input.value = ticket;
form.append(input);
document.body.append(form);
form.submit();Hedef, ticket'ı beklenen POST gövdesinden kabul eder ve aynı değeri URL üzerinden verme girişimlerini reddeder.
Bu, kimlik bilgisini zararsız hâle getirmez; ancak onu normal gezinme URL'lerinin dışında tutar ve URL geçmişi, kopyalanmış bağlantılar, referrer'lar ve URL odaklı loglama yoluyla kazara açığa çıkma riskini azaltır.
Kimliği doğrulanmış siteler arası bağlantılar gerektiğinde aktarımı devreye alabilir
Mevcut site kullanıcının kimliğinin doğrulandığını zaten biliyorsa, başka bir güvenilir siteye giden normal bir bağlantı fırsat bulduğunda aktarımı gerçekleştirebilir.
Bağlantı yine de gerçek bir href içermelidir. SSO, normal gezinmenin yerine geçen bir şey değil, ona eklenen bir iyileştirmedir.
Basitleştirilmiş istemci akışı şöyle görünür:
if (session.status !== 'authenticated') {
return; // ordinary browser navigation
}
if (!isTrustedTarget(destination)) {
return;
}
event.preventDefault();
const ticket = await requestHandoff({
targetSite,
returnPath: destinationPath
});
postTicketToTarget(ticket);Üretim sürümü ayrıca yinelenen gönderimlere karşı koruma sağlamalı, hedefi güvenilir bir kayıt defterine göre doğrulamalı ve aktarım oluşturulamazsa normal gezinmeye geri dönmelidir.
Tarayıcının doğal davranışı doğal kalmalıdır. Orta tuşla tıklama, değiştirici tuşlarla tıklama, indirmeler ve başka bir tarama bağlamı için tasarlanmış bağlantılar sessizce kimlik doğrulama isteklerine dönüştürülmemelidir.
İkinci sitedeki Giriş Yap düğmesinin bilinen bir yayımlayıcıya ihtiyacı vardır
Daha zor durum, kullanıcı Site B'yi doğrudan açtığında başlar. Site B'de yerel oturum yoktur; bu nedenle tarayıcıda Site A için hâlâ geçerli bir oturum bulunsa bile ziyaretçiyi doğru biçimde anonim sayar.
Site B, Site A'nın yalnızca host'una ait çerezini inceleyemez. Ayrıca rastgele bir Site C'ye yönlendirip bu sitenin sistemin geri kalanına ait oturumları keşfetmesini bekleyemez. Birbiriyle ilgisiz her kök etki alanı yalnızca kendisine ait çerezleri görebilir.
Bu nedenle anonim bir Giriş Yap akışının bilinen bir yayımlayıcıya ihtiyacı vardır: ya kanonik bir kimlik doğrulama origin'i ya da yeniden kullanılabilir giriş oturumunu tuttuğu varsayılan, özel olarak seçilmiş güvenilir bir site.
Basit Site A → Site B durumu için sıra şöyledir:
- Kullanıcı Site A'da oturum açar.
- Daha sonra kullanıcı Site B'yi doğrudan açar.
- Site B'de oturum yoktur, bu yüzden Giriş Yap gösterir.
- Kullanıcı Giriş Yap düğmesine tıklar.
- Tarayıcı, seçilen yayımlayıcı olan Site A'ya üst düzey gezinme gerçekleştirir.
- Site A kendi mevcut, yalnızca host'una ait oturum çerezini okuyabilir.
- Bu oturum hâlâ geçerli olduğu için Site A, Site B için hemen tek kullanımlık bir aktarım üretir.
- Tarayıcı ticket'ı Site B'ye POST eder.
- Site B kendi yerel oturumunu oluşturur ve kullanıcıyı istenen sayfaya geri döndürür.
Kullanıcının gözünden bu, Site A'ya gidip geri gelen hızlı bir yönlendirmedir. Parola gerekmez; çünkü Site A zaten görmesine izin verilen oturuma sahiptir.
Sınırlama da aynı derecede önemlidir: kullanıcı yalnızca Site C'de oturum açmışsa, Site B'yi Site A'ya yönlendirmek işe yaramaz; ancak Site A, giriş durumuna ilişkin kendi bağımsız bilgisine sahip gerçek bir merkezi kimlik doğrulama aracıysa durum değişir. Ortak bir kullanıcı veritabanı, tarayıcı oturumlarını ortak hâle getirmez.
Kanonik bir yayımlayıcı sistemi neden basitleştirir
Bağımsız site sayısı arttıkça kanonik bir yayımlayıcı, rastgele etki alanları arasında oturum keşfetme ihtiyacını ortadan kaldırır.
Böyle bir yayımlayıcı olmadan anonim Site B, şu anda hangi başka sitede işe yarar bir çerez bulunabileceğine bir şekilde karar vermek zorunda kalırdı. Site A, Site C'nin çerezini inceleyemez; Site C, Site A'nın çerezini inceleyemez; Site B ise ikisini de inceleyemez.
Kanonik yayımlayıcı, her anonim giriş denemesine tek ve öngörülebilir bir yol verir:
Site B
→ authentication issuer
→ issuer session exists?
yes → create one-time handoff
no → show normal login
→ Site B consumes handoff
→ Site B creates local sessionYayımlayıcının uygulamanın geri kalanını sunması gerekmez. İlgili sorumluluğu, yeniden kullanılabilir kimlik doğrulama oturumuna sahip olmak ve güvenilir sitelere dar kapsamlı aktarımlar üretmektir.
Daha küçük bir sistemde mevcut sitelerden biri bu rolü üstlenebilir. Daha geniş bir mimaride, özel bir kimlik doğrulama origin'i güven modelini anlamayı kolaylaştırabilir.
Her ziyaretçiyi yayımlayıcı üzerinden sektirmeyin
Yayımlayıcı akışı her anonim sayfa görüntülemesinde otomatik olarak çalışmamalıdır.
Sırf mevcut bir oturum var mı diye kontrol etmek için her ziyaretçiyi başka bir etki alanına yönlendirmek gereksiz gezinme, ek gecikme, daha fazla hata modu, daha gürültülü analitik ve daha karmaşık önbellek davranışı yaratır.
Bu, herkese açık ve SEO açısından hassas sayfalar için de kötü bir uyumdur. Herkese açık bir URL isteyen tarama botu veya sıradan ziyaretçi, önce bir kimlik doğrulama dolambaçlı yolundan geçmeye zorlanmak yerine o herkese açık sayfayı doğrudan almalıdır.
Benim kullandığım kural şu:
Sayfa yüklenirken yayımlayıcıyı yoklamayın. Etki alanları arası kimlik doğrulamayı yalnızca açık bir Giriş Yap eyleminden veya bilinçli, kimliği doğrulanmış bir siteler arası gezinmeden sonra başlatın.
Bu, herkese açık trafiği basit tutarken seçilen yayımlayıcıda zaten geçerli oturumu olan kullanıcı için kimlik doğrulamayı yine hızlı kılar.
Dönüş yolu doğrulaması bir önek kontrolünden daha fazla özen ister
Aktarımın genellikle tarayıcının daha sonra nereye gitmesi gerektiğini hatırlaması gerekir. Bu değer, istemci tarafından kontrol edilen rastgele bir URL gibi ele alınırsa açık yönlendirmeye dönüşebilir.
Mümkün olduğunda en güvenli tasarım, tam hedef URL'yi hiç kabul etmemektir. Bilinen hedefler için kısa bir tanımlayıcı veya sunucu tarafı eşleme, gereken URL ayrıştırma ve doğrulama miktarını azaltır.
Göreli bir yol kabul edilmek zorundaysa, dikkatle doğrulayın ve hedef siteyle sınırlı tutun.
Bilerek basitleştirilmiş bir örnek — eksiksiz bir yönlendirme doğrulama yordamı değildir — şöyledir:
function isSafeReturnPath(value) {
if (!value.startsWith('/')) return false;
if (value.startsWith('//')) return false;
if (value.includes('\\\\')) return false;
const base = 'https://example.invalid';
const parsed = new URL(value, base);
return parsed.origin === base;
}Gerçek bir uygulamada ayrıca kod çözme davranışı, bozuk girdiler, kontrol karakterleri, normalizasyon, uygulama yönlendirme kuralları ve yönlendirme üretilmeden önce gerçekleşen proxy veya framework dönüşümleri de dikkate alınmalıdır.
Güvenlik değişmezi ayrıştırıcı ayrıntılarından daha basittir: aktarım, hedef uygulama üzerinde izin verilen bir konumu seçebilir; rastgele harici bir hedefi asla seçemez.
Başarısızlık durumları başarılı senaryodan daha önemlidir
Başarılı bir giriş yalnızca temel yolun çalıştığını kanıtlar. Daha yararlı testler güven sınırlarını zorlar.
Şunlar gibi durumların davranışını doğruladım:
- kimliği doğrulanmamış bir kaynağın aktarım üretmeye çalışması;
- CSRF korumasının eksik olması;
- eksik, opak veya beklenmeyen bir origin;
- güvenilmeyen bir hedef;
- güvenli olmayan bir dönüş hedefi;
- ticket'ın yanlış hedefe sunulması;
- yanlış origin'den tüketim denemesi;
- ticket replay'i;
- ticket süresinin dolması;
- artık var olmayan bir kullanıcı;
- ticket'ın beklenen POST gövdesi yerine URL üzerinden verilmesi.
İki değişmezi doğrudan test etmek özellikle yararlıdır: reddedilen bir istek, meşru akış için hâlâ geçerli olan bir ticket'ı yanlışlıkla yok etmemelidir; başarılı bir tüketim ise o ticket'ı hemen kullanılamaz hâle getirmelidir.
Protokol, ikili entegrasyonlar olmadan ölçeklenir
Daha fazla site eklendiğinde aktarım tasarımı temelden değişmez.
Katılan her sitenin aynı küçük sözleşmeye ihtiyacı vardır:
- kararlı bir mantıksal tanımlayıcı;
- güvenilir, herkese açık bir origin;
- ortak hesap kimliğine erişim;
- kimliği doğrulanmış kaynaklar için bir aktarım üretme işlemi;
- bir aktarım tüketme işlemi;
- güvenilir site kayıt defteri;
- host'a bağlı yerel oturum;
- güvenli dönüş hedefi politikası;
- ve anonim Giriş Yap akışları için bilinen bir yayımlayıcı.
Ölçekleme açısından önemli tercih, özel ikili kimlik doğrulama mantığından kaçınmaktır. Zaten kimliği doğrulanmış bir site, başka bir güvenilir siteye aynı tür aktarımı üretebilir. Anonim bir site ise tarayıcıyı bilinen yayımlayıcıya gönderebilir.
Böylece sistem, giderek büyüyen özel durumlar matrisi yerine tek bir protokole dayanır.
Giriş taşınabilirliği, çıkışın yayılmasından ayrıdır
Bağımsız, host'a bağlı oturumlar başka bir yararlı ayrım yaratır: girişi taşınabilir hâle getirmek global çıkışı otomatik olarak tanımlamaz.
Kullanıcı Site A'dan çıkış yaparsa, backend bunu kasıtlı olarak iptal etmediği sürece başka bir sitenin yerel oturumu geçerli kalabilir. Bu, SSO aktarımının hatası değil, bir oturum yönetimi politikasıdır.
Bir sistem yerel çıkış, global çıkış veya açık oturum yönetimi seçeneklerinden birini seçebilir. Bu kararı aktarımdan ayrı tutmak, her iki mekanizmayı da anlamayı kolaylaştırır.
Kullandığım model
Sorunu “etki alanları arasında giriş çerezlerini paylaşmak” diye tanımlamayı bıraktığımda mimari daha basit hâle geldi.
Tek bir global kimlik vardır, her etki alanı kendi tarayıcı oturumuna sahiptir ve kısa ömürlü, tek kullanımlık bir aktarım, kimliği doğrulanmış güvenilir bir origin'in başka bir yerel oturum oluşturulmasına yetki vermesini sağlar.
Anonim bir siteye doğrudan girişte ek bir kural önemlidir: tarayıcı, kullanıcının mevcut oturumunu gerçekten görebilen bir yayımlayıcıya gönderilmelidir. Kullanıcı daha önce o yayımlayıcıda oturum açtıysa gidiş-dönüş neredeyse görünmez olabilir. Yayımlayıcıda geçerli bir oturum yoksa, ilgisiz başka bir etki alanının oturumunu görebiliyormuş gibi davranmak yerine normal kimlik doğrulamaya geri dönmelidir.
Bu model tarayıcının etki alanı sınırlarını korur, herkese açık ziyaretçiler için gereksiz kimlik doğrulama yönlendirmelerinden kaçınır ve yine de hedeflenen deneyimi sağlar: yayımlayıcıda oturum açın, daha sonra başka bir güvenilir siteyi açın, Giriş Yap düğmesine tıklayın ve parolayı yeniden girmeden yeni bir yerel oturumla geri dönün.