Photo by Jason Dent on Unsplash

Teknik Muhabbetler #5 (Jwt-Authentication-Gerçek Senaryolar)

Furkan Güngör
Mobiroller Tech
Published in
5 min readFeb 18, 2021

--

Bir gün development biter buglar kalır. 😂 Belirli işleri gerçekleştirmek için geliştirilen yapılar bazen düşünülmeyen senaryolarla karşı karşıya kalabilir. İşte bunlar her yazılımcının tatlı baş belası olan ve genellikle halk arasında “Uygulama patladı… 😡” olarak bilenen hatalar yani buglardır. 🐛
Teknik Muhabbetler serisinin bu sayısında gerçek hayatta yaşadığımız bir sorun için ürettiğimiz çözümleri ve tercih ettiğimiz yolu sizlerle paylaşmaya çalışacağım.✨

Yazının başlığından anlaşılacağı üzere sorunlu olan yapı JWT. 🔥
Serinin önceki yazılarında Mobiroller içerisinde microservice dönüşümü yapmaya başladığımızı ve bu dönüşümü yaparken hem replatforming hem de uygulamanın ana hücrelerini, gelişen Business kurallarına göre yeniden tasarladığımızdan bahsetmiştim. Microservice dönüşümünü yaparken ilk olarak Authentication ve Authorization konularını adreslemek için bir microservice tasarladık. Bu service sistem üzerindeki tüm kullanıcılar ve uygulamalar için Authentication ve Authorization işlemlerini üstlendi ve microservice geçişimizdeki ilk adımımız oldu. Authentication işlemlerini gerçekleştirmek için bizim tercihimiz JWT olmuştu. Yazının gelişme aşamasına geçmeden önce JWT’yi tekrar hatırlayalım.🤔

JSON Web Token, tarafların birbirleri arasındaki veri alışverişini ve bunun doğrulamasını sağlayan JSON tabanlı RFC 7519'de tanımlanmış açık bir standarttır.

JWT kendi içinde 3 ana kısımdan oluşur;
* Header
* Payload
* Signature
JWT kullanarak ürettiğimiz tokenlar içerisinde isteklere göre farklı parametreler yerleştirebiliriz. Bu parametreler Payload kısmında bulunur ve token ile beraber ilgili yerlere aktarılmış olur.

JWT ile üretilen tokenlar bir süre sonra expire olacaktır. Mobiroller içerisinde yer alan kullanıcıların şimdiye kadar alıştığı davranışları göz önünde bulundurduğumuzda sürekli çıkış giriş yapmalarını istemediğimiz için JWT ile beraber bu microservice içerisine Refresh Token yapısını da kurmuş olduk. Özetlemek gerekirse kullanıcı giriş yaptığında JWT ile beraber bir Refresh Token alır ve JWT expire olduğunda client login aşamasında almış olduğu Refresh Token ile beraber yeni bir token alarak yoluna devam eder.

Geliştirilen bu servis ek olarak Change Password, Forgot Password gibi temel üyelik işlemlerini de gerçekleştirebiliyor.
Yazının temel amacı gerçek bir hatayı incelemek olduğu için içerideki yapıyı olabildiğince açık ve sade bir şekilde aktarmaya çalıştım ancak görsel olarak bir kez daha özetlemekte fayda var. 🧐

İlgili servisin küçük özeti 😀

Şekilde belirtildiği gibi kullanıcı login işlemini gerçekleştirdikten sonra JWT Token ve Refresh Token bilgilerini alır ve bu bilgileri kullanarak mevcut şifresini değiştirebilir. İşte sorun tam bu noktada başlıyor. Şifre değiştikten sonra kullanıcının aldığı token invalid olarak işaretlenmiyor. Developer friendly olarak konuyu özetlemeye çalıştım. Bakalım kullanıcı bu hikayeyi bize nasıl anlatmış: 🧐🤷‍♂️

