.NET – Lambda Authorizer İle Amazon API Gateway’in Güvenliğini Sağlama
Merhaba,
Bu içeriğimizde, önceden .NET 7 – AWS Lambda İle Serverless Computing ve .NET İle Amazon API Gateway – AWS Lambda & DynamoDB Entegrasyonu başlıkları altında kaleme aldığımız makalelerimizde incelediğimiz AWS Lambda ve Amazon API Gateway konularının güvenliğini sağlayabilmek için Amazon tarafından sunulan Lambda Authorizer özelliğini inceleyecek ve teknik olarak nasıl yapılandırıldığını tecrübe ediyor olacağız.
Lambda Authorizer Nedir?
Oluşturduğumuz AWS Lambda Function’larını kullanabilmek için Amazon API Gateway tarafından üretilmiş olan endpoint’lere karşın erişim haklarını yönetmek için kullanılan esnek, yapılandırılabilir bir yetkilendirme mekanizmasıdır.
Lambda Authorizer iki farklı türde mevcuttur;
- Token-Based Lambda Authorizer
İlgili endpoint’e istek gönderen kişinin bilgilerini Bearer Token olarak JSON Web Token(JWT) formatında almaktadır. Ayrıca TOKEN Authorizer olarakta nitelendirilmektedir. - A Request Parameter-Based Lambda Authorizer
İlgili endpoint’e istek gönderen kişinin bilgilerini request’in header bölümünden veya query string değerlerinden almaktadır. Bu yönteme de ayrıca REQUEST Authorizer dendiğini görebilirsiniz. WebSocket API’leri için bu yöntem kullanıma daha yatkındır.
Lambda Authorizer Workflow’u Nasıldır?
Amazon API Gateway üzerinden AWS Lambda Function’larına yapılan istek süreçlerinde Lambda Authorizer’ın sergilediği davranışı anlayabilmek için aşağıdaki iş akışı diyagramına göz atabilirsiniz.
- 1. Adım
Client, bir bearer token yahut request parameter eşliğinde API Gateway üzerinden bir endpoint’e istek gönderir. - 2. Adım
API Gateway ise endpoint’e karşılık bir Lambda Authorizer’ın yapılandırılıp yapılandırılmadığını kontrol eder. Eğer herhangi bir Lambda Authorizer yapılandırması söz konusu değilse endpoint’e karşılık lambda function tetiklenecektir. Yok eğer Lambda Authorizer yapılandırılmışsa sürecin devamı gerçekleşecektir. - 3. Adım
Hedef lambda function tetiklenmeden önce istek yapan kişinin kimliği aşağıdaki yollarla doğrulanmaya çalışılır.- Bir OAuth access token’ı alabilmek için bir OAuth provider’ını çağırmak
- SAML onayı almak için bir SAML provider’ını çağırmak
- Request parameter değerine göre bir IAM politikası oluşturmak
- Credentials bilgilerini bir database’den almak
- 4. Adım
Bu yollardan hangisi olursa olsun başarılı bir netice söz konusuysa lambda authorizer, en az bir IAM politikası(IAM policy) ve ana tanımlayıcı(principal identifier) içeren bir output object return ederek erişim izni verir. - 5. Adım
Ve son olarak API Gateway gelen IAM policy’i değerlendirir ve bu değerlendirme neticesinde erişim reddedilmişse 403 ACCESS_DENIED gibi uygun bir HTTP durum kodu döndürür. Yok eğer erişime izin verilmişse API Gateway ilgili lambda function’ı tetikler.
Tüm bu süreçte lambda authorizer ayarlarında önbelleğe alma(caching) etkinleştirildiyse eğer API Gateway bu policy’i önbelleğe alacaktır ve böylece bir sonraki request’te lambda authorizer sürecinin yeniden çalıştırılmasına gerek kalınmayacaktır.
.NET İle Lambda Authorizer Kullanımı
Şimdi .NET ile lambda authorizer kullanımını pratiksel olarak inceliyor olacağız. Bunun için örnek olarak bir önceki .NET İle Amazon API Gateway – AWS Lambda & DynamoDB Entegrasyonu başlıklı makalede oluşturduğumuz şu projeyi temel olarak baz alıyor olacağız.(İlgili makaledeki çalışmayı bu içeriğimiz için ben deniz baştan sona farklı bir projede tekrar geliştirmiş bulunuyorum. Bu içerikle birlikte geliştirilmiş nihai projeyi makalemizin sonundaki github adresinden edinebilirsiniz.)
Bir API Gateway’in tüm endpoint’lerine tek bir Lambda Authorizer eklenebileceği gibi her bir endpoint için ayrı Lambda Authorizer’da oluşturulabilir. Bu tamamen sizlerin tercihine, kullanım durumuna ve işin gereğine bağlıdır.
Altyapı projemizdeki genel davranışı hızlıca anımsarsak eğer; ‘persons’ adında bir DynamoDB tablosuna veri ekleyen, eklenmiş olan tüm person’ları elde eden ve id bazlı sorgulama gerçekleştiren üç adet lambda function’ımız mevcuttur. Ayrıca bu function’lara dışarıdan erişim göstermemizi sağlayan ve üç adet endpoint’e sahip olan API Gateway tasarlanmıştır. Şimdi bizler Lambda Authorizer ile bu endpoint’lerin güvenliğini sağlamaya çalışıyor olacağız. Bunun için iki yol benimseyeceğiz;
1. Yol | 2. Yol |
---|---|
Username, password vs. gibi gönderilen kimlik bilgilerini(credentials) okuyacak ve bunları doğrulayacak bir lambda function geliştireceğiz. Bu doğrulama neticesinde bir JWT döndürecek ve client’ı böylece yetkilendireceğiz. Client elde edeceği bu JWT değeri ile request’lerini gerçekleştirecektir.
Yani anlayacağınız authentication sorumluluğunu üstlenecek bir uygulama oluşturacağız. |
Lambda Authorizer, özünde authorizer olarak davranış sergileyen ap ayrı bir AWS Lambda projesidir. Yani function’dır. Bu function’ın sorumluluğu, client’lar tarafından yapılan istek süreçlerinde tarafımıza iletilen JWT değerlerini doğrulamaktır. Bu doğrulama neticesinde hedef lambda function tetiklenecek yok eğer doğrulama gerçekleşmezse Amazon API Gateway tarafından yorumlanabilecek/anlaşılabilecek bir IAM politikası döndürülecektir.
Bu da bir authorization sorumluluğu üstlenecek uygulama olacaktır. |
O halde hadi başlayalım…
Authentication uygulamasını geliştirme;
- Adım 1 – (Uygulama Oluşturma)
Solution içerisinde authentication işlemlerini üstlenmesi için AWS.Lambda.Authorizer.Example.Authentication adında bir ‘Empty Function’ oluşturalım. - Adım 2 – (User Modelini Oluşturma)
Doğrulama sürecinde kullanıcı bilgilerini tutmak için bir model tasarlayalım.[DynamoDBTable("users")] public class User { [DynamoDBHashKey("email"), DynamoDBProperty("email")] [JsonPropertyName("email")] public string? Email { get; set; } [DynamoDBProperty("username")] [JsonPropertyName("username")] public string? Username { get; set; } [DynamoDBProperty("password")] [JsonPropertyName("password")] public string? Password { get; set; } }
- Adım 3 – DynamoDB’de Tablo Oluşturma
Tasarlanan modele uygun DynamoDB’de tablo oluşturalım. - Adım 4 – Token Generate Etme
Kullanıcı bilgilerini doğruladıktan sonra token üretimini gerçekleştirecek servisi tasarlayalım.public interface ITokenGenerator { string GenerateJWT(User user, int minute); }
public class TokenGenerator : ITokenGenerator { public string GenerateJWT(User user, int minute) { List<Claim> claims = new() { new(ClaimTypes.Email, user.Email), new(ClaimTypes.Name,user.Username) }; byte[] secretKey = Encoding.UTF8.GetBytes("doldur be meyhaneci, boş kalmasın kadehim..."); SigningCredentials credentials = new( key: new SymmetricSecurityKey(secretKey), algorithm: SecurityAlgorithms.HmacSha256); JwtSecurityToken token = new( claims: claims, expires: DateTime.UtcNow.AddMinutes(minute), signingCredentials: credentials); JwtSecurityTokenHandler tokenHandler = new(); return tokenHandler.WriteToken(token); } }
- Adım 5 – GenerateTokenAsync Function’ını Tasarlama
Bu adımda da oluşturduğumuz ‘TokenGenerator’ sınıfını kullanarak gerekli JWT üretimini gerçekleştirecek function’ı tasarlayalım.using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DataModel; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; using AWS.Lambda.Authorizer.Example.Authentication.Models; using AWS.Lambda.Authorizer.Example.Authentication.Services; using AWS.Lambda.Authorizer.Example.Authentication.Services.Abstractions; using Microsoft.Extensions.DependencyInjection; using System.Text.Json; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace AWS.Lambda.Authorizer.Example.Authentication; public class Function { readonly IServiceProvider _serviceProvider; public Function() { ServiceCollection serviceCollection = new(); serviceCollection.AddScoped<ITokenGenerator, TokenGenerator>(); _serviceProvider = serviceCollection.BuildServiceProvider(); } public async Task<string> GenerateTokenAsync(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) { User? user = JsonSerializer.Deserialize<User>(request.Body); AmazonDynamoDBClient dynamoDBClient = new(); DynamoDBContext dynamoDBContext = new(dynamoDBClient); User hasUser = await dynamoDBContext.LoadAsync<User>(user.Email); if (hasUser != null) { if (hasUser.Password != user.Password) throw new Exception("Invalid credentials!"); using (IServiceScope scope = _serviceProvider.CreateScope()) { ITokenGenerator tokenGenerator = scope.ServiceProvider.GetService<ITokenGenerator>(); return tokenGenerator.GenerateJWT(user, 3); } } throw new Exception("User not found!"); } }
- Adım 5 – Function’ı Publish Etme ve DynamoDB Erişim Politikasıyla Eşleştirme
Şimdide oluşturduğumuz function’ı publish edelim.
Function’ı publish ettikten sonra AWS Lambda servisinden yandaki gibi kontrol etmenizde fayda vardır. Şimdi bu lambda function ile DynamoDB arasında ilişki kuracağız. Bunun için IAM Service üzerinden politikalara geliniz ve tüm aksiyonlar eşliğinde DynamoDB erişim iznine sahip olan politikayı ilgili lambda function’la ilişkilendiriniz. - Adım 6 – API Gateway İle Endpoint Alma
Ve nihai olarak artık function’a erişip, authentication işlemlerini başarıyla gerçekleştirebilmek için bir API Gateway üzerinden endpoint ayarlayalım. Tabi burada isterseniz mevcut API Gateway’lerden birini kullanabilir yahut sırf bu işlem için başlı başına bir API Gateway oluşturabilirsiniz. Ben genel anlamda endpoint’in origin’i değişmemesi için var olan bir API Gateway üzerinden bu function’a özel bir entegrasyonda bulunacağım. Bunun için API Gateway servisinde aşağıdaki görseldeki gibi davranış sergilenmesi yeterli olacaktır.
Görüldüğü üzere yeni eklediğimiz function’ı API Gateway ile ilişkilendirdik. Bu işlemin ardından aşağıdaki gibi endpoint tanımlamasında bulunabiliriz.
- Adım 7 – Test
Ve son olarak oluşturduğumuz endpoint üzerinden token talebinde bulunabiliriz.
Evet, görüldüğü üzere authentication uygulamasını başarıyla geliştirmiş bulunuyoruz. Şimdi sıra authorization uygulamasında…
Authorization(Lambda Authorizer) uygulamasını geliştirme;
- Adım 1 – Uygulama Oluşturma
Yine ilk olarak aynı solution içerisinde bu sefer de authorization işlemlerini üstlenmesi için AWS.Lambda.Authorizer.Example.Authorization adında bir proje oluşturalım. - Adım 2 – Token’ı Doğrulama
Authentication uygulamasından alınmış olan token’ı doğrulamak için gerekli servisi oluşturalım.public interface ITokenValidator { ClaimsPrincipal ValidateToken(string accessToken); }
public class TokenValidator : ITokenValidator { public ClaimsPrincipal ValidateToken(string accessToken) { JwtSecurityTokenHandler tokenHandler = new(); byte[] secretKey = Encoding.UTF8.GetBytes("doldur be meyhaneci, boş kalmasın kadehim..."); TokenValidationParameters validationParameters = new() { ValidateLifetime = true, ValidateAudience = false, ValidateIssuer = false, IssuerSigningKey = new SymmetricSecurityKey(secretKey), }; try { ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken( token: accessToken, validationParameters: validationParameters, validatedToken: out SecurityToken _); return claimsPrincipal; } catch (Exception ex) { return null; } } }
- Adım 3 – ValidateToken Function’ını Tasarlama
Şimdi de oluşturduğumuz ‘TokenValidator’ sınıfını kullanarak token’ı doğrulayacak olan function’ı oluşturalım.using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; using AWS.Lambda.Authorizer.Example.Authorization.Services; using AWS.Lambda.Authorizer.Example.Authorization.Services.Abstractions; using Microsoft.Extensions.DependencyInjection; using System.Security.Claims; using System.Text.Json; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace AWS.Lambda.Authorizer.Example.Authorization; public class Function { readonly IServiceProvider _serviceProvider; public Function() { ServiceCollection serviceCollection = new(); serviceCollection.AddScoped<ITokenValidator, TokenValidator>(); _serviceProvider = serviceCollection.BuildServiceProvider(); } public APIGatewayCustomAuthorizerResponse TokenValidator(APIGatewayCustomAuthorizerRequest request, ILambdaContext context) { string accessToken = request.Headers["authorization"]; ITokenValidator tokenValidator = _serviceProvider.GetRequiredService<ITokenValidator>(); ClaimsPrincipal claimsPrincipal = tokenValidator.ValidateToken(accessToken); string? principalId = "401"; if (claimsPrincipal is { Identity.IsAuthenticated: true }) principalId = claimsPrincipal.FindFirst(ClaimTypes.Name)?.Value; return new APIGatewayCustomAuthorizerResponse() { PrincipalID = principalId, PolicyDocument = new APIGatewayCustomAuthorizerPolicy { Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement> { new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement() { Effect = claimsPrincipal != null ? "Allow" : "Deny", Resource = new HashSet<string>{ "arn:aws:execute-api:ap-south-1:567921601443:uyv8v6df12/*/*" }, Action = new HashSet<string>{ "execute-api:Invoke" } } } } }; } }
Evet, oluşturulan function’ın detaylarından bahsetmemiz gerektiği aşikar. O halde ilk olarak
APIGatewayCustomAuthorizerRequest
veAPIGatewayCustomAuthorizerResponse
olmak üzere parametre ve geri dönüş türlerini ele alarak başlayalım. Bu referanslar, Lambda Authorizer için kullanılan özel sınıflardır.APIGatewayCustomAuthorizerRequest
sınıfı JWT doğrulamak için gelen istekleri temsil ederken bizlere bu istekle ilgili headers, query string ve diğer bilgileri getirmektedir. Aynı şekildeAPIGatewayCustomAuthorizerResponse
sınıfı ise yetkilendirme neticesinde gerekli bilgileri içerecek olan (adı üzerinde) response’u temsil etmektedir.APIGatewayCustomAuthorizerResponse
nesnesinin property’lerine gelirsek eğer;PrincipalID
property’si, JWT doğrulandığı taktirde ‘name’ claim’inin değerini, yok eğer doğrulanmazsa “401” değerini almaktadır.PolicyDocument
property’si,APIGatewayCustomAuthorizerPolicy
türünden nesne alan bu property lambda authorizer tarafından API Gateway’e gönderilen ve yetkilendirmenin başarılı olup olmadığını ifade edecek olan yanıt amaçlı kullanılmaktadır. Bu nesnenin de ‘Effect’, ‘Resource’ ve ‘Action’ property’leri mevcuttur. Bu property’lerden; ‘Effect’, yetkilendirmenin başarılı olup olmadığını ifade etmektedir. Örnekte de görüldüğü üzere başarılıysa ‘Allow’ değilse ‘Deny’ değerlerini almaktadır. ‘Resource’, işlem yapılacak kaynağın adını temsil etmektedir. ‘Action’ ise işlem yapılacak kaynağın üzerinde yapılacak eylemi/işlemi temsil etmektedir.Tabi burada ‘Resource’ property’sine biraz daha odaklanmamız gerekmektedir. ‘Resource’, yapısal olarak bir ARN(Amazon Resource Name) formatında değer beklemektedir. ARN’ler AWS’de ki kaynakların benzersiz tanımlayıcısıdırlar. Misal olarak bir lambda function’ın ARN değeri şöyle olmalıdır:
arn:aws:execute-api:<aws-region>:<aws-account-id>:<amazon-gateway-id>/*/*- <aws-region> : AWS servislerinin bulunduğu bölgenin değerini ifade eder.
- <aws-account-id> : AWS Account ID’yi ifade eder. Bu değer için AWS sayfasından sağ üst köşedeki profilinize tıklamanız yeterli olacaktır.
- <amazon-gateway-id> : API Gateway’in ID değerini ifade eder. Bu değer için ise ilgili API Gateway’in detaylarına bakmanız yeterli olacaktır.
- Adım 4 – Function’ı Publish Etme
Oluşturulan function’ı publish edelim. - Adım 5 – Endpoint’e Lambda Authorizer Olarak Tanımlama
Ve son olarak istenilen herhangi bir endpoint’in güvenliğini sağlayabilmek için ilgili function’ı o endpoint’e lambda authorizer olarak ekleyelim. Bunun için API Gateway sayfasında istediğiniz endpoint’i seçebilirsiniz.Misal olarak bizler yukarıdaki ekran görüntüsünde olduğu gibi GET türünden olan ‘/persons’ endpoint’ine bu function’ı authorizer olarak atamak istediğimiz taktirde ilgili endpoint’i seçip devamında sağ taraftaki ‘Attach authorization’ butonuna tıklamalıyız.
Ardından ‘Create and attach an authorizer’ butonuna tıklayarak devam edelim.
Devamında ise authorizer type olarak ‘Lambda’ sekmesini seçelim ve gerekli alanları yukarıdaki gibi dolduralım.
Tasarladığımız lambda authorizer gelen istekleri doğrulamak için IAM politikası döndürdüğünden dolayı ‘IAM Policy’ sekmesini seçelim.
Tüm bu işlemler neticesinde API Gateway ekranında ‘Authorization’ sayfasına baktığımızda ilgili endpoint’in ‘Lambda Auth’ ile işaretlendiğini görerek, yetkilendirilmeye tabii tutulduğunu anlayabilmekteyiz.
- Adım 6 – Test
Artık yaptığımız tüm bu çalışmaların nihai testini gerçekleştirebiliriz.
Evet… Görüldüğü üzere Lambda Authorizer yapılandırması işte bu kadar 🙂 Kıssadan hisse yaparsak, bir authentication bir de authorization işlemlerinin sorumluluğunu üstlenen ayrı function’lar tarafından rahatlıkla gerçekleştirilebilmekte ve hangi endpoint’i koruyacaksak ilgili authorization uygulaması onunla eşleştirilmektedir.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek projeyi aşağıdaki github adresinden edinebilirsiniz.
https://github.com/gncyyldz/AWS.Lambda.Authorizer.Example