JSON Web Token (JWT)
Last updated
Last updated
Bu yazımda, JSON Web Token (JWT) nedir, nasıl kullanılır ve hangi avantajları sunar gibi konulara değineceğim. İyi okumalar diliyorum :)
JWT (JSON Web Token) kullanıcıların kimliklerini doğrulamak ve kaynaklara erişimlerinde yetkilerini kontrol etmek için kullanılır. En önemli özelliği, veri tabanı ile etkileşime geçme ihtiyacı duymadan bu doğrulamayı yapıyor olmasıdır. Bu sebeple, özellikle mikroservis mimarisi kullanan sistemlerde tercih edilmektedir.
Örneğin, ürünler sayfası, ödeme sayfası ve kullanıcı giriş sayfasının hepsinin farklı mikroservisler üzerinde çalıştığı ve veri tabanlarının birbirinden soyutlandığı bir sistemi düşünelim. Bu durumda, ödeme sayfasında kullanıcı kimlik ve yetki kontrolünü nasıl yapabiliriz? İşte burada veritabanı etkileşimine ihtiyaç duymadan kontrolü yapan JWT kullanabiliriz.
JWT, sunucunun atamış olduğu (istemci tarafında da ataması yapılabilir ancak güvensizdir) özel bir imza içerdiğinden, doğruluğu bu imzadan ve payload kısmından kontrol edilebilir. Bu özellikleri sayesinde, mikroservisler arasında güvenli ve verimli bir şekilde kimlik doğrulama ve yetki kontrolü yapılabilir. Bunun ayrıntılı incelemesini ilerleyen kısımlarda yapacağız.
JWT ve sessionlar arasındaki farkı anlayabilmek için öncelikle HTTP'nin stateless yani durum bilgisi tutmayan bir yapıda olduğunu bilmemiz gereklidir. Bunu şu şekilde örneklendirebiliriz:
Bir kullanıcının bir web sayfasına arka arkaya istekler attığını düşünelim. Kullanıcı o sayfaya her istek gönderdiğinde, sunucu aynı sayfayı yeniden getirecektir. Sunucu, kullanıcının önceki isteklerini hatırlamaz ve her isteği bağımsız olarak işler. Örneğin, kullanıcı 2 saniye önce aynı sayfayı istemiş olsa bile, sunucu bu önceki isteği hatırlamaz ve sayfayı tekrar gönderir. Çünkü HTTP stateless bir protokoldür, yani her istek bağımsızdır ve önceki isteklerle ilgili herhangi bir bağlam veya durum bilgisi saklanmaz.
Session kullandığımızda, HTTP'nin stateless yapısını stateful hale getiririz, yani artık durum bilgisi tutar hale geliriz. Bu, sunucunun her kullanıcı için oturum verilerini saklamasını gerektirir. Ayrıca, session bilgileri sunucuya gittiğinde bu bilgilerle kullanıcının kim olduğu bilinmez; bunun için veri tabanı sorgusuyla doğrulanması gerekir.
JWT ise HTTP'nin stateless yapısına uyar ve durum bilgisi tutmaz. Kullanıcının kimlik doğrulama bilgilerini taşıyan bir token oluşturur ve bu token istemci tarafından saklanır. Her istekle birlikte bu token sunucuya gönderilir ve sunucu, token içerisindeki imza ile kullanıcıyı, verinin bütünlüğünü doğrular ve payload içerisindeki verileri doğru kabul eder. Doğrudan veri tabanıyla etkileşime geçmez. Bu şekilde, kullanıcı oturum bilgileri istemci tarafında taşınır ve sunucu tarafında durum bilgisi tutulmasına gerek kalmaz.
Özet olarak, session ile kullanıcı bilgilerini bir veri tabanında veya sunucuda saklanırken JWT'de böyle bir durum yoktur. JWT, istemci tarafında çerezlerde (örneğin, güvenli olması için HttpOnly flag'i ile), localStorage'da, sessionStorage'da veya bellekte tutulur ve veri tabanıyla bir etkileşime girmez. Bunun ayrıntılı incelemesini daha sonra yapacağız.
JWT üç parçadan oluşur. İlk kısmına header, ikinci kısmına payload ve üçüncü kısma signature denmektedir.
Yukarıda örnek bir JWT verilmiştir. Noktadan sonraki her kısım yeni parçadır. Şimdi bunları teker teker inceleyelim.
Header kısmında tokenı imzalayan algoritmanın adı ve tokenın hangi tipte tutulduğu bilgileri yer alır ve bu kısım base64 ile encode edilmiştir. İmzalama algoritması HS256, HMAC SHA256 ya da RSA olabilir. Token tipi ise JWT’dir.
Base64 ile encode edilmeden önceki hali bu şekilde gözükmektedir.
Burası JWT’nin iletmek için oluşturduğu verileri içerir bunlara da claimler denir. Database tablolarındaki alanlar gibi de düşünebiliriz. Yani veri tabanının json halidir. Örneğin admin yetkisine sahip bir kullanıcı belirli bir sayfaya erişmek istesin. Bu durumda JWT’nin bu parçasında rol bilgisi yer almak zorundadır. Onun dışında da bir çok bilgi yer alabilir: kullanıcı adı, e-postası gibi. Ancak parola gibi kritik bilgilerin yer almaması gerekir. Hatta parolanın veri tabanı dışında herhangi bir yerde bulunmaması gerekir :). Bu kısım da base64 ile şifrelendiği için decode edilmesinin çok kolay olduğunun farkında olmalıyız.
Claimler üç çeşittir; registered, public ve private.
Payload’da bulunması zorunlu olmayan ama bulunması da tavsiye edilen verilere registered claims denir. En çok kullanılanı “exp”dir çünkü güvenlik sebebiyle bu tokenların uzun süre geçerli olmasının önüne geçmemiz gerekir. Registered claimler şunlardır;
iss: Tokenı oluşturan kişinin bilgisini içerir.
sub: Token kim için oluşturulduysa onun bilgisini içerir.
aud: Tokenın hangi sistem tarafından kullanılıp kabul edileceğinin bilgisini içerir.
exp: Tokenın ne kadar süre geçerli olacağının bilgisini içerir.
nbf: Tokenın geçerli olmaya başlayacağı zamanı içerir.
iat: Tokenın oluşturuma zamanını içerir.
jti: Tokenın ID'sidir.
Bunların hiçbirisinin kullanımı zorunlu değildir. İstenirse kullanılabilir.
Private claimler, tokenı oluşturan ve tokenı kullanacak olan kişi/kurumun aralarında özel olarak oluşturdukları claimlerdir. Bu claimler, registered ve public claimlerin isimleriyle çakışmamalıdır.
Private oldukları için dışarıdan gören birisi ne işe yaradığını anlamlandıramayabilir veya başka bir kurum bu isimdeki claimi çok daha farklı bir amaç için kullanabilir. Başka sistemler ile paylaşılamazlar çünkü diğer sistemler bu claimleri tanımaz.
Bütün olarak bakarsak JWT payload kısmı şu şekilde olabilir:
Burası JWT'yi oluşturan son kısımdır ve JWT'nin doğrulanabilmesi için en önemli yerdir. İmzayı oluşturmak için headerda belirtilen algoritma, gizli bir anahtar kelime ve payloaddaki bilgiler gerekir. Bunların hepsinin birleşimi ile JWT'nin imzası oluşturulur. Bu imza hem kimin gönderdiğinin bilgisini içerir hem de iletim sırasında bozulup bozulmadığının doğrulanabilmesini sağlar. Yani verinin bütünlüğünün kontrolünü sağlar. Eğer buradaki imza bozulmuşsa istenen yere erişime izin vermez.
JWT tokenlerarının içeriğinden bahsettik. Şimdi nasıl kullanılır, nerelerde tutulur bunları inceleyelim.
Kullanıcının hesabına giriş yapması ile beraber sunucu, kullanıcı için bir token oluşturur. Bu token genellikle localStorage, sessionStorage veya bir cookie içerisinde saklanır. Ancak bu XSS ve CSRF saldırılarına yol açabilmektedir. Bu yüzden JWT'leri iki kısımdan oluşacak şekilde kaydederiz: refresh token ve access token.
XSS nedir?
XSS, saldırganların kötü amaçlı JavaScript kodlarını, kurbanlarının web uygulamasına enjekte etmesini sağlar. Çoğunlukla cookie'lere erişmek için kullanılır.
CSRF nedir?
Access Token: Bu token bellekte saklanır ve kullanıcının önemli bilgilerini içeren kısımdır. Genelde 1 saat kadar kısa bir süreliğine oluşturulur. API ile diğer hizmetlerle iletişim kurarken kullanılır.
Refresh Token: Refresh token cookie içerisinde HTTPONLY falg ile kaydedilir. Duruma göre SameSite flag de eklenebilir. Access tokenın yenilenebilmesini sağlayan ve access tokenlara göre daha uzun süre geçerliliği olan tokenlardır. Bu tokenlar sayesinde kullanıcı her access token bittiğinde yeniden hesabına giriş yapmak zorunda kalmaz.
HTTPONLY: JavaScript'in cookieye erişmesini engeller ve XSS saldırısı ile çalınmasının önüne geçmiş olur.
SameSite: Cookienin hangi isteklerde iletileceğini kontrol edebilmemizi sağlar. Strict olarak işaretliyse cookie, sadece aynı kaynaktan gelen isteklere eklenir başka kaynaktan yapılan hiçbir isteğe eklenmez.
Kullanıcılar sunucuya istek atacağı zaman access token gönderilir. Access token,
Authorization: Bearer <token>
şeklinde sunucuya gönderilir. Doğrulanırsa da sunucu istek doğrultusunda bir cevap oluşturur.
Aşağıdaki görsel ile access ve refresh tokenların sistemde nasıl çalıştığını görebiliriz.
Görüldüğü üzere access token ile biz sistem kaynaklarına ulaşıyoruz, refresh token ile ise yeni access token alıyoruz. Kullanım süreleri dolduğunda ise ya bize erişim izni vermiyor ya da tekrar kullanıcı girişi yaptırıyor.
Şimdi aklınıza şu sorular gelmiş olabilir "refresh token ile yeni access token isteyebiliyorsam ayrı ayrı yapmaya neden gerek var? saldırganın eline refresh token geçerse yeni access token da alamaz mı?" Gelin bu soruları beraber inceleyelim.
Sadece Access Token kullanımının tercih edilmemesinin nedeni, access tokenların kritik kaynaklara erişim izni sağlaması ve bu nedenle güvenlik açısından potansiyel risk oluşturmasıdır. Bu yüzden access tokenlar saldırganlar tarafından ele geçmesi durumunda fazla zarar vermesini engellemek için kısa süreli olarak tanımlanır.
Access tokenların süresi dolduğunda kullanıcı tekrar giriş yapmak zorunda kalmasın diye refresh token ile sunucudan yeni access token talep edilir ve hem kullanıcı deneyimi iyileştirilmiş olur hem de saldırganın access token ile sistemde gezinmesi kısıtlanır.
Eğer direkt access token olsaydı o zaman bu tokenın süresi her dolduğunda kullanıcının, kaynağa erişmek için tekrar giriş yapması gerekirdi bu da kullanıcının sistemi kullanmasını negatif etkilerdi. Diğer bir seçenek ise access tokenlara uzun süre tanımlamak ama bu da ele geçmesi durumunda ciddi güvenlik riskleri doğurur. Ayrıca refresh tokenlar access tokenların süresi dolmadan kullanılmadığı için ele geçirilmesi daha zor olur ve güvenlik sağlanır.
Bir diğer sebep ise, access tokenlar ve refresh tokenların farklı yerlerde tutulmasıdır. Bu sayede saldırılardan korunur ve ele geçirilme riski azalır.
Bu durumda ise saldırgan refresh token geçerli olduğu sürece yeni access token oluşturabilecektir. Bu da ciddi bir güvenlik sorunu oluşturur çünkü refresh tokenların geçerlilik süresi access tokenlara göre daha uzundur.
Kullanıcı giriş yaptığında refresh token ve access token oluşturulurken, aynı zamanda sunucu tarafından authorization code (yetkilendirme kodu) oluşturulursa bu çalınma sonucunda yeni access token alınmasının önüne geçilebilir. OAuth spesifikasyonuna göre bu şekilde yapılması önerilir. Bu authorization code kullanıcının hangi URI/URL üzerinden istek yaptığının bilgisini tutar. Bu şekilde yeni access token talebinde bu kod kontrol edilir ve sunucudaki ile aynı değilse yeni token oluşturulmaz, invalid_grant hatası verir.
Public claimler e-posta, kullanıcı adı gibi, kullanıcı hakkında veya sistem hakkında daha spesifik bilgiler içeren kısımdır. Yani uygulamanın kendine özgü bilgileri diyebiliriz. Geliştiriciler bunları ihtiyaçlarına göre özelleştirebilir. Tüm public claimlere ulaşabilirsiniz. Eğer özel bir public claim oluşturacaksak bu listedeki isimlerle çakışmamasına veya namespace(_) ile bu kelimeleri birbirinden ayırmaya dikkat etmeliyiz.
Saldırgan kullanıcının cookiesini ele geçirerek kendi oluşturduğu zararlı web sayfaya istek atılmasını sağlayabilir. CSRF hakkında daha ayrıntılı bilgiler için önceden yazdığımız yazıya ulaşabilirsiniz.
JWT'nin nasıl oluşturulduğundan nasıl kullanıldığına kadar bir çok konuyu ele aldık. Umarım JSON Web Token'ların kullanım amacını daha iyi anlamanızı sağlamışımdır. Ek olarak base64 ile oluşturduğunuz JWT'lerin decode edince nasıl göründüğünü incelemek isterseniz linke göz atabilirsiniz.