“Mobiroller üzerinde iOS ve Android platformlarda çalışabilecek bir uygulama tasarladım. Uygulamamı kullanan kullanıcıları aktif olarak uygulamam içinde tutabilmek için gün içerisinde Mobiroller paneli üzerinden Push Notification gönderiyorum. Bu işlemi gün içerisinde gerçekleştirmesi için arkadaşımla şifremi paylaştım. Ancak belirli sebeplerden dolayı artık arkadaşımın uygulamama erişmesini istemiyordum. Şifremi paylaştığım için hemen panel üzerinden şifremi değiştirdim ancak arkadaşım kendi tarayıcısı üzerinden hala sistemi kullanabiliyor. Şifremi değiştirdim ve güncel şifremi bilmiyor. Sistemden çıkış yapmasını sağlayabilir misiniz? ”

Özetlemek gerekirse kullanıcı şifresini değiştirmiş ancak diğer kullanıcıda hem token hem de refresh token var. Token süresi her dolduğunda kendisinde bulunan refresh token ile mevcut token bilgisini yenileyerek sistemi kullanmaya devam ediyor. 😡 Bu sorunu nasıl düşünemedik diye ekip arkadaşlarımla tartışırken aşağıdaki çözüm önerileri ortaya atılmaya başladı:

1) Şifre değiştiğinde sistemde bu kullanıcı için kaç farklı client varsa hepsini logout endpointine yönlendirelim kullanıcılar tekrar giriş yapsın.

Bu çözüm önerisini gerçekleştirmek için clientları tetikleyebilecek bir teklonojiye ihtiyacımız vardı. SignalR ile bu işlemi gerçekleştirebilirdik. SignalR ile real time communication sağlayabilirdik ve channel üzerinde bulunan tüm clientları logout yönlendirmesi yapması için bir message yayınlayabilirdik. Kulağa güzel geliyor ancak servisler arasında Real Time Communication için daha önce bir çözüm geliştirmemiştik. Bu çözümü uygulamak, test etmek ve sonrasında yayınlamak uzun bir süre gerektirecekti. Bu çözüm güzeldi ancak o kadar vakit yoktu. 😒😢

2) Kullanıcı şifre değiştirdiğinde bir event fırlatalım. Bu eventi dinlemesi için tüm servislerde geliştirme yapalım. Event nesnesinin içinde kullanıcının token bilgisi olsun. Eventi dinleyen servisler bu token bilgisini kara listeye alsın ve bu token ile gelen request olursa 401 veya 403 dönsün.

Servisler arasındaki iletişimi zaten Event-Driven bir yapıya geçirmiştik. Bu çözüm önerisi ilk başta mantıklı gelmişti ancak beyaz tahtanın önüne geçip çizmeye başlayınca bu çözüm önerisini uygularken çok fazla development yükü çıkacağını farkettik. Ayrıca kara liste için her serviste yeni bir table veya yeni bir collection açma fikri pek hoşumuza gitmedi. Kara liste için cache kullanabiliriz diye bir fikir ortaya atılmıştı bile. 🤔 Ancak ne olursa olsun çok sayıda servise sadece bu kontrolü yapması için geliştirme yapmak artık hiç mantıklı değildi. Nerede Single Responsibility Principle nerede DRY (Don’t Repeat Yourself) Principle diye sesler yükselmişti bile. 😂

3) Aylar önce geliştirdiğimiz bir feature bizi kurtarabilir. Permission based Authorization kullandığımız için her servis gelen requesti handle ederek kullanıcının bu requesti atmasına yetkisi var mı diye kontrol ediyor zaten. Böylece tüm istekler en az bir kere User servisine uğruyor. Eğer sadece User servisi üzerinde kullanıcının ne zaman şifre değiştirdiğini tutarsak request geldiğinde request üzerinde bulunan token bilgisini parse edip bu token bilgisinin kullanıcıya ne zaman verildiğini bulabiliriz. Sonrasında hemen bir if yazarız. Token verilme tarihi şifre değiştirme tarihinden küçükse 401 döneriz. Client Refresh Token ile token bilgisini yenilemek için tekrar geldiğinde Refresh Token endpointi yine 401 dönerse o zaman zaten client bu kullanıcıyı logout yapması için yönlendirecektir. Eğer şifreyi biliyorsa tekrar giriş yapabilir.

