Utilizando tokens para segurança no asp.net core com JOSE e JWT

Facebooktwittergoogle_plusredditpinterestlinkedinmail

Olá pessoal, hoje vamos ver como implementar autenticação e segurança utilizando tokens, com JOSE (Javascript Object Segning and Encryption) e JWT (JSON Web Token) em uma aplicação asp.net core. Para começar, vamos criar uma nova aplicação web asp.net core no Visual Studio 2017, no wizard de criação de novo projeto, selecione a opção ASP.NET Core Web Application .NET Core), e de o nome de JoseJwtSample, na tela seguinte, selecione o template Web API e clique em OK.

Após a criação do projeto, vamos instalar dois pacotes utilizando o Nuget, para isso abra o console do Nuget e digite:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

E depois digite:

Install-Package jose-jwt

O primeiro pacote é um middleware asp.net core, que habilita uma aplicação para receber um OpenID Connect bearer token, e é esse pacote que irá validar o token, e permitir ou negar o acesso aos métodos de nossa aplicação web. O segundo pacote, é uma implementação JOSE e JWT para .net, e é com ele que iremos gerar e assinar nosso token.

O JWT é constituído de três partes separadas por ponto (.), que são: cabeçalho (header), payload, assinatura (signature). Dessa forma, o token fica parecido com:

xxxxx.yyyyy.zzzzz

Para mais detalhes sobre o JWT, acesse o site jwt.io, lá você encontrará detalhes sobre o formato, exemplos, e diversas bibliotecas que implementam o JWT em várias linguagens.

Agora vamos ao código, em nosso projeto, vamos criar uma classe para representar o nosso objeto JWT, que conterá as roles (permissões) de acesso para nosso site. Crie uma classe com o nome AuthorizationToken com o código abaixo:

using System.Collections.Generic;

namespace JoseJwtSample
{
    public class AuthorizationToken
    {
        public string Iss { get; private set; }

        public string Aud { get; private set; }

        public string Sub { get; private set; }

        public IList<string> Roles { get; private set; }

        public AuthorizationToken(string iss, string aud, string sub, IList<string> additionalRoles)
        {
            Iss = iss;
            Aud = aud;
            Sub = sub;
            Roles = new List<string>() { "User" };
            if (additionalRoles != null)
                foreach(string role in additionalRoles)
                    Roles.Add(role);
        }
    }
}

Nessa classe apenas definimos quatro propriedades que fazem parte de um JWT, e nós à usaremos para criar um token a partir de sua serialização utilizando o pacote JOSE que adicionamos em nosso projeto anteriormente. Agora vamos criar um novo controller com o nome TokenController, que será o responsável por gerar e devolver um toker válido que permitirá acesso à nossa aplicação, ele deverá ter o código abaixo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text;

namespace JoseJwtSample.Controllers
{
    [Route("api/[controller]")]
    public class TokenController : Controller
    {
        [AllowAnonymous]
        [HttpPost]
        public string Post()
        {
            AuthorizationToken token = new AuthorizationToken("http://desenvolvedor.ninja", "desenvolvedor.ninja", "DesenvolvedorNinja", null);
            return Jose.JWT.Encode(token, Encoding.UTF8.GetBytes("abcdefghijklmnopqrs"), Jose.JwsAlgorithm.HS256);
        }
    }
}

Em nosso controller, declaramos apenas um método, do tipo POST, que será o responsável por gerar o token de autorização e devolver à aplicação cliente que está consumindo nossa API. Na primeira linha apenas criamos uma instância de nossa classe AuthorizationToken, e na segunda, utilizamos o a classe JWT do pacote JOSE para criarmos o token, para isso passamos como parâmetros nossa classe que vai representar o token, depois passamos qual será o secret (chave secreta) e por fim, qual algoritmo de assinatura será utilizado, e em nosso caso escolhemos o HS256.

Agora que já criamos e devolvemos o token, precisamos adicionar o middleware de autenticação que baixamos via Nuget no começo, para que nossa aplicação valide sese o token recebedo é válido para nós. Para isso, abra a classe Startup, e crie o método abaixo:

private TokenValidationParameters GetTokenValidationParameters()
{
	return new TokenValidationParameters
	{
		ValidateIssuerSigningKey = true,
		RequireSignedTokens = true,
		IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdefghijklmnopqrs")),

		ValidateIssuer = false,
		ValidIssuer = "http://desenvolvedor.ninja",

		ValidateAudience = false,
		ValidAudience = "desenvolvedor.ninja",

		ValidateLifetime = false,
		RequireExpirationTime = false,
		ClockSkew = new TimeSpan(0, 5, 0),

		ValidateActor = false
	};
}

