Cell CMS — Autenticando o Admin
Uma vez, durante um projeto, levei o maior esporro de um amigo por jogar para sprints tardios a parte de autenticação. Ele dizia que:
“Fazer isso é jogar algo de muito valor para o usuário pro canto, vai gerar a maior dor de cabeça para amarrar todos os Models, Services e Rotinas. Isso deveria ter sido uma das primeiras coisas a serem entregues!”
Ele estava tremendamente certo! Eu, de fato, adiava a parte de autenticação pois era, na época, era receoso com fazer a implementação disso do zero. Lição aprendida arduamente, agora todo projeto já começo pensando em como irei autenticar e autorizar os usuários.
Antes de começarmos o post de hoje, deixo aqui o link para o código fonte. Não colocarei todos os refactors e ajustes nos posts, porém tentarei manter todos os commits atômicos , para que seja prático consultar exatamente o que foi feito entre cada feature/post!
Vamos começar preparando o nosso Tenant no Azure. Caso você não tenha uma conta no Azure crie-a (existem até promoções para novas contas, como créditos extras ou até mais funcionalidades gratuítas). O Recurso que vamos utilizar é gratuíto para o uso simples, apenas se quisermos algumas funcionalidade “empresariais” que teriamos de realizar um upgrade para versões pagas.
Criada a sua conta, acesse https://portal.azure.com e busque por “Azure Active Directory” na busca de recursos:

Dentro do blade do Azure AD procure por “ App registrations ”, entre nela e clique em “ New registration ”:

Escolha um nome para sua aplicação (no nosso caso “Cell CMS”), que tipo de contas podem utilizar sua aplicação e adicione a URL “ https://localhost:5001/oauth2-redirect.html ” como uma Web Redirect URI.

A URL https://localhost:5001/oauth2-redirect.html é o padrão do Swashbuckle.AspnetCore.SwaggerUi. Ela pode ser alterada, via código, porém costumamos utilizar ela em seu valor default
Uma breve explicação sobre os tipos de contas (maiores detalhes podem ser encontrados na documentação)
- Accounts in this organizational directory only : O nível mais restrito, apenas pessoas que já estiverem cadastradas em nosso Azure poderão fazer login no nosso app
- Accounts in any organizational directory : O nível intermediária, apenas pessoas com contas de trabalho ou educacionais poderão utilizar o App. Por exemplo uma pessoa com a conta aluno@universidade.onmicrosoft.com poderia fazer login no nosso app.
- Accounts in any organizational directory and personal Microsoft Accounts: O nível mais amplo, todos os tipos de contas da Microsoft (pessoais, empresariais, educacionais, xbox, skype, etc etc etc) poderão fazer login no nosso app.
Criado o app anote os seguintes valores que são exibidos na tela, os utilizaremos mais tarde:
- Application (client) id: É um ID que identifica nosso app, será utilizado para identificar “quem” está pedindo autenticação no Azure AD
- Directory (tenant) id: É um ID que identifica nosso tenant (ou seja, nosso Azure AD), poderemos utilizar este no futuro para diferenciar tenants.

Em seguida, clique em “ Endpoints ” e anote os seguintes endpoints:
- OAuth 2.0 Authorization endpoint (v2): É o endpoint que vai permitir que os usuários do Azure AD “autorizem” nosso app a ver quem eles são
- OAuth 2.0 token endpoints (v2): É o endpoint que vai emitir tokens, desde que autorizados pelo usuário, que identificam o usuário do Azure AD
- OpenID Connect metadata document : É um json que possui todas as configurações necessárias para clients (por exemplo nosso futuro frontend) saberem “conversar” com o Azure AD.

Agora precisamos habilitar o Implicit Flow para que o SwaggerUI e o nosso futuro frontend possam utilizar este fluxo de autenticação. Futuramente farei um post explicando cada fluxo em detalhes, por hora acesse a aba “ Authentication ” e habilite os dois checks para o Implicit Grant:

Finalmente, vamos à aba Expose an API , defina um applicationId para a sua API e crie um novo Scope, conforme:


A ideia aqui é criar um Scope que permitirá que apenas alguns usuários possam utilizar nosso SwaggerUi. Criado o Scope, navegue à aba “Api Permissions” e escolha a opção “ Add a Permission ”, aba “ My APIs ” e selecione “ Cell CMS ”:
Agora selecione o Scope que criamos anteriormente e clique em Add Permissions :

Vamos voltar à api! Para quem quiser acompanhar direto pelos commits é só procurar os commits relacionadas ao branch feature/azure-ad-auth.
Nossa primeira preocupação vai ser trazer , para a API , as configurações! Lembram os campos que anotamos? Precisamos deles na API. Para isso, vamos criar uma interface IAzureAdSettings que será implementada pela classe AzureAdSettings:
namespace CellCms.Api.Settings | |
{ | |
/// <summary> | |
/// Configurações do Azure AD. | |
/// </summary> | |
public class AzureAdSettings : IAzureAdSettings | |
{ | |
/// <summary> | |
/// Chave para buscar as configurações. | |
/// </summary> | |
public const string SettingsKey = "AzureAd"; | |
public string ClientId { get; set; } | |
public string TokenEndpoint { get; set; } | |
public string AuthorizeEndpoint { get; set; } | |
public string MetadataEndpoint { get; set; } | |
} | |
} |
namespace CellCms.Api.Settings | |
{ | |
/// <summary> | |
/// Descreve as propriedades para configurar autenticação através do Azure AD. | |
/// </summary> | |
public interface IAzureAdSettings | |
{ | |
/// <summary> | |
/// ClientId esperado. | |
/// </summary> | |
string ClientId { get; } | |
/// <summary> | |
/// Endpoint para obter access tokens. | |
/// </summary> | |
string TokenEndpoint { get; } | |
/// <summary> | |
/// Endpoint para obter autorização dos usuários. | |
/// </summary> | |
string AuthorizeEndpoint { get; } | |
/// <summary> | |
/// Endpoint para buscar o well-known document. | |
/// </summary> | |
string MetadataEndpoint { get; } | |
} | |
} |
A constante SettingsKey está ali para facilitar nossa vida ao buscar estes dados na IConfiguration do DotNet Core. Vamos editar nosso appsettings.json para armazenar estes valores. Substitua apenas o valor para o ClientId pelo criado em seu Azure Active Directory
{ | |
..., | |
"AzureAd": { | |
"ClientId": "setMeOnSecrets.json", | |
"MetadataEndpoint": "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", | |
"AuthorizeEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", | |
"TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token" | |
} | |
} |
Para definirmos o ClientId de maneira mais segura, clique com o botão direito no projeto da API e escolha Manage User Settings, alterando o json que ele abrir no Visual Studio para conter o AzureAd__ClientId:

Finalizado nossos preparativos, vamos utilizar estas configurações! Vamos adicionar , já, um novo NuGet que precisaremos para lidar com JWT: Microsoft.AspNetCore.Authentication.JwtBearer. Adicionado o NuGet, vamos voltar ao Startup.cs e planejar nossas alterações:
- Precisamos buscar as Configurações do Azure AD através do nosso IConfiguration
- Com estas configurações vamos adicionar e configurar os serviços de autenticação
- Vamos, finalmente, utilizar o Middleware de autenticação
namespace CellCms.Api | |
{ | |
public class Startup | |
{ | |
// This method gets called by the runtime. Use this method to add services to the container. | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
// Buscando as nossas configurações do Azure AD | |
var aadSettings = Configuration.GetSection(AzureAdSettings.SettingsKey).Get<AzureAdSettings>(); | |
if (aadSettings is null) | |
{ | |
throw new InvalidOperationException("As configurações do Azure AD são obrigatórias para o Cell CMS."); | |
} | |
// Adicionando os serviços de autenticação | |
services | |
.AddAuthentication(c => | |
{ | |
c.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // Definindo que os JWTs serão nosso schema de autenticação padrão | |
c.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; // Definindo que os JWTs serão nosso schema padrão para challenges de autenticação | |
}) | |
.AddJwtBearer(c => | |
{ | |
c.IncludeErrorDetails = true; // Garantindo que o Middleware nos retorno erros detalhados, em produção pode ser melhor desabilitar | |
c.SaveToken = true; // Indica que o Token deve ser salvo | |
// As linhas abaixo configuram a validação do Token, com base nas nossas configurações | |
c.MetadataAddress = aadSettings.MetadataEndpoint; | |
c.TokenValidationParameters = new TokenValidationParameters | |
{ | |
ValidAudience = aadSettings.ClientId, | |
ValidateAudience = true, | |
// Na documentação (https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant#update-your-code-to-handle-multiple-issuer-values) | |
// recomenda-se validar Guid-a-Guid os issuers para cada tenant que puder utilizar nosso App. | |
// Como nossa ideia aqui é fazer apenas o App, não vamos fazer este passo | |
ValidateIssuer = false | |
}; | |
}); | |
// ... Outros services | |
} | |
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
// ... Outros Middlewares | |
app.UseAuthentication(); | |
app.UseAuthorization(); | |
// ... Outros Middlewares | |
} | |
} | |
} |
Agora precisamos informar ao Swagger.json que nossa API suporta autenticação e configurar para que o SwaggerUI consiga realizar a autenticação em nosso nome. Para atingir nosso objetivo vamos:
- Criar um filtro para que o Swashbuckle.Swagger saiba quais endpoints requerem autenticação
- Aplicar este filtro na geração do Swagger.json
- Definir , na API, os Scopes que configuramos para a aplicação
- Adicionar à geração do Swagger.json informações sobre nossos endpoints de autenticação
- Adicionar ao SwaggerUi o ClientId
O código abaixo já está considerando um refactor que realizei no branch feature/refactor-swagger-config, que move a configuração do Swagger para Extension Methods, deixando nosso Startup.cs mais limpo:
namespace CellCms.Api.Constants | |
{ | |
/// <summary> | |
/// Scopes para Cell CMS. | |
/// </summary> | |
public static class CellScopes | |
{ | |
// A ideia aqui é armazenar todas as nossas strings "fixas" em um único lugar, pra caso precisemos alterar ficar fácil. | |
/// <summary> | |
/// Permite acesso ao SwaggerUi para testes da API. | |
/// </summary> | |
public const string AcessoSwagger = "api://cell-cms-dev/SwaggerUi.Acesso"; | |
} | |
} |
namespace Microsoft.AspNetCore.Builder | |
{ | |
/// <summary> | |
/// Extensions para expor os middlewares do Swashbuckle. | |
/// </summary> | |
public static class CellSwaggerApplicationBuilderExtensions | |
{ | |
/// <summary> | |
/// Adiciona e configura o middleware para o SwaggerUI. | |
/// </summary> | |
/// <param name="app"></param> | |
/// <param name="configuration"></param> | |
/// <returns></returns> | |
public static IApplicationBuilder UseCellSwaggerUi(this IApplicationBuilder app, IConfiguration configuration) | |
{ | |
if (app is null) | |
{ | |
throw new ArgumentNullException(nameof(app)); | |
} | |
// Buscando as configurações do Azure AD. | |
var aadSettings = configuration.GetSection(AzureAdSettings.SettingsKey).Get<AzureAdSettings>(); | |
app.UseSwaggerUI(cfg => | |
{ | |
cfg.RoutePrefix = string.Empty; | |
cfg.SwaggerEndpoint("/swagger/v1/swagger.json", "Cell CMS API"); | |
cfg.OAuthClientId(aadSettings.ClientId); // Repassando nosso CLientId para facilitar o login via swagger | |
}); | |
return app; | |
} | |
} | |
} |
namespace Microsoft.Extensions.DependencyInjection | |
{ | |
/// <summary> | |
/// Extensions para a configuração de services relacionados ao Swagger. | |
/// </summary> | |
public static class CellSwaggerServicesExtensions | |
{ | |
/// <summary> | |
/// Configura e adiciona os serviços de geração do Swagger.Json do Cell CMS. | |
/// </summary> | |
/// <param name="services"></param> | |
/// <param name="configuration"></param> | |
/// <returns></returns> | |
public static IServiceCollection AddCellSwagger(this IServiceCollection services, IConfiguration configuration) | |
{ | |
if (services is null) | |
{ | |
throw new ArgumentNullException(nameof(services)); | |
} | |
// Buscando as configurações do Azure AD | |
var aadSettings = configuration.GetSection(AzureAdSettings.SettingsKey).Get<AzureAdSettings>(); | |
services.AddSwaggerGen(cfg => | |
{ | |
// Indicando, ao gerador do Swagger.json, que utilizamos autenticação | |
// Onde o Bearer é enviado | |
// E quais são as URLs para autorizar e obter tokens | |
cfg.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |
{ | |
In = ParameterLocation.Header, | |
Name = "Azure AD", | |
OpenIdConnectUrl = new Uri(aadSettings.MetadataEndpoint), | |
Type = SecuritySchemeType.OAuth2, | |
Flows = new OpenApiOAuthFlows | |
{ | |
Implicit = new OpenApiOAuthFlow | |
{ | |
AuthorizationUrl = new Uri(aadSettings.AuthorizeEndpoint), | |
TokenUrl = new Uri(aadSettings.TokenEndpoint), | |
Scopes = new Dictionary<string, string> | |
{ | |
{ CellScopes.AcessoSwagger, "Permite que o usuário faça login no SwaggerUi" } | |
} | |
} | |
} | |
}); | |
// Utilizando o filtro que criamos anteriormente | |
cfg.OperationFilter<SecurityRequirementsOperationFilter>(); | |
}); | |
return services; | |
} | |
} | |
} |
namespace CellCms.Api.Swagger | |
{ | |
/// <summary> | |
/// Filtro para operações protegidas. | |
/// </summary> | |
/// <see cref="https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/test/WebSites/OAuth2Integration/ResourceServer/Swagger/SecurityRequirementsOperationFilter.cs"/> | |
public class SecurityRequirementsOperationFilter : IOperationFilter | |
{ | |
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |
{ | |
// Policy names map to scopes | |
var requiredScopes = context.MethodInfo | |
.GetCustomAttributes(true) | |
.OfType<AuthorizeAttribute>() | |
.Select(attr => attr.Policy) | |
.Distinct(); | |
if (requiredScopes.Any()) | |
{ | |
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); | |
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); | |
var oAuthScheme = new OpenApiSecurityScheme | |
{ | |
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } | |
}; | |
operation.Security = new List<OpenApiSecurityRequirement> | |
{ | |
new OpenApiSecurityRequirement | |
{ | |
[ oAuthScheme ] = requiredScopes.ToList() | |
} | |
}; | |
} | |
} | |
} | |
} |
Finalmente, para testarmos toda essa configuração, vamos ao WeatherForecastController adicionar o attribute [Authorize] ao método Get(). Feito isso, basta executar sua API, você será recepcionado pelo seguinte SwaggerUi:
Conseguirá fazer login através do Authorize :
E, finalmente, conseguirá chamar o endpoint protegido:

Finalizaremos por aqui este post! No próximo post partiremos para a implementação inicial da Pesistência, utilizando EntityFrameworkCore e Sqlite! Caso queria ver o resultado final no GitHub basta acessar este merge commit. Obrigado por ler meu post e abraços!
Top comments (0)