Nesses últimos dias, me deparei com um desafio e por isso resolvi escrever esse post. O desafio foi: como podemos utilizar o Azure AD para proteger um gateway feito com o Ocelot?
Antes de começarmos, sugiro um pequeno estudo sobre o que são gateways e a lib Ocelot para .NET!
Vamos inicialmente criar um web api para servir como fonte de dados. Em uma aplicação de produção, essa aplicação não seria exposta na internet, e poderia ser consumida (ou não, dependendo do caso) via gateway.
dotnet new webapi -n App.Api
Nesse projeto, essa web api será protegida somente pelo gateway, não precisando validar access tokens para cada requisição.
Para o gateway, vamos criar um projeto com o template web do .NET:
dotnet new web -n App.Gateway
E adicionaremos os seguintes pacotes NuGet:
dotnet add package Microsoft.Identity.Web
dotnet add package Ocelot
Eu já escrevi uma série de posts sobre como podemos proteger nossas aplicações com o Microsoft identity platform. Vamos usá-la como base para essa solução!
Como mostrado na série de posts de referência, vamos criar dois registros de aplicativos no Azure AD:
- App.Gateway;
- App.Client;
O App.Gateway será o aplicativo web protegido, e o App.Client será o aplicativo que irá fazer requisições para o gateway. Na nossa solução, a web api não será exposta na internet, por tanto não iremos protege-lá com o Azure AD.
Para o App.Gateway, vamos criar um App Role chamado Admin:
Vamos permitir que essa role seja atribuída a grupos ou usuários. A ideia é atribuirmos essa role ao nosso usuário para podermos ser autorizados a chamar os endpoints do gateway.
Para o client, vamos seguir como no post de referência: criar um app registration para ser usado pelo postman.
O código da nossa web api vai ser mantido como está, já para a aplicação do gateway, modificar o Program.cs:
using App.Gateway;
using Microsoft.Identity.Web;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddOcelot(builder.Configuration);
builder.Services.DecorateClaimAuthoriser();
var app = builder.Build();
app.UseAuthentication();
app.UseOcelot().Wait();
app.Run();
- Adicionamos o arquivo ocelot.json ao provedor de configurações;
- Protegemos a aplicação com o Microsoft identity platform;
- Configuramos o Ocelot;
Além disso, observe que adicionamos o builder.Services.DecorateClaimAuthoriser(). Esse método de extensão fará com que o arquivo ocelot.json seja lido de forma correta para as chaves do tipo "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" (mais detalhes aqui!).
using Ocelot.Authorization;
namespace App.Gateway;
public static class ServiceCollectionExtensions
{
public static IServiceCollection DecorateClaimAuthoriser(this IServiceCollection services)
{
var serviceDescriptor = services.First(x => x.ServiceType == typeof(IClaimsAuthorizer));
services.Remove(serviceDescriptor);
var newServiceDescriptor = new ServiceDescriptor(serviceDescriptor.ImplementationType, serviceDescriptor.ImplementationType, serviceDescriptor.Lifetime);
services.Add(newServiceDescriptor);
services.AddTransient<IClaimsAuthorizer, ClaimAuthorizerDecorator>();
return services;
}
}
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using System.Security.Claims;
using Ocelot.Authorization;
using Ocelot.Responses;
namespace App.Gateway;
public class ClaimAuthorizerDecorator : IClaimsAuthorizer
{
private readonly ClaimsAuthorizer _authoriser;
public ClaimAuthorizerDecorator(ClaimsAuthorizer authoriser)
{
_authoriser = authoriser;
}
public Response<bool> Authorize(ClaimsPrincipal claimsPrincipal,
Dictionary<string, string> routeClaimsRequirement,
List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues)
{
var newRouteClaimsRequirement = new Dictionary<string, string>();
foreach (var kvp in routeClaimsRequirement)
{
if (kvp.Key.StartsWith("http///"))
{
var key = kvp.Key.Replace("http///", "http://");
newRouteClaimsRequirement.Add(key, kvp.Value);
}
else
{
newRouteClaimsRequirement.Add(kvp.Key, kvp.Value);
}
}
return _authoriser.Authorize(claimsPrincipal, newRouteClaimsRequirement, urlPathPlaceholderNameAndValues);
}
}
Finalmente temos o arquivo ocelot.json:
{
"Routes": [
{
"DownstreamPathTemplate": "/WeatherForecast",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5014
}
],
"UpstreamPathTemplate": "/api/weather-forecast",
"UpstreamHttpMethod": [ "Get" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "Bearer"
},
"RouteClaimsRequirement": {
"http///schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin"
}
}
],
"GlobalConfiguration": {
"BaseUrl": "https://localhost:7096"
}
}
- Ajuste o valor da chave Port com a porta correta que você vai rodar a web api;
- O gateway retornará 403 se a requisição para o caminho /api/weather-forecast não possuir a role Admin;
Para finalizar, precisamos adicionar ao appsettings.json os dados do registro de aplicativo App.Gateway:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<your-domain>",
"ClientId": "<your-client-id>",
"TenantId": "<your-tenant-id>"
},
Dito isso, vamos executar o gateway e a web api:
dotnet run
Como mostrado no post de referência, vamos adquirir um novo access token via postman e fazer a requisição ao gateway.
Observe que estamos autenticados, porém, não possuímos a role Admin no access token. Vamos ao portal do Azure, na aba "Enterprise Application", e adicionar a app role Admin do App.Gateway ao usuário que estamos usando.
De volta ao postman, podemos fazer uma aquisição de um novo access token. Utilizando o site jwt.io, podemos ver que o access token recebido possui a role Admin.
Vamos refazer a requisição ao gateway utilizando esse novo access token:
Tudo ocorreu bem! Dessa forma podemos acessar web apis através de um gateway feito em Ocelot e protegido com o Azure AD.
Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!
Até a próxima, abraços!