fbpx
JWT no ASP.NET Core – Standalone
Publicado em: sábado, 19 de ago de 2017

Após o hangout que rolou nessa sexta estávamos discutindo JWT no ASP.NET Core (JSon Web Tokens) e ao apresentar um dos meus projetos cheguei a ficar envergonhado, pois eu havia dado uma certa volta para evitar a utilização de criptografia simétrica e acabei fazendo uma implementação de ISecurityTokenValidator o que é uma imensa volta para uma implementação padrão de geração tokens JWT. Bom, madrugada livre, resolvi acertar isso de uma vez e acabei transformando esse aprendizado em post.

A utilização de um JWT independente se dá porque não tenho uma autoridade externa no meu projeto, não tenho SSO, não tenho Identity, sequer OpenID ou logins sociais, tenho uma simples autenticação, baseada em hash, claro. O projeto consiste em uma aplicação standalone, geralmente utilizada por um ou dois usuário no máximo e para simplificar o deploy sequer tenho um banco de dados. A função da webapp é ser um utilitário de gestão. Assim, subir um gestor de identidade é too much para a necessidade do projeto e não soa tão bem. Após a vergonha alheia, resolvi fazer da forma certa, e nada melhor do que aproveitar a release do .NET Core 2.0 para entender, estudar e escrever sobre o tema. Espero em breve falar de JWT com WSO2 Identity Server, que é um tema legal também, mas por enquanto, vamos à implementação standalone, assim é plausível apresentar os requisitos para a implementação de JWT de forma mais clara, já que encontramos de tudo no nosso oráculo contemporâneo.

O JWT consiste em json estrutruado em Header e Payload assinado. Seus cabeçalhos são pertinentes ao protocolo e no payload você utiliza para colocar dados adicionais que ficam sob a sua escolha. Quais dados? Quem gera esses tokens é responsável por defini-los, é bem flexível, geralmente são adicionadas claims que definem características quaisquer dos seus usuários. Podem ser dados simples, como nome de usuário, friendly names, até roles e atributos. Como são assinados? Criptografia simétrica, chave pública e privada, certificados, há algumas muitas implementações possíveis. Todo esse entendimento é facilmente apresentado no jwt.io, aproveitei para tirar um print e mostrar de forma mais didática no que consiste um JWT.

Todos os pontos são relevantes, então na esquerda temos o token “encodado”, e quebrado em 3 elementos: Header, Payload e Assinatura. A assinatura garante a autenticidade do documento e esse é um desenho muito interessante para inclusive outras aplicações.

Sobre JWT no ASP.NET Core, há regras a serem consideradas:

  1. Durante a autenticação você precisa gerar o token e devolver para seu cliente. Pois esse token precisa ser enviado a cada requisição autenticada. A propósito, a geração da string que representa o JWT é um processo bem definido na plataforma e utiliza classes específicas como JwtSecurityToken e JwtSecurityTokenHandler.
  2. Actions ou Controllers inteiros que precisam de autorização, dependem do atributo [Authorize], independente de você da existência de uma policy adicional ou default.
  3. A propósito, é necessário definir uma política (policy), explícita ou padrão, mas esta só é utilizada quando o Controller ou Action está marcado com o atributo Authorize, o construtor desse atributo permite a utilização de um nome de política, há um exemplo aqui nos códigos.

#showMeTheCode

Autenticação

/// <summary>
/// Authenticate user with credentials
/// </summary>
/// <param name="username">Username</param>
/// <param name="password">Password</param>
/// <returns>JWT Token</returns>
internal string Authenticate(string username, string password)
{
    string jwtToDelivery = null;
    var user = this.users.SingleOrDefault(it => it.Username == username);
    if (user != null && BCrypt.Net.BCrypt.Verify(password, user.Password))
    {
        var jwt = new JwtSecurityToken(
            issuer: jwtConfiguration.Issuer,
            audience: jwtConfiguration.Audience,
            claims: new Claim[] {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
                new Claim(JwtRegisteredClaimNames.UniqueName, user.Username),
                new Claim("management", "management"),
            },
            expires: DateTime.UtcNow.AddMinutes(jwtConfiguration.TokenLifetimeInMinutes),
            signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfiguration.SymmetricSecurityKey)), SecurityAlgorithms.HmacSha256)
        );
        jwtToDelivery = new JwtSecurityTokenHandler().WriteToken(jwt);
    }
    return jwtToDelivery;
}

Alguns aspectos precisam ser considerados nessa implementação. Na linha 11 eu estou usando uma biblioteca chamada BCrypt para gestão de hash de senha (por si só é um tema interessante para entender e estudar). Nas linhas 14, 15, 21 e 22 a instância jwtConfiguration é usada na configuração de diversos parâmetros necessários para a configuração do ASP.NET Core. Esse objeto foi preenchido a partir da configuração da aplicação e devidamente setado na infra de injeção de dependência. Atenção, nenhum desses parâmetros deve ser null ou empty! Nas linhas 14 e 15 temos as definições de Issuer e Audience, esses são elementos importantíssimos para o fluxo, leve em conta o parágrafo anterior para que não sofra com pequenos erros omitidos pela infraestrutura. De qualquer forma vou falar sobre troubleshooting neste post ainda. Na linha 24 temos o método WriteToken da classe JwtSecurityTokenHandler sendo chamado para gerar o token que será enviado para o navegador. Não ignore isso, pois a infraestrutura do ASP.NET Core precisará validar a assinatura do seu token, a cada request, portanto, esse fluo é responsabilidade da infra do asp.net core, e gerar esse token com algoritmos próprios fará você fugir do protocolo, como estava acontecendo com a minha implementação anterior.

Configuração

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    
    ...
    
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(jwtBearerOptions =>
    {
        jwtBearerOptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
        {
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfiguration.SymmetricSecurityKey)),
            ValidIssuer = jwtConfiguration.Issuer,
            ValidAudience = jwtConfiguration.Audience,
        };
    });

    services.AddAuthorization(authorizationOptions =>
    {
        authorizationOptions.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().RequireClaim("management").Build();
    });
    
    ...
    
}

Acima temos a configuração final, utilizada dentro do método ConfigureServices da classe Startup. Assim como no exemplo anterior, o objeto jwtConfiguration está populado com as configurações do meu appsettings.json.

{
"SymmetricSecurityKey": "123456789123456789123456789123456789123456789",
"Issuer": "url-do-emissor-ou-outro-texto",
"Audience": "url-da-app-que-precisa-de-acesso",
"TokenLifetimeInMinutes": 10
}

Nesse exemplo estou usando uma política default, já que essa minha aplicação não precisa diferenciar políticas de autorização, ou faz-se tudo ou não faz nada. Mas você poderia trabalhar com políticas diversas, e assim flexibilizar o modelo de autorização de sua API. Vale a pena lembrar que para chegar até aqui foi necessário algum troubleshooting, e isso não é transparente pois a não ser que você faça alguma coisa muito grotesca, exceptions não serão lançadas, e as que serão, não pararão a execução, serão logadas no console e você ficará a ver navios, portanto vou à dica de 2 dolares: A imagem acima apresenta a classe JwtBearerEvents que você pode instanciar e atribuir a jwtBearerOptions.Events para interceptar os eventos do fluxo de JwtBarear. Eu ignorei o evento OnMessageReceived pois ele é repetitivo e não traz nenhum grande valor no troubleshooting, os outros 3 (OnAuthenticationFailedOnChallengeOnTokenValidated) te ajudam a entender o que está acontecendo. Como você pode ver no console, uma exception foi lançada e tratada. Nesse caso a exception acontece em virtude do token expirado, esse é um cenário comum, e não tem relação com erros de configuração, no entanto o fluxo é exatamente o mesmo quando há erros de configuração, portanto, enquanto implementa sua configuração de JWT é muito interessante manter esse código e esses breakepoints para que possa inspecionar cada um dos contextos e entender o que está errado..

