Javascript ile Manuel JWT Token Oluşturma ve Güvenlik

Selamlar,

Bu yazıda, bir önceki makalede anlatılan Login Olunmayan Mobile Bir Projede, Token İle Güvenlik konusu,  web ortamına taşınarak Javascript ile manuel JWT token üretilmiş ve webapi servisinden güvenli bir request’in nasıl yapılabileceğine değinilmiştir. Client Side ve Server Side tarafta güvenlik ne kadar yapılabilir, sınırlar nereye kadar zorlanabilir. İşte bu soruların hepsine, bu makalede cevap aranmıştır.

Bu projede Angular 9.1 kullanılmıştır.

Öncelikle, Front-End için Angular projesi aşağıdaki komut ile oluşturulur:

İlk olarak Encoding için yeni bir servisi oluşturalım: Bu projede encoding için crypto-js kütüphanesini kullanılmıştır.

Aşağıdaki komut ile crypto-js projeye eklenir.

src/app/Services/encryptionService.ts:

Jwt Token yukarıda görüldüğü gibi 3 parçadan meydana gelmiştir: jwt.io sitesine girildiğinde, yukarıdaki gibi 3 ana bölüm ile karşılaşılır.

1-)Header:

src/app/Services/encryptionService.ts/CreateToken()-1:

  • Header’da iki tip bilgi tanımlanır. Bunlardan biri, Hashleme(şifreleme) algoritmasıdır. Yukarıda görüldüğü gibi, birçok farklı versiyonu vardır. 2.si ise tipidir. Yani “JWT”.
  • CryptoJS.enc.Utf8.parse(JSON.stringify(header))” : CryptoJS kütüphanesi ile header bilgisi, “etc.Utf8” ile encode edilir. Aşağıda görüldüğü gibi, daha birçok encode tipi mevcuttur.

  • this.encodeSource(stringifiedHeader)” : Bir sonraki aşamada bu method, detaylıca incelenecektir. Amaç, header’ın şifrelenmesidir.

src/app/Services/encryptionService.ts/encodeSource():

  • CryptoJS.enc.Base64.stringify(secretKey)” : Kendisine verilen text’i, Base64 olarak encode edilip şifrelenmektedir.
  • encodedSource.replace(/=+$/, ”)” : Esas önemli kısım, bende hatalar çıktığı için encode edilen header’ın base64Url’e, istenmeyen karakterlerin replace edilerek temizlenmesidir.

2-)Payload :

JWT’de saklanacak olan data, bu bölümde tanımlanır. Her requestde farklı bir data’nın oluşması amaçlanmıştır.

  • String(new Date().getTime() / 1000 + 60 * 30)” : Şimdiki zaman /1000’e bölünerek sn’ye çevrilir. Token’ın Expire süresi olarak, şu anki süreye 30 dakika, saniye cinsinden eklenerek tanımlanır(30*60).
    • Not : Elle müdahale edilmesi durumu göz önüne alınarak, ServerSide tarafta ayrıca bir kontrol yapılacaktır..
  • new Date().getTime() + this.appConfigService.apiDeviceKey” : Her seferinde farklı bir tokenın oluşması amacı ile güncel zaman, data’nın içine konmuştur. “apiDeviceKey“, angular’da “config.json” bir  dosyadan çekilmektedir. Yazının hemen devamında, angular’ın bir alt konusu olarak, bu konuya da değinilecektir.
  • *“”exp”: actual30mTimeInSeconds” : Token’ın Expire süresi bu kısımda belirlenir. Token birkere oluştuktan sonra, expire süresi değiştirilemiyeceğinden dolayı, süre dikkatli verilmelidir. Not: Bu kısımın elle değiştirilebilmesi ihtimaline karşın, backend tarafında ayrıca bir güvenlik adımı yazılmıştır.
  • iat” : “issued at”, yani Jwt’nin oluşturulduğu zamanı işaret etmektedir. Jwt’nin yaşını belirlemek için kullanılır. Burada, expire zamanı ile aynı verilmiştir.
  • CryptoJS.enc.Utf8.parse(JSON.stringify(data))” : Aynı Header’da olduğu gibi payload, “etc.Utf8” ile encode edilir.
  • this.encodeSource(stringifiedData)” : JWT Token’a konacak payload, Base64 formatında encode edilir.

src/app/Services/encryptionService.ts/CreateToken()-2:

Angular Bir Projede config.json’dan Değer Okuma:

assets/config.json: JWT Token için payload kısmınına data koymak için, deviceId property’si config.json’da tanımlanmıştır.

app/services/appConfigService.ts: Aşağıda görüldüğü gibi Angular’da, local bir json dosyasından kayıt okumak için servis yazılması gerekmektedir. “httpClient“‘ın, “get()” methodu ile ilgili json’dan veriler çekilir.

  • loadAppConfig() { return this.http.get(‘/assets/config.json’) .toPromise() .then(data => { this.appConfig = data; }); }“: loadAppConfig() methodu, proje daha ayağa kalkarken app.modules’da çağrılacak olan methodudur. “http.get()” ile ilgili json dosyasından tüm data, tek seferde uygulama daha ayağa kalkarken çekilir.
  • get apiDeviceKey() {return this.appConfig.deviceId}” : Methodu ile çekilen tüm data içinden, “deviceId” property’si geri dönülür.

app.modules: Aşağıda appConfigServices’inin, @NgModule‘ün provider property’sine nasıl eklendiği gösterilmiştir. “config.json” dosyasındaki tüm data, application daha ayağa kalkarken çekilir.

3-)Token & Signature:

Gönderilecek olan JWT Token’ın dynamic yaratılacak secret key ile encode edilip, geri dönüldüğü yerdir.

  • encodedHeader + “.” + encodedData” : İlgili Token, Header ve Payoad ile birleştirilerek oluşturulur.
  • CryptoJS.HmacSHA256(token, this.Guid())” : Oluşturulan Token , makalenin devamında anlatılacak secret “Guid()” kullanılarak “SHA256” ile imzalanır.
  • signedToken = token + “.” + signature” : Oluşturulan yukarıdaki token ve imza birleştirilerek “JWT Token” oluşturulur.

src/app/Services/encryptionService.ts/CreateToken()-3:

“JWT Token = encodedHeader + “.” + encodedData” + “.” + signature

*Generate Secret !: 

src/app/Services/encryptionService.ts/Guid(): Secret bir key’i, backend’de oluşturmak kolaydır. Çünkü, zaten kimsenin erişemeyeceği bir yerde gizli birşey yapmaktan daha kolay ne olabilir :) Ama aynı durum forntend için geçerli değildir. Googlelayıp örneklere bakacak olursanız, genelde secret ortada açık ve herkesin görebileceği bir şekilde tutulmaktadır. Ben bunu javascript’de nasıl gizlerim diye uzun uzun düşündüm. Öncelikle, secret key’in dinamik olmasını ve her request’de değişmesini amaçladım. Bu nedenle, aşağıdaki gibi random Guid yaratan bir method oluşturdum. Şimdi aklınıza gelen ilk soruyu söyliyim :

  • Backend, bu secret key’i nerden bilecek ? Yani, aynı secret key backend’de olmadığı sürece, gelen Token’ın, JWT’de geçerli olup olmadığı belirlenemez. Biz Header’a SecretKey’i koyup, göndereceğiz.
  • Peki o zaman bunun neresi secret olacak ? Header’da açık göndermeyeceğiz. İlgili SecretKey’i, Encrypt edip göndereceğiz.

Aşağıdaki koda biraz dikkat ederseniz, oluşan key direk çağrılıp kullanılmış ve bir değişkene doğrudan atanmamıştır. Amaç secret’ın, direk görülmesini engellemektir. Ama yeterli midir ? Malesef değildir. Makalenin sonunda hepsine detaylıca değineceğiz. Birçok projede, değişkene, config’e, hatta LocalStorage’a koyanları gördüm. En azından biz bunları yapmayacağız. Şimdi gelin satır satır kodları inceleyelim:

  • String([‘xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx’.replace(/[xy]/g, function (c) {” :  Geri dönülecek Guid formatı belirlenir ve tüm x ve y karakterleri regex ile tek tek gezilir.
  • “Math.random() * 16 | 0, v = c == ‘x’ ? r : (r & 0x3 | 0x8)” : Her bir x ve y karakteri için, farklı sayı veya alfabetik karakterler oluşturulur.
    • .map(x => { _this.encrptedSecret = _this.EncryptText(x); return x; }” : Tüm karakterler oluşturulduktan sonra, map() methodu ile oluşan Guid değeri, makalenin devamında anlatılacak olan, EncryptText() methodu yardımı ile şifrelenip header’da gönderilecek olan, encrptedsecret değişkenine atanır. Ve son olarak geri dönülür. Evet burada bir değişkene atama işlemi vardır ama şifreli olarak atılmıştır.

src/app/Services/encryptionService.ts/Guid():

src/app/Services/encryptionService.ts/EncryptText() : Aşağıda görüldüğü gibi amaç, kendisine atanan bir text’i mümkün olduğunca şifrelemektir. Bu projede random oluşturulan secret, server side tarafa header’da encrypted olarak gönderilmektedir. İlgili secret, server side tarafta decrypt edilerek, gönderilen JWT Token’ın doğrulanması amacı ile kullanılır.

app/Model/TokenModel: Encryption servisinden geri dönülen Authentication parametreleri, aşağıda görüldüğü gibi TokenModel olarak geri dönülmektedir.

Şimdi sıra geldi, app.component.html’de “Get User” butonuna tıklayıp, ilgili datanın çekilmesine. Tabi bu sırada Header’a, yukarı tanımlanan Token ve Secret’ın koyulması gerekmektedir. Aksi takdirde, Authentication’a takılınır ve “401 Unauthorized” hatası alınır.

src/app/app.component.html: Bu projede css olarak bootstrap kullanılmıştır.

  • “<button (click)=”getWeatherList()” class=”btn btn-warning” style=”height: 25px;line-height: 0.5;margin-left: 80px”><font color=”green”><b>Get Weather List</b></font></button>” : Aşağıdaki Html’de, “Get Weather List” button’una tıklandığında, “getWeatherList()methodu çağrılarak, server side taraftan ilgili hava durumu listesi ekrana basılır.
  • <table class=”table table-striped” *ngIf=”weatherModel.length>0″>” : Sayfa ilk yüklendiğinde, “weatherModel” boş olacağı için, bu table’in ekrana basılmaması için konulmuştur.
  • <td>{{item.Date}}</td> <td>{{item.TemperatureC}}</td> <td>{{item.TemperatureF}}</td> <td>{{item.Summary}}</td>” : Hava durumundan dönen liste, tek tek gezilerek, ilgili alanlar ekrana basılır.

Geri kalan kısımlar, bir angular projesi yaratıldığında örnek amaçlı otomatik oluşan bölümlerdir. Bazıları çıkarılmış, bazıları korunmuştur. Yukarıda Css olarak bootstrap kullanılmıştır. Index sayfasına, ilgili bootstrap css url’i tanımlanmıştır.

src/app/app.component.ts: Aşağıda görüldüğü gibi AppComponent sayfası ilk yüklenirken Constructor’ında, EncryptionService ve AppHttpService, dependency injection ile alınmıştır.

  • EncryptionService : Backend Servis’den request çekerken, Authentication’ı geçebilmek için dynamic bir secret key ve bu keyden bir JWT Token oluşturularak “HttpHeaders“‘a, konmaları gerekmektedir. İşte tüm authentication işlemleri için kullanılan servis “EncryptionService”‘dir.
  • AppHttpService : .Net Core WebApi Servisinden ilgili parametreleri Header’a koyup, “WeatherForecast” listesini çeken servis budur. İlgili model servisden dönünce global tanımlanan [ ] diziye==> “this.weatherModel = res“‘e atanmıştır.

ServerSide .NET Core:

Şimdi sıra geldi, ServerSide tarafta request çekilen “WeatherForecast” servisin, bu JWT Authentication doğrulamasını nasıl yaptığına.

Öncelikle aşağıdaki komut ile .Net Core projesi oluşturulur.

Bu projede, default oluşturulan WeatherForcast servisi kullanılmıştır. Sadece Get() methodu’na güvenlik adımları eklenmiştir. Bu method içine, bir de Helper sınıfı kullanılmıştır. Son olarak Startup.cs ilgili konfigürasyonlar eklenmiştir.

Controllers/WeatherForecastController/Get():

  • if (this.HttpContext.Request.Host.Host != “localhost”)” : Burada güvenlik amaçlı, request çeken Host’un adresine bakılmaktadır. Kısmen de olsa, postman gibi farklı platformlardan request çekilmesi engellenmiştir. Ama tabi ki art niyetli bir kişi, virtual bir host yaratıp, server side tarafta beklenen hostmuş gibi davranıp, request çekebilir.
  • string authHeader = this.HttpContext.Request.Headers[“Authorization”]” : Header ile gönderilen JwtToken alınır.
  • if (authHeader != null && authHeader.StartsWith(“Bearer”))“: Header’a, Bearer var mı diye bakılır. Token yok ise, 401 hatası dönülür.
  • var token = authHeader.Substring(“Bearer “.Length).TrimStart()” . Gelen token değeri, “Bearer” kelimesinden temizlenir.
  • var secret = this.HttpContext.Request.Headers[“EncryptedSecret”].FirstOrDefault()” : JwtToken’ın oluşturulduğu secret, Encrypted olarak Header’dan alınır.
  • secret = Helper.DecryptFromClientData(secret)” : Makalenin devamında anlatılacak olan, Decrypt methodu ile ilgili SecretKey açılır.
  • if (Helper.ValidateJwtToken(token, secret))” : Yine makalenin devamında anlatılacak olan JWTValidate() methodu, decrypt edilen secret key ile gönderilen JwtToken’ın geçerli olup olmadığını kontrol eder. Doğrulama başarısız olur ise, 401 hatası dönülür.

  • return Enumerable.Range(1, 5).Select(index => new WeatherForecast” : Örnek amaçlı default gelen servis ile tanımlı 10 hava koşulundan 5 tanesi random olarak seçilir ve WeatherForecast modelinin Date, TemperatureC ve Summary alanları doldurulup, bir liste olarak geri dönülür.

Decrypt:

Controller/Helper/DecryptFromClientData(): Client Side taraftan gönderilen Encrypt Secret, Server Side tarafta kullanılmak üzere aşağıdaki method ile Decrypt edilir.

Validation:

Öncelikle, JWT kütüphanesi aşağıdaki komut ile indirilir.

Controller/Helper/ValidateJwtToken(): ClientSide’dan gönderilen token’ın geçerliliği, yine clientSide’dan header ile gönderilen SecretKey ile JWT kütüphanesi kullanılarak geçerliliği kontrol edilir.

  • var tokenHandler = new JwtSecurityTokenHandler()” : Esas kontrolü yapacak olan JWT sınıfıdır.
  • tokenHandler.ValidateToken(webToken, new TokenValidationParameters” : Geçerliliği kontrol edilecek Token, diğer parametreler ile birlikte methoda verilir.
    • ValidateIssuerSigningKey = true” : Bir secretKey ile kontrol edileceği, burada tanımlanır.
    • ValidateIssuer = false, ValidateAudience = false” : Bu örnekte, user ve audience’a bakılmayacaktır.
    • IssuerSigningKey = new SymmetricSecurityKey(key)” : Gönderilen secretKey, Decrypt edilip Byte[ ] array’e çevrildikten sonra, ilgili methoda parametre olarak verilmiştir.

  • * “DateTime localDate = validatedToken.ValidTo.ToLocalTime()”: Validate işleminden geçen token’ın geçerlilik zamanı, bulunulan lokasyon’a(ToLocalTime()) göre alınır. Yukarıdaki resimde görüldüğü gibi, ToLocalTime() methodu ile ==> ValidTo() methodu ile dönen saatin üstün, bulunulan konuma göre +3 saat eklemektedir!
  • TimeSpan span = DateTime.Now – localDate; if (span.TotalMinutes > 30)” : Token’ın expire zamanı ile şu anki zaman arasında 30 dakikadan daha büyük bir fark var ise, client side taraftan elle bir müdahale durumu var demektir. Bu durumda geriye kayıt dönülmez!

Gördüğünüz gibi hem ClientSide tarafta, hem de ServerSide tarafta bir çok önlem aldık. Ama yetenekli bir yazılımcı için bu adımları geçmek, çocuk oyuncağıdır. Bu makalede esas amaç, standart bir kullanıcının rahatça servisden bir kaydı çekmeseni önlemektir. Öncelikle Client-Side tarafta bir JWT Token’ı üretmek için gerekli olan secret’ı saklamak, bir developer için nerde ise imkansızdır. Belki ilgili secret’ı bir değişkene atmamak, config.json gibi bir yere yazmamak ve header’da gönderilirken Encrypt etmek ilk başta güvenliği sağlasa da, kodların herkesin erişebileceği bir ortamda olması, bütün bu önlemlere rağmen gerçek anlamda bir güvenliği sağlanamadığı anlamına gelmektedir. Encryption methodu çözülebilir. Değişkenlere secret doğrudan atanmasa da, ilgili script’in browser tarafında debug edilmesi ile özellikle ==>map() methodunda, açık hali kolaylıkla alınabilir. Host url’i için server side tarafta alınan güvenlik, sanal bir host ile kolayca atlanabilir. Jwt Token süresi elle değiştirilse de, o kısım server side taraftan yakalanabilir. Zaten Client Side taraftan büyük ölçüde bir güvenlik sağlanabilse idi, Authentcation ile Login olmaya, UserName ve Password ile doğrulama yapmaya ne gerek olurdu :) Burada esas amaç, standart kullanıcıları elemektir. Didos atağa yapmak isteyen kişilere zorluk çıkartmak , Postman gibi her yerden request çekilmesini engellemek ve en azından bir secret ve expire time ile JWT kontrolü yapmaktır. İnanın bu gibi önlemler bile, her 100 kişiden 80’inini elemenize ve servislerinize doğrudan erişmesine engel olacaktır.

Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzer hepinize hoşçakalın. Sağlıklı kalın.

Github Source Code: https://github.com/borakasmer/CreateClientSideManuelJwtToken

Source : 

 

 

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

2 Cevaplar

  1. seb dedi ki:

    Hocam elinize sağlık. Makaleyi okurken sorduğum bütün sorulara son paragrafta cevap veriyorsunuz, müthiş…

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir