Tratamento Personalizado de Respostas para Tokens JWT Inválidos ou Expirados e Configuração do Prefixo Bearer no Swagger

No desenvolvimento de APIs com ASP.NET Core, é comum encontrar dois desafios relacionados à autenticação JWT. O primeiro é a necessidade de digitar manualmente o prefixo "Bearer " ao inserir o token no Swagger UI, o que pode ser inconveniente. O segundo, mais crítico, ocorre quando um token expira: o front end frequentemente receba apenas uma exceção genérica, forçando o usuário a recarergar a página manualmente para efetuar um novo login, gerando uma experiência ruim.

A solução envolve customizar os eventos do handler de autenticação JWT no ASP.NET Core. O trecho a seguir demonstra a configuração principal, incluindo a definição de políticas de autorização, parâmetros de validação do token e a manipulação de eventos como OnAuthenticationFailed e OnMessageReceived.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Text;

namespace ProjetoApi.Configuracao
{
    public static class ConfiguracaoAutenticacao
    {
        public static IServiceCollection ConfigurarAutenticacaoJwt(this IServiceCollection servicos, IConfiguration configuracao)
        {
            servicos.AddAuthorization(opt =>
            {
                opt.AddPolicy("SomenteUsuarios", policy => policy.RequireRole("Usuario"));
                opt.AddPolicy("AdminOuSistema", policy => policy.RequireRole("Administrador", "Sistema"));
            });

            var chaveBase64 = configuracao["Jwt:Chave"];
            var chaveBytes = Encoding.ASCII.GetBytes(chaveBase64);
            var chaveAssinatura = new SymmetricSecurityKey(chaveBytes);

            var parametrosValidacaoToken = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = chaveAssinatura,
                ValidateIssuer = true,
                ValidIssuer = configuracao["Jwt:Emissor"],
                ValidateAudience = true,
                ValidAudience = configuracao["Jwt:Audiencia"],
                ValidateLifetime = true,
                ClockSkew = TimeSpan.FromMinutes(5),
                RequireExpirationTime = true
            };

            servicos.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(opcoes =>
                {
                    opcoes.TokenValidationParameters = parametrosValidacaoToken;
                    opcoes.Events = new JwtBearerEvents
                    {
                        OnAuthenticationFailed = contexto =>
                        {
                            if (!contexto.Response.Headers.ContainsKey("Access-Control-Allow-Origin"))
                            {
                                contexto.Response.Headers.Add("Access-Control-Allow-Origin", "*");
                            }
                            contexto.Response.Headers.Add("X-Token-Expirado", "true");
                            contexto.Response.Headers.Add("X-Token-Valido", "false");
                            return Task.CompletedTask;
                        },
                        OnMessageReceived = contextoRequisicao =>
                        {
                            var cabecalhoAuth = contextoRequisicao.HttpContext.Request.Headers["Authorization"].ToString();
                            if (!string.IsNullOrEmpty(cabecalhoAuth) && !cabecalhoAuth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                            {
                                var valorFormatado = $"Bearer {cabecalhoAuth}";
                                contextoRequisicao.HttpContext.Request.Headers["Authorization"] = valorFormatado;
                            }
                            return Task.CompletedTask;
                        }
                    };
                });

            return servicos;
        }
    }
}

Para que o frontend consiga ler os cabeçalhos de resposta personalizados (como X-Token-Expirado) em requisições de outro domínio, a política de CORS precisa expô-los explicitamente. A configuração abaixo permite qualquer origem e expõe os cabeçalhos relevantes.

using Microsoft.Extensions.DependencyInjection;

namespace ProjetoApi.Configuracao
{
    public static class ConfiguracaoCors
    {
        public static IServiceCollection PermitirCorsPersonalizado(this IServiceCollection servicos)
        {
            servicos.AddCors(opcoes =>
            {
                opcoes.AddPolicy("PoliticaCorsPermissiva", construtor =>
                    construtor
                        .AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .WithExposedHeaders("X-Token-Expirado", "X-Token-Valido"));
            });

            return servicos;
        }
    }
}

Por fim, para aprimorar a experiência de desenvolvimento, configura-se o Swagger para gerar a documentação da API. A seguir, a ativação do middleware do Swagger UI no pipeline da aplicação.

app.UseSwagger();
app.UseSwaggerUI(configuracao =>
{
    configuracao.SwaggerEndpoint("/swagger/v1/swagger.json", "API do Projeto - v1");
    configuracao.RoutePrefix = string.Empty; // Acessa na raiz do site
});

A integração com a autenticação JWT no Swagger é feita na fase de serviços. O código a seguir configura o gerador de documentação e adiciona um botão para inserir o token no topo da interface do Swagger UI, permitindo que o valor seja enviado no cabeçalho Authorization das requisições de teste.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System;
using System.IO;
using System.Reflection;

namespace ProjetoApi.Configuracao
{
    public static class ConfiguracaoSwagger
    {
        public static IServiceCollection ConfigurarDocumentacaoSwagger(this IServiceCollection servicos)
        {
            servicos.AddSwaggerGen(configurador =>
            {
                configurador.SwaggerDoc("v1", new OpenApiInfo
                {
                    Title = "Documentação da API do Projeto",
                    Version = "1.0"
                });

                var nomeArquivoXml = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var caminhoArquivo = Path.Combine(AppContext.BaseDirectory, nomeArquivoXml);
                configurador.IncludeXmlComments(caminhoArquivo);

                configurador.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    Description = "Insira o token JWT com o prefixo 'Bearer '. Exemplo: 'Bearer eyJhb...'",
                    Name = "Authorization",
                    In = ParameterLocation.Header,
                    Type = SecuritySchemeType.ApiKey,
                    Scheme = "Bearer"
                });

                configurador.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            }
                        },
                        Array.Empty<string>()
                    }
                });
            });

            return servicos;
        }
    }
}

Tags: ASP.NET Core jwt Swagger autenticacao middleware

Publicado em 6-2 22:27 por Thomas