info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[1]
Failed to validate the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMjhjNmUwZmMzNjE0MzlmODg0NjlhZGE5OGI4YzIzYiIsInVuaXF1ZV9uYW1lIjoibHVpemNhcmxvc2ZhcmlhIiwibWFuYWdlbWVudCI6Im1hbmFnZW1lbnQiLCJleHAiOjE1MDMxMjk2MTQsImlzcyI6ImVuZ2luZS14LWNvcGlsb3QiLCJhdWQiOiJlbmdpbmUteC1jb3BpbG90In0.b8aNfU6pTtA3jHAeizJZq3S68PDvzSh3KJp7TZ1Hhzo.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidAudienceException: IDX10208: Unable to validate audience. validationParameters.ValidAudience is null or whitespace and validationParameters.ValidAudiences is null.
at Microsoft.IdentityModel.Tokens.Validators.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateAudience(IEnumerable`1 audiences, JwtSecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwt, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.<HandleAuthenticateAsync>d__6.MoveNext()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: Failed to validate the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMjhjNmUwZmMzNjE0MzlmODg0NjlhZGE5OGI4YzIzYiIsInVuaXF1ZV9uYW1lIjoibHVpemNhcmxvc2ZhcmlhIiwibWFuYWdlbWVudCI6Im1hbmFnZW1lbnQiLCJleHAiOjE1MDMxMjk2MTQsImlzcyI6ImVuZ2luZS14LWNvcGlsb3QiLCJhdWQiOiJlbmdpbmUteC1jb3BpbG90In0.b8aNfU6pTtA3jHAeizJZq3S68PDvzSh3KJp7TZ1Hhzo.

Microsoft.IdentityModel.Tokens.SecurityTokenInvalidAudienceException: IDX10208: Unable to validate audience. validationParameters.ValidAudience is null or whitespace and validationParameters.ValidAudiences is null.
   at Microsoft.IdentityModel.Tokens.Validators.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateAudience(IEnumerable`1 audiences, JwtSecurityToken securityToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwt, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameter

Eu, para demonstrar um erro de configuração, troquei os valores das linhas 16 e 17 de “`ValidIssuer = jwtConfiguration.Issuer, ValidAudience = jwtConfiguration.Audience“` para “`ValidIssuer = string.Empty, ValidAudience = string.Empty“`, e como disse, misteriosamente não temos uma exception lançada, como esperado, temos apenas o log de uma. A infraestrutura simplesmente ignora o erro, logando-o, e não deixa a autenticação proceder. Para alívio o evento OnAuthenticationFailed é chamado sempre que ocorrer um erro na autenticação, com a inspeção do contexto é possível entender qual exception foi lançada e assim corrigir sua configuração. Vale lembrar que esse post é complementar e aborda um fluxo de autenticação local, com jwt. Existem algumas outras preocupações quando você está trabalhando com o Identity, por exemplo, facebook e outros. A forma de trabalhar ligeiramente diferente e você precisa estar mais compromissado com os valores de Issuer, Audience e Authority

Autorizando sua API

Antes que eu me esqueça, abaixo estão 3 modelos de utilização do Authorize. Se você seguir o exemplo acima, apenas o primeiro funcionará, no entanto, caso você utilize/defina políticas e roles, os outros 2 exemplos funcionarão, respectivamente olhando para uma política ou role.

[Route("api/[controller]")]
public class AuthenticationController : Controller
{
    
    ... 

    [HttpGet("check")]
    [Authorize]
    [Authorize(Policy = "api")]
    [Authorize(Roles = "admin")]
    public object Check()
    {
        var returnValue = new
        {
            isAuthenticated = this.User.Identity.IsAuthenticated,
            isFirstRun = authenticationService.IsFirstLogin
        };
        return returnValue;
    }
    
    ...
    
}

Outro ponto relevante é que, caso você não marque essa action com o atributo Authorize, o resultado de this.User.Identity.IsAuthenticated é falso! A infraestrutura de autenticação/autorização simplesmente é ignorada.

Adicionando o cabeçalho à requisição

private buildRequestOptions(params: any = null): RequestOptions {
    const token: string = sessionStorage.getItem("token");
    if (token) {
        const headers = new Headers();
        headers.append("Authorization", "Bearer " + token);
        return new RequestOptions({ headers: headers, params: params });
    }
    return null;
}

O exemplo acima é utilizando em uma aplicação angular 4, no meu caso eu escolhi guardar o token no SessionStorage mas poderia guardar no LocalStorage ou fazer um mix dos dois. Por fim, você saberá que configurou tudo direito quando o breakpoint do evento OnTokenValidated for alcançado. A classe TokenValidatedContext, usada no contexto desse método possui a propriedade SecurityToken e contém tudo o que você precisa para entender seu JWT. Ao chegar nesse ponto, provavelmente não faltará mais nada, e as partir daí, aconselho apagar esse código de troubleshooting.

Abaixo seguem alguns links legais que apresentam outras implementações sob outros cenários:

https://blogs.msdn.microsoft.com/webdev/2017/04/06/jwt-validation-and-authorization-in-asp-net-core/

Angular Token Based Authentication using Asp.net Core Web API and JSON Web Token

Bom, é isso! Por hoje é só!

O Cloud Native .NET é meu principal projeto.

Onde empenho energia para ajudar, acompanhar, direcionar Desenvolvedores, Líderes Técnicos e jovens Arquitetos na jornada Cloud Native.

Conduzo entregando a maior e mais completa stack de tecnologias do mercado.

Ao trabalhar com desenvolvedores experientes, eu consigo usar seu aprendizado com .NET, banco de dados, e arquitetura para encurtar a jornada.

Ao restringir à desenvolvedores .NET eu consigo usar do contexto de tecnologias e problemas do seu dia-a-dia, coisas que você conhece hoje, como WCF, WebForms, IIS e MVC, por exemplo, para mostrar a comparação entre o que você conhece e o que está sendo apresentado.

É assim que construímos fundamentos sólidos, digerindo a complexidade com didática, tornando o complexo, simples.

É assim que conseguimos tornar uma jornada densa, em um pacote de ~4 meses.

Eu não acredito que um desenvolvedor possa entender uma tecnologia sem compreender seus fundamentos. Ele no máximo consegue ser produtivo, mas isso não faz desse desenvolvedor um bom tomador de decisões técnicas.

É preciso entender os fundamentos para conseguir tomar boas decisões.

0 comentários

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

[docker de a a z]

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.