Nesse método apenas criamos e retornamos um objeto do tipo TokenValidationParameters, que será utilizado pelo middleware para validação do token recebido, ele irá confrontar os valores contidos no token, com os informados no objeto do tipo TokenValidationParameters que acamos de criar.

Agora, nós vamos alterar o método Configure, também da classe Startup, nós vamos adicionar o seguinte código nele:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
	AutomaticAuthenticate = true,
	TokenValidationParameters = GetTokenValidationParameters(),
});

O que acabamos de fazer foi incluir no pipeline de nossa aplicação o middleware para validação JWT, com os parâmetros que definimos no método criado anteriormente. O conteúdo completo do método configure ficou desse jeito:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	loggerFactory.AddConsole(Configuration.GetSection("Logging"));
	loggerFactory.AddDebug();

	app.UseJwtBearerAuthentication(new JwtBearerOptions
	{
		AutomaticAuthenticate = true,
		TokenValidationParameters = GetTokenValidationParameters(),
	});

	app.UseMvc();
}

Pronto, agora nossa aplicação está pronta para validar o token recebido em cada request que receber, e nós podemos trabalhar de duas formas:

  • todos métodos de nossos controllers são abertos e podem ser acessados sem utilização de um token, a não ser que explicitamente nós implementemos o contrário.
  • todos métodos de nossos controllers são fechados e exigem que um token seja informado, a não ser que nós explicitamente digamos o contrário.

Como boa prática, o recomendado é que nossa aplicação seja fechada, e a gente informe o que possa ser acessado de forma pública sem token. E é isso que vamos fazer agora, informar nossa aplicação que ela sempre exigirá um token, a não ser que informemos o contrário em um método de um controller. Para fazermos isso, vamos alterar o método ConfigureServices da classe Startup, deixando ele da seguinte forma:

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc(config =>
	{
		var policy = new AuthorizationPolicyBuilder()
						 .RequireAuthenticatedUser()
						 .Build();
		config.Filters.Add(new AuthorizeFilter(policy));
	});
}

O que fizemos acima foi criar uma nova policy, que exige um usuário autenticado, e também criamos um novo filtro, em que adicionamos nossa política, com isso, sempre que receber um request, nossa aplicação irá validar se recebeu um token, e se esse token contém a role User definida, caso não tenha, ela recusará o request.

Agora vamos implementar um novo controller, com o nome NinjasController, e nele vamos criar o código abaixo:

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace JoseJwtSample.Controllers
{
    [Route("api/[controller]")]
    public class NinjasController : Controller
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "Genin", "Chuunin", "Jonin" };
        }

        [HttpGet("{level}")]
        public string Get(string level)
        {
            switch (level)
            {
                case "Genin": return "Shinobi Baixo";
                case "Chuunin": return "Shinobi Intermediário";
                case "Jonin": return "Shinobi Alto";
                default: return "Nível não encontrado";
            }
        }
    }
}

Em nosso controller criamos dois métodos, um que retorna um lista de níveis da hierarquia ninja, e outro que recebe uma hierarquia e devolve sua descrição. Agora vamos rodar nossa aplicação e testá-la? Para ajudar em nosso teste nós vamos utilizar o ARC (Advanced Rest Client, uma extensão para o Chrome), mas você pode utilizar outra aplicação como o Postman ou outra de sua prefêrencia que permita a realização de GET e POST passando um token no cabeçalho. Primeiro, rode nossa aplicação apertando F5 no Visual Studio, e abra o ARC.

O primeiro teste que vamos fazer é chamar o método GET para recebermos a lista de níveis da hierarquia ninja, para isso faça uma requisição GET para o endereço abaixo (lembre de substituir a porta 55576 pela porta utilizada em sua máquina):

http://localhost:55576/api/ninjas

Veja que recebemos como resposta o código abaixo:

401 Unauthorized

Como não informamos nenhum token, nossa aplicação não permitiu nosso acesso, então agora vamos pegar o token, para isso faça uma requisição POST no endereço abaixo:

http://localhost:55576/api/token

Veja que recebemos como resposta um token, com o conteúdo abaixo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3MiOiJodHRwOi8vZGVzZW52b2x2ZWRvci5uaW5qYSIsIkF1ZCI6ImRlc2Vudm9sdmVkb3IubmluamEiLCJTdWIiOiJEZXNlbnZvbHZlZG9yTmluamEiLCJSb2xlcyI6WyJVc2VyIl19.4VsB2YtVv_aHG-c2aSOTtlpdlCMwzokMGtfs6eaeYOo

Agora vamos chamar novamente o método para receber os níveis de hierarquia dos ninjas, mas dessa vez vamos utilizar o nosso token no cabeçalho da requisição, então usando o ARC coloque novamente a URL abaixo:

http://localhost:55576/api/ninjas

E adicione o valor abaixo ao cabeçalho, mas é importante colocar exatamente dessa forma:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3MiOiJodHRwOi8vZGVzZW52b2x2ZWRvci5uaW5qYSIsIkF1ZCI6ImRlc2Vudm9sdmVkb3IubmluamEiLCJTdWIiOiJEZXNlbnZvbHZlZG9yTmluamEiLCJSb2xlcyI6WyJVc2VyIl19.4VsB2YtVv_aHG-c2aSOTtlpdlCMwzokMGtfs6eaeYOo

O cabeçalho é composto por parâmetros do tipo chave-valor, sendo que a chave é Authorization, e o valor é a palavra Bearer seguida do nosso token. Faça o request e no lugar do 401, você agora deve receber um 200 com o conteúdo abaixo:

[
  "Genin",
  "Chuunin",
  "Jonin"
]

Agora vamos fazer um request para a URL que devolve a descrição do nível da hierarquia ninja, para isso faça um request do tipo GET para a URL abaixo:

http://localhost:55576/api/ninjas/Jonin

E não se esqueça de passar o token no cabeçalho, da mesma forma que fizemos acima:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3MiOiJodHRwOi8vZGVzZW52b2x2ZWRvci5uaW5qYSIsIkF1ZCI6ImRlc2Vudm9sdmVkb3IubmluamEiLCJTdWIiOiJEZXNlbnZvbHZlZG9yTmluamEiLCJSb2xlcyI6WyJVc2VyIl19.4VsB2YtVv_aHG-c2aSOTtlpdlCMwzokMGtfs6eaeYOo

O resultado do request será:

Shinobi Alto

E pronto, com pouco código, já temos uma aplicação asp.net core que utiliza JWT para autenticação e segurança.

O código completo do nosso exemplo está disponível em meu GitHub: https://github.com/rdakar/jose-jwt-sample

Para mais informações sobre o pacote JOSE que utilizamos em nosso exemplo, visite sua página oficial no GitHub: https://github.com/dvsekhvalnov/jose-jwt

Facebooktwittergoogle_plusredditpinterestlinkedinmail

3 thoughts on “Utilizando tokens para segurança no asp.net core com JOSE e JWT

  1. Oi, eu ainda estou recebendo 401 Unauthorized no GET /ninjas

    Tentei pegar o seu codigo do github mas tbm da a mesma coisa. Esta faltando algo ? Alguma configuração talvez? Tentei mudar o Issuer para o localhost, mas tbm nao dá certo.

    1. Descobri o que era, eu tinha mudado o nome da Role pra “Player”, pra se encaixar no meu modelo.
      Como eu faria se quisesse adicionar mais roles (admin, guest, etc) ? No startup.cs vc adicionou a politica RequireAuthenticatedUser(), é por isso que nao pude mudar pra “Player” ? Como eu faria se quisesse usar outra palavra sem ser “User”?
      Você poderia demonstrar também como retornar um tempo de expiração do token ?

      Obrigado pelo tutorial!

      1. Olá David,

        Da forma que criamos a policy, precisamos utilizar a role “user”, é ela quem vai ser procurada no token para validação pelo middleware. Nós podemos trabalhar com roles diferentes, e eu vou fazer em breve um post sobre o assunto. Pois é possível validar outras roles, ou criar novas policies com validações customizadas. Uma das formas para trabalhar com a role “player” é você utilizar o atributo [Authorize] nos seus métodos dos controllers, por exemplo: [Authorize(Roles = “Player”)].

        Com relação ao tempo de expiração do token, também vou fazer um post sobre isso, mas basta que seu token tenha o campo “exp” preenchido no formato NumericDate, que representa a quantidade de segundos desde 01/01/1970, por exemplo: “exp”: 1371720939. E na classe TokenValidationParameters, você trocar o valor da propriedade ValidateLifetime para “true”.

        Abraços

Deixe uma resposta

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