Dotnet İstanbul Authentication & Authorization ve Permission Semineri
Selamlar,
28 Ocak 2023 Cumartesi günü, Authentication & Authorization 1. sunum, Permission da 2. bir sunum olacak şekilde, “dotnetistanbul” etkinliği adı altında toplam 2 session gerçekleştirdik.
Platformlar Frontend: Angular, Backend: .Net 6.0, DB: MsSql server ve Mobile: Genel olarak ele alınmıştır.
1. Session Authentication ve Authorization:
1. Aşamada Authentication ve Authorization nedir ? Authorization ile permission arasındaki farklar nelerdir konuşulmuştur.
2. aşamada UI tarafından nasıl UserName, Password’ün girildiği, Password’ün daha FrontEnd tarafında nasıl şifrelenebileceği ve backend cevabından sonra encrypted Token & RefreshToken’ın, Client’ın zaman dilimi ile belli bir expire süresi ile nasıl Cookie’de saklanabileceği anlatılmıştır.
3. Aşamada backend tarafında ilgili Username ve Password’e göre 1 saatlik Token ve RefreshToken üretildi. İlgili Tokenlar Redis’e, belli bir isim patternleri ile koyuldu.
RedisCacheService: Redis Keyin, global bir yerden alınması sağlandı. Böylece mükerrer kaydın önüne geçildi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class CacheKeys { /* Redisteki isimlerin oluşturulma şekilleri. Yani burası şema. Redise birşey atarken yada okurken bu keylerden faydalanacağız. Buraya genelde parametreli olanlar ekleniyor, parametresizleri eklemiyoruz çünkü çok bir anlamı olmuyor. */ public const string UserDetail = "User:{0}"; public const string UserDetailByDb = "{0}:User:{1}"; public const string GetUserCompaniesById = "UserCompanies:{0}"; public const string GetAllUsers = "GetAllUsers"; public const string GetAllControllers = "GetAllControllers"; public const string GetAllActions = "GetAllActions"; public const string GetAllActionsByController = "GetAllActionsByController:{0}"; public const string GetAllRoles = "GetAllRoles"; public const string GetUserPermissionByUserId = "GetUserPermissionByUserId:{0}"; public const string GetRolePermissionByRoleId = "GetRolePermissionByRoleId:{0}"; public const string Menu = "Menu:{0}"; public const string MenuRemovePattern = "Menu:*"; //Menu Insert'de User'a ait Menu Cache'i silinir.. public static object lockActionObject = new Object(); public static object lockControllerObject = new Object(); } |
Encryption.cs: Token ve RefreshToken’ın encypted ve açık olarak yaratılması sağlandı. Encrypted client’a, clear text olanı Redis’e kaydedildi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public string GenerateSalt() { byte[] randomBytes = new byte[128 / 8]; using (var generator = RandomNumberGenerator.Create()) { generator.GetBytes(randomBytes); return Convert.ToBase64String(randomBytes); } } public (string encToken, string decToken) GenerateToken(string emailUser) { var token = new StringBuilder(); var guid = Guid.NewGuid(); var time = DateTime.Now; var email = emailUser; token.Append(email); token.Append("ß"); token.Append(guid); token.Append("ß"); token.Append(time); var encryptToken = EncryptText(token.ToString()); return (encryptToken, token.ToString()); } public string HashCreate(string value, string salt) { var valueBytes = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2( value, Encoding.UTF8.GetBytes(salt), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512, 10000, 256 / 8); return System.Convert.ToBase64String(valueBytes) + "æ" + salt; } public bool ValidateHash(string value, string salt, string hash) => HashCreate(value, salt).Split('æ')[0] == hash; } |
4. Aşamada Backend tarafında Middleware ile gelen istekler önce Token kontrolü, Token yenilenme süre ve multithread isteklerde karşımıza çıkabilecek sorunlar tartışıldı.
DoubleCheck Lock: Bu yöntem ile 3 multithread istekde, sadece 1 kere Token’in değişimi sağlandı.
Save OldRedis Cache: Bu yöntemle de süresi biten Tokenlar, 1 dakka boyunca “Old” ismi ile Redisde tekrardan saklandı. Böylece yukarıda görüldüğü gibi “C” ile değişen Tokenların, “A” ve “B” için hataya düşüp UnAuthorized vermesi engellendi.
LoginFilter.cs(1): Aşağıdaki kod parçası, Redis’den Token kontrolü ve yok ise OldToken kontrolünün yapıldığı, middleware katmanıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
if (string.IsNullOrEmpty(cacheRedistoken) && isMobile) // Redis'de Token Key yok ise , bu durum SADECE MOBILE'DE BAKILMALIDIR. { CreateTokensByCheckRefreshToken(context, true); //true'nun amacı context.Result = new UnauthorizedResult() dönüşünün yapılmasının istenmesidir. } //Redis'de Token Yok Ya da Redis'de Token Var ama tokenlar eşit değil , geçerli bir oturum isteği değil. else if ((string.IsNullOrEmpty(cacheRedistoken)) || (!string.IsNullOrEmpty(cacheRedistoken) && cacheRedistoken.Trim() != decryptToken.Trim())) { //Check Token-Old------------------------------------------------- //İlgili UserID'ye ait Token-Old Redis'den alınır. cacheOldRedistoken = _redisCacheService.Get<string>(_redisCacheService.GetTokenKey(userId, isMobile, false, unqDeviceId) + "-Old"); //Token-Old ile aynı mı diye bakılır. if ((string.IsNullOrEmpty(cacheOldRedistoken)) || (!string.IsNullOrEmpty(cacheOldRedistoken) && cacheOldRedistoken.Trim() != decryptToken.Trim())) { context.Result = new UnauthorizedResult(); return; } } |
5. Aşamada neden JWT yerine custom Token kullanıldığı, alttaki maddeler detaylandırılarak açıklandı.
- İstendiği zaman Expire edilememsi.
- Belli gurupların Web veya Mobile gibi logout edilememesi.
- Her user’a ait Token’ın, client’a özel UserID ile kontrolünün sağlanamaması.
- Mobile’de Force Update’in yapılamaması.
- Mobilede UniqueDeviceID’ye göre Login olma işinin sınırlandırılamaması.
6. Aşamada Token yenilemede neden RefreshToken gerekiyor ? Mobile ve Web ortamında işler nasıl değişiyor sorularına hep beraber çözüm arandı.
Güvenlik herşeyden önce gelir. Token, her request’de ama RefreshToken sadece Expire süresine yaklaşıldığında 1 kere gönderilir. Doğal olarak Token’ı alan ya da çalan, en fazla Expire süresi kadar mesela 1 saat sisteme giriş yapıp kullanabilir. Çünkü Refresh token olmadan tokenlar yenilenememektedir. Doğal olarak expire’a düşen token, artık geçersiz olacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if (context.HttpContext.Request.Headers["RefreshToken"].FirstOrDefault() != null) // client refresh token göndermiş. { bool.TryParse(context.HttpContext.Request.Headers["IsMobile"].FirstOrDefault(), out var isMobile); int.TryParse(context.HttpContext.Request.Headers["UserId"].FirstOrDefault(), out var userId); var unqDeviceId = context.HttpContext.Request.Headers["UnqDeviceId"].FirstOrDefault(); if (userId == 0) { context.Result = new UnauthorizedResult(); return; } var clientRefreshToken = context.HttpContext.Request.Headers["RefreshToken"].FirstOrDefault(); var redisRefreshToken = _redisCacheService.Get<string>(_redisCacheService.GetTokenKey(userId, isMobile, true, unqDeviceId)); if (string.IsNullOrEmpty(redisRefreshToken))//rediste refresh token yok { context.Result = new UnauthorizedResult(); return; } } |
7. Aşamada Web ile Mobile platformu arasındaki Token kurallarının, bu casedeki bussines farkı anlatılmıştır.
- Mobile’de Token Key içinde ayrıca UniqueDeviceID bulunmaktadır. Örnek Redis Key:(1:12345678-Mobile-Token) Amaç, aynı user account ile, farklı mobile cihazlardan giriş sayısını sınarlamaktır.
- Mobilede Token olmasa dahi, RefreshToken ile girişe izin verilmektedir. Amaç, kullanıcı 6 ay girmese bile Login olmadan Token ve RefreshToken’ın yenilenip sisteme dahil olmasını sağlamaktır.
- Mobile’de RefreshToken süresi web’e nazaran 1 yıldır.
1 2 3 4 5 |
// Redis'de Token Key yok ise , bu durum SADECE MOBILE'DE BAKILMALIDIR. if (string.IsNullOrEmpty(cacheRedistoken) && isMobile) { CreateTokensByCheckRefreshToken(context, true); } |
2. Session Permission:
Öncelikle yetkilendirme algoritmalarından Bitwise konusu anlatılmıştır. En fazla 63 tane olabilen “n^2” şeklinde ilerleyen sayı kümesidir. Seçilen seçeneklerin, Bitwise karşılığı değerlerinin toplamlarını saklamak yeterlidir.
Örneğin “4==(5&4)” 4 seçeneği, seçilen 2 şıkkın bitwise toplamı olan(1+4) = 5’in, içinde var mı şeklinde kontrol edilir.
Daha sonra Controller ve Action bazında nasıl bir User’a yetki verildiği konusuna, detaylıca girilmiştir. Aşağıda görüldüğü gibi projedeki tüm Controller ve Actionlar, DB’de tanımlanmıştır.
İzin gerektiren Actionlar’a yetki vermek için Custom Attributelar tanımlanmış ve parametre olarak, koda implemente edilen Action ve Controller Enumlar atanmıştır.
UserController.cs/GetUserbyId(): Aşağıda görüldüğü gibi Method üstüne, yetki gerektiren Controller ve Action “Bitwise” değerleri, Enum parameter ile tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 |
[LogAttribute] [Infrastructure.SecurityActionAttribute((int)SecurityControllers.User, (Int64)UserActions.GetUserById)] [HttpGet("GetUserById/{userId}")] public ServiceResponse<UserModel> GetUserById(int userId) { var response = new ServiceResponse<UserModel>(HttpContext); response.Entity = _userService.GetById(userId).Entity; response.IsSuccessful = true; return response; } |
Enum.cs: Tum Controller ve controllerlara ait Actionların Bitwise karşılıkları, Enum olarak aşağıda tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Enums { // Dikkat: buraya yazdığınız her yeni değeri veritabanına tanımlamayı unutmayınız. // Sadece yetki gerektiren controller isimlerini yazıyoruz public enum SecurityControllers { User = 2, Report = 1, UserPermission = 7 } //Controller bazında sadece yetki gerektiren action isimlerini yazıyoruz public enum UserActions { GetCustomer = 1, GetUserById = 2, GetCustomerList = 4, InsertUser = 8, LoginAsBeHalfof=16, GetAllUsers = 32 } |
Bir Kullancının Controller bazında Yetki Ataması:
Örneğin aşağıda UserID=1 için UserController “2” yetkisi tüm Actionlar için yeterli olduğu, 63 değerinin tüm Action değerlerinin [1+2+4+8+16+32] toplamına eşit olması ile anlaşılır.
Yine yetki kontrolü, MiddleWare’de “LoginFilter“‘da yapılmaktadır. User’ın, o controller için yetkisi Bitwise olarak alınıp ilgili Action’a yetkisi olup olmadığı kontrol edilir.
Not: Performans amaçlı User Login olurken, yetkileri Redise kaydedilebilir. Böylece herbir işlemde DB’ye gidilmesine gerek kalmaz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var userAction = _userActionRepository.Table .Include(r => r.IdSecurityControllerNavigation) .FirstOrDefault(ur => ur.IdUser == userId && ur.IdSecurityController == idSecurityController); if (userAction != null) { if (actionNumber == (userAction.ActionNumberTotal & actionNumber)) { var securityAction = _actionRepository.Table.Where(r => r.ActionNumber == actionNumber).FirstOrDefault(); if (securityAction != null) { model = new SecurityActionCustomModel() { IdSecurityAction = securityAction.IdSecurityAction, ActionName = securityAction.ActionName, IdSecurityController = (int)userAction.IdSecurityController, ActionNumber = actionNumber, IdUser = userId, ControllerName = userAction.IdSecurityControllerNavigation.ControllerName }; } } response.Entity = model; } |
Ayrıca yetkisi olmayan clientların, ElasticSearch’e yetkisiz giriş denemeleri, loglanabilirler.
MenuService.cs / GetMenuListByUserId(int userId)(): Bir kullanıcının yetkili olduğu menu, aşağıdaki gibi Linq sorgusu ile oluşturulur. Burada önemli olan konu “ActionNumberTotal” ile o form içindeki tüm yetkileri, Bitwise olarak tekbir sayı olarak gönderilmesidir. Böylece UI’da herhangi bir button veya alana client yetkisi, bu ActionNumberTotal üzerinden kontrol edilecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var result = from m in _context.DbMenu from ua in _context.DbSecurityUserAction.Where(u => u.IdSecurityController == m.IdSecurityController && u.IdUser == userId && u.Deleted == false).DefaultIfEmpty() where m.IsActive == true && (ua.Deleted == false || ua.Deleted == null) && ((m.ActionNumber == null || m.ActionNumber == (m.ActionNumber & ua.ActionNumberTotal)) || isAdmin == 1) orderby m.OrderNumber select new CustomMenuModel { IdMenu = m.IdMenu, OrderNumber = m.OrderNumber, MenuName = m.MenuName, IdSecurityController = m.IdSecurityController, ActionNumber = m.ActionNumber, RoutingPath = m.RoutingPath, ImageClass = m.ImageClass, IdMenuParent = m.IdMenuParent, IsParent = m.IsParent, HasChild = m.HasChild, ActionNumberTotal = ua.ActionNumberTotal, IdUser = ua.IdUser }; |
Örnek Angular UI’da Menu, Aşağıda görüldüğü gibidir. Örneğin burada UserPermission(7) Controller’ı için Toplam yetki sayısı => 63’dür. Kaydet buttonun yetkisi Angular’da aşağıdaki gibi tanımlanmıştır.
Angular/UI:
- “[disabled]=”!myForm.form.valid || !hasPermision(_UserEnum.InsertUser)“: Kaydet Button’unun Disabled olup olmayacağı “InsertUser” Action BitwiseID=8 değerine göre kontrol edilir. Bu değer Backend ve DB’deki Action yetki değeri ile aynıdır.
- “actionRoleID == (this.UserRoleID & actionRoleID) ? true : false” : Bitwise kontrolü ile “8 == (63 & 8)” şeklinde yapılır. 63 sayısı, backend servisinden daha menu enbaşta ilgili client için oluşturulurken gönderilir. Sayfa üzerinde herbir button bir Action’a bağlıdır. Ve yetki gerektiren tüm objelerin DB tarafındaki karşılığı olan BitwiseID değeri UI’da da Enum olarak tanımlanmış ve client bazlı kontrol edilmiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<button type="submit" [disabled]="!myForm.form.valid || !hasPermision(_UserEnum.InsertUser) || selectedTable!=null" class="btn btn-info float-right" [attr.roleID]="_UserEnum.InsertUser" id="Kaydet">Kaydet</button> public hasPermision(actionRoleID): boolean { if (this.isAdmin) { return true; } return actionRoleID == (this.UserRoleID & actionRoleID) ? true : false; } export enum UserActions { GetCustomer = 1, GetUserById = 2, GetCustomerList = 4, InsertUser = 8, LoginAsBeHalfof = 16, GetAllUsers = 32, } |
İlgili Client’ın UserPermission Controlün’deki “InsertUser” yetkisi kaldırılır ise, aşağıda görülen Kaydet buttonu, sayfa yenilenmesinden sonra Pasif hale geldiği görülür. Sayfa yenilemeden ilgili buttonun pasif olması için, Socket kullanılması ve yetkisi değişen Client’ın UI’da notify olması gerekmektedir.
.Net üzerinde konuştuğumuz Middleware mimarisnin, Go üzerindeki bir benzeri, aşağıdaki gibidir.
Geldik bir seminerin daha sonuna. Bu seminerde çok fazla konu hakkında konuştuk. Eğer Authentication ve Authorization işlemlerini, custom bir şekilde kendiniz yönetirseniz, sonsuz bir esnekleye kavuşursunuz. İstediğniz yere kendi custom rulelarınız koyar, farklı interceptorler ile araya girer ve farklı bussinesları birbirine bağlıyabilirsiniz. Ayrıca yetkilendirme konusunda, button altı ya da field’a kadar bir yetki işlemini, kolayca Attribute tanımlayarak ve bitwise ile matematiğin de gücünü kullanarak çok baside indirgiye bilirsiniz.
Seminere katılan tüm katılımcılara burdan tekrardan teşekkür etmek istiyorum. İster soruları ile olsun, isterse paylaştıkları tecrübeleri ile olsun, seminere yön verip renk kattıları için, hepsine çok teşekkür ederim. Beni 3 saat boyunca sıkılmadan dinledikleri ve sabahın erken saatlerinde o soğukta üşenmeden etkinliğe geldikleri için ayrıca da hepinizi teker teker kutluyorum. Demekki ne imiş “İlim Çin’de de olsa gidip alınıyor imiş :)” Hoşçakalın, Selametle kalın..
Seminer Sunumu: https://www.canva.com/design/DAFYQOL7zik/M03bX5_EcRKW-jZdFtvV9g/view#1
Son Yorumlar