Bu çözüm önerisi bizim için oldukça uygun sadece ilgili servis üzerinde küçük bir middleware yazarak bu problemi çözebilir ve sonrasında support ekibi müşteriye haber verirken kahvemizi içip kahkaha atabilirdik. 😂☕

İlgili servis Asp.Net Core ile geliştirilmiş bir Web Api projesi olduğu için ilk olarak bu teklonoji üzerinde Middleware nedir ve nasıl gerçekleştirilir sorusuna cevap arayalım.

Middleware kavramını interceptor gibi düşünebilirsiniz. Yani ilgili metoda veya sınıfa uğramadan önce araya girip istediğiniz işi yapmanıza olanak sağlayan bir yapı. Middleware ile Request-Response arasında geçen süreçleri handle edebilir payloadlar üzerinde kontrol sağlayabilir veya bu isteği çeşitli business kurallarından geçirerek farklı senaryoları gerçekleştirebilirsiniz.

Örneğin bazı kullanıcılar için servislerinize atılan requestleri kısıtlamak istiyorsanız middleware yardımıyla request başladığında araya girip kullanıcının gün içerisinde kaç request attığını ve mevcut isteği gerçekleştirip gerçekleştiremediğini kontrol edebilirsiniz.

Middleware kavramının daha iyi anlaşılması için oldukça sık kullanılan bir görseli sizlerle paylaşıyorum.

Tek çözüm yolu middleware yazmak değil tabii ki, Action Filter yapısını kullanarak istediğimiz sonucu elde edebiliriz. Action Filter yapısıda benzer bir kurguyla çalışmakta. Action Filter merak uyandırdıysa sizi şöyle alalım. 🧐

Problemi ve çözüm önerilerini belirttiğimize göre artık teknik implementasyona geçebiliriz.

Öncelikle kullanıcıların en son ne zaman şifre değiştirdiğini bulabilmek için ilgili entity üzerinde LastPasswordChange isimli bir property tanımlayalım.

Artık her şifre değişikliğinde bu değeri güncelleyebiliriz.

AuthenticationCheck isimli middleware yardımıyla request üzerinde bulunan JWT parse edilecek ve token verilme tarihi ile şifre değişikliği tarihi karşılaştırılacak. Eğer şifre değişikliği daha büyükse o halde kullanıcının bu requesti atmasına izin verilmeyecek.

İşte o meşhur if 😂

Son olarak Startup.cs içerisinde ilgili middleware yapısını pipeline üzerine eklemeyi unutmayalım.

app.UseMiddleware<AuthenticationCheckMiddleware>();

Teknik anlamda yapılan iş aslında oldukça basit. Bu yazının amacı uçtan uca bir teknik implementasyon sürecini anlatmak değil, Mobiroller içerisinde kullanıcının yaşadığı hataları çözerken geçirdiğimiz süreçleri özetlemektir.
Ekip olarak JWT için bulunan bu handikapı gidermek için aklımıza gelen yöntemleri ve seçtiğimiz yöntemi özetlemeye çalıştım. Eğer aklınıza daha kolay ve kullanışlı bir çözüm gelirse lütfen yorum olarak paylaşın. Değerlendirip içeride bulunan implementasyonu güncelleyebiliriz.

Yazdığınız kodların production ortamında hatasız çalışması dileğiyle.

Testler sizi korusun. 🤲

--

--

Furkan Güngör
Mobiroller Tech

Solution Developer — I want to change the world, give me the source code.