JWT: Customizando o Identity no ASP.NET CORE 3.0

Esse artigo foi escrito a fim de disponibilizar uma solução genérica e completa interagindo com Identity do ASP.NET CORE 3.0 usando autenticação JWT (Json Web Token) SEM precisar de toda aquela papagaiada do Entity Framework, DbContext e tabelas do SQL Server.

JWT: Customizando o Identity no ASP.NET CORE 3.0

O que iremos fazer é algo flexível e genérico que possibilite usufruir autenticação e autorização do ASP.NET CORE mas sem usar a todos os métodos pré-existentes.

Essa é para você que já tem o seu próprio cadastro de usuários (banco de dados ou serviço) e quer usar o Identity do ASP.NET CORE.

JWT – Json Web Token: Basicamente é um padrão seguro para troca de dados assinado digitalmente entre duas aplicações.

Algumas informações importantes para quem está chegando agora. Se você é um desenvolvedor .NET mas nunca viu nada de .NET Core, sugiro você ler antes o artigo .NET Core para DesenvolvedoreS .NET.

JWT: Customizando o Identity no ASP.NET CORE 3.0

Dica: Não é necessário para esse artigo, mas se você não sabe como configurar o .NET Core no IIS, veja IIS: Como Hospedar Aplicação .NET Core em 10 Passos.

Se você tem preferência em usar MAC ou Linux, também vai conseguir acompanhar, já que toda a solução é em .NET Core.

A partir desse ponto, a leitura do artigo é de 10 minutos, e sua aplicação leva 10 minutos.

# JWT – O que faremos?

1 – API ASP.NET Core 3.0 C#
Criação de uma API em .NET Core 3.0 na linguagem de programação C# com objetivo de interagir com regras de negócio e retornar informações do Identity.

2 – Endpoint público
Criação de um endpoint público para LOGIN, ou seja, a partir de login/senha, autenticar o usuário gerando um JWT.

3 – Endpoint privado
– Criação de um endpoint privado, que só funcionará recebendo um JWT válido, ou seja, só funcionará com o usuário autenticado.

4 – Classes e Interfaces
Criação das interfaces e classes genéricas para compor a solução completa.

5 – NuGet
Instalação de pacotes NuGet correspondentes ao JWT e outros.

6 – Configuração do Identity
Configuração do Identity para usar essa solução customizada.

Os Design Patterns usados nesse artigo podem ser encontrados no meu eBook Design Patterns Vol. 1 Programação no Mundo Real.


# JWT – Workflow

Por experiência, eu prefiro começar de trás para frente, ou seja, primeiro criar o endpoint público de Login para autenticar um usuário.

Pois, se começarmos a partir da configuração do Identity vai dar um nó na cabeça.

O Workflow é o seguinte:

JWT: Customizando o Identity no ASP.NET CORE 3.0

Os sufixos Service e Configuration são apenas para padronizar nomenclatura, mas você poderia chamá-los do jeito que quiser.

A partir de uma Action POST, o endpoint LoginController irá receber login/senha e enviá-los ao método Authorize de IAuthorizationService.

Se a autorização do usuário/senha for realizada com sucesso, LoginController acionará o método Authenticate de IAuthenticationService gerando um Token JWT conforme código abaixo.

{
    "authenticated": true,
    "created": "2019-11-25 09:38:03",
    "expiration": "2019-11-25 09:48:03",
    "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6WyI1MCIsIjUwIl0sImp0aSI6IjIxN2M5ODQ2ODMxODQzYjVhNjc0ZDI3ZjY3NDhlOTNlIiwiTmFtZSI6IlBhdWxvIE1ldnMiLCJJZCI6IjUwIiwiQ3JlZGVuY2lhaXMiOiJbXCIyMDNcIixcIkJDX01FVV9MXCIsXCJCQ19QXCIsXCJCQ19QTEFcIixcIkJDX1JFTFwiLFwiQkNfVElNXCIsXCJCQ19USU1fRVwiLFwiQ19IT1JBU1wiXSIsIm5iZiI6MTU3NDY4NTQ4MywiZXhwIjoxNTc0Njg2MDgzLCJpYXQiOjE1NzQ2ODU0ODMsImlzcyI6IkJyb2tlckdlc3RhbyIsImF1ZCI6IkJyb2tlckdlc3RhbyJ9.J8KIh0g7R3nnSug-cgQJDQCzeLuKPGgDAXFvW-0O-LdI4MoLx19fLClvs-q7jmxOw7w3S-x__e19caSptZ1QP1KoN2TIlhonea1emOUJ3yEk7Ri0COSPlV07Y0Sjgq3E85N_KmYVF8-3vr8-sw_aGYFUWhXadjo9p_KnAqo6QlkhFfDFlhoQhDLk_7MFXqubMDaRKFgULnr-_T5W3eS3269ZPT_0qC2kiixr-J_MP-EyIbuvd1lhtG8ns5qmxyck_GUvRy_KmSA5X2PHfEur7FBABYubTPXZRlb727Fz-NLmbBTVBAU0ootlCBIefnQ3pBxCUqEgwvwDGMGncJpadw",
    "message": "OK"
}


# JWT – Criando a API ASP.NET CORE 3.0

Para criar uma aplicação API ASP.NET CORE 3.0 no Visual Studio 2019, vá em File, New, Project.

Na janela que será aberta, escolha ASP.NET Core Web Application e clique em Next.

Coloque FSL.ApiCustomIdentity no nome do projeto e clique em Create.

Agora, escolha ASP.NET Core 3.0, template API e desmarque a opção HTTPS.

Por fim, deixe sem autenticação e clique em Create.

Se você precisar baixar a versão ASP.NET CORE 3.0, visite o site asp.et

Após a criação da API em ASP.NET CORE 3.0, aperte F5 para executar a aplicação.

Dessa forma será exibido o resultado de uma API demonstração.


# JWT – Criando LoginController e interfaces

Conforme desenho de arquitetura e workflow, o LoginController ficou assim:

    [Route("api/login")]
    [ApiController]
    public sealed class LoginController : ControllerBase
    {
        private readonly IAuthenticationService _authenticationService;
        private readonly Service.IAuthorizationService _authorizationService;

        public LoginController(
            IAuthenticationService authenticationService,
            Service.IAuthorizationService authorizationService)
        {
            _authenticationService = authenticationService;
            _authorizationService = authorizationService;
        }

        [AllowAnonymous]
        [HttpPost]
        public async Task<IActionResult> PostAsync(
            [FromBody] LoginUser loginUser)
        {
            var authorization = await _authorizationService.AuthorizeAsync(loginUser);
            if (!authorization.Success)
            {
                return Ok(authorization);
            }

            var authentication = await _authenticationService.AuthenticateAsync(authorization.Data);
            if (!authentication.Success)
            {
                return Ok(authentication);
            }

            return Ok(authentication);
        }
    }

As instâncias que implementam as interfaces IAuthenticationService e IAuthorizationService serão injetadas no construtor de LoginController. Esse Design Pattern é chamado de Dependency Injection.

Para que tudo isso funcione, ainda é necessário criar uma classe para cada uma dessas interfaces e fazer uma configuração no arquivo Startup.cs da aplicação web.

As classes e interfaces usadas no LoginController são:

    public sealed class LoginUser
    {
        public string LoginOrEmail { get; set; }
        public string Password { get; set; }
    }
    public interface IAuthorizationService
    {
        Task<BaseResult<IUser>> AuthorizeAsync(
            LoginUser loginUser);
    }
    public interface IAuthenticationService
    {
        Task<AuthenticationResult> AuthenticateAsync(
            IUser user);
    }
    public interface IUser
    {
        string Id { get; set; }
        string Name { get; set; }
    }
    public class BaseResult<T>
    {
        public string Message { get; set; }
        public bool Success { get; set; }
        public T Data { get; set; }
    }
    public sealed class AuthenticationResult : BaseResult<object>
    {
        public bool Authenticated { get; set; }
        public string Created { get; set; }
        public string Expiration { get; set; }
        public string AccessToken { get; set; }
    }

Todas essas interfaces e classes são genéricas, assim poderiam estar em uma DLL separada para serem reutilizadas em outros projetos.


# JWT – Criando as classes Fake

No item anterior desenvolvemos toda a lógica do LoginController, usando as interfaces sem precisar ter as classes que implementam essas interfaces. Esse conceito é chamado de Inversion of Control.

Mas para testar o endpoint de LoginController, precisamos das classes que implementam IAuthenticationService e IAuthorizationService.

Vamos inicialmente criar classes “Fake” para simular a entrada/saída de dados.

    public sealed class FakeAuthorizationService : IAuthorizationService
    {
        public async Task<BaseResult<IUser>> AuthorizeAsync(
            LoginUser loginUser)
        {
            var loginOrEmail = loginUser?.LoginOrEmail ?? "";
            var password = loginUser?.Password ?? "";

            var result = new BaseResult<IUser>();

            if (loginOrEmail == "fsl" && password == "1234")
            {
                result.Success = true;
                result.Message = "User authorized!";
                result.Data = new MyLoggedUser
                {
                    Id = Guid.NewGuid().ToString(),
                    Name = "Name test",
                    Credentials = "01|02|09",
                    IsAdmin = false
                };
            }
            else
            {
                result.Success = false;
                result.Message = "Not authorized!";
            }

            return await Task.FromResult(result);
        }
    }
    public sealed class FakeAuthenticationService : IAuthenticationService
    {
        public async Task<AuthenticationResult> AuthenticateAsync(
            IUser user)
        {
            var dateFormat = "yyyy-MM-dd HH:mm:ss";
            var result = new AuthenticationResult()
            {
                Success = true,
                Authenticated = true,
                Created = DateTime.UtcNow.ToString(dateFormat),
                Expiration = DateTime.UtcNow.AddHours(2).ToString(dateFormat),
                Message = "OK",
                AccessToken = "2349urf99de99423r99ifr2"
            };

            return await Task.FromResult(result);
        }
    }

Por fim, com as classes e intefaces de autorização e autenticação criadas, precisamos configurar o Dependency Injection do ASP.NET CORE através do arquivo Startup.cs.

        public void ConfigureServices(
            IServiceCollection services)
        {
            services.AddSingleton<IAuthenticationService, FakeAuthenticationService>();
            services.AddSingleton<IAuthorizationService, FakeAuthorizationService>();
            services.AddControllers();
        }

A partir desse ponto já é possível testar a aplicação apertando F5.

Use a ferramenta Postman para fazer o POST no endpoint LoginController conforme configuração e Request Body abaixo:

{
     "loginOrEmail": "",
     "password": ""
}

JWT: Uma Solução para ASP.NET CORE Identity 3.0

Agora, mude o Request Body no Postman para o loginOrEmail “fsl” e password “1234” e clique em Send novamente.

Agora teremos um response dizendo que o usuário está autenticado.

{
    "authenticated": true,
    "created": "2019-11-25 21:11:55",
    "expiration": "2019-11-25 23:11:55",
    "accessToken": "2349urf99de99423r99ifr2",
    "message": "OK",
    "success": true,
    "data": null
}

Obs.: O valor da propriedade accessToken é apenas um exemplo, não é um JWT válido.

Testado o workflow do LoginController com classes Fake, agora precisamos criar a classe real que irá gerar um token JWT válido.


# JWT – Criando JWT Service

Antes de tudo é necessário instalar o pacote NuGet Microsoft.AspNetCore.Authentication.JwtBearer.

A primeira versão da classe de autenticação que irá gerar um JWT JSON Web Token é:

    public sealed class JwtIdentityAuthenticationService : IAuthenticationService
    {
        private readonly SigningConfiguration _signingConfiguration;

        public JwtIdentityAuthenticationService(
            SigningConfiguration signingConfiguration)
        {
            _signingConfiguration = signingConfiguration;
        }

        public AuthenticationResult Authenticate(
            IUser user)
        {
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
                new Claim(JwtRegisteredClaimNames.UniqueName, user.Id),
                new Claim("Data", ToJson(user))
            };

            var identity = new ClaimsIdentity(
                new GenericIdentity(user.Id, "Login"),
                claims);

            var created = DateTime.UtcNow;
            var expiration = created + TimeSpan.FromSeconds(60000);
            var handler = new JwtSecurityTokenHandler();
            var securityToken = handler.CreateToken(new SecurityTokenDescriptor
            {
                Issuer = "FSL",
                Audience = "FSL",
                SigningCredentials = _signingConfiguration.SigningCredentials,
                Subject = identity,
                NotBefore = created,
                Expires = expiration
            });

            var dateFormat = "yyyy-MM-dd HH:mm:ss";
            var result = new AuthenticationResult
            {
                Success = true,
                Authenticated = true,
                Created = created.ToString(dateFormat),
                Expiration = expiration.ToString(dateFormat),
                AccessToken = handler.WriteToken(securityToken),
                Message = "OK"
            };

            return result;
        }

        private string ToJson<T>(
            T obj)
        {
            if (obj == null)
            {
                return null;
            }

            return JsonConvert.SerializeObject(obj,
                 new JsonSerializerSettings()
                 {
                     NullValueHandling = NullValueHandling.Ignore
                 });
        }
    }

Você pode reparar que existem alguns Magic Numbers e Magic Strings espalhados no método Authenticate, como quantidade de segundos de expiração do token “60000”, valores de Issuer e Audience “FSL” entre outros. Veremos como melhorar isso mais adiante.

Antes de testar, precisamos informar a Dependency Injection no Startup.cs, trocando a classe FakeAuthenticationService por JwtIdentityAuthenticationService.

       public void ConfigureServices(
            IServiceCollection services)
        {
            var siginingConfiguration = new SigningConfiguration();
            services.AddSingleton(siginingConfiguration);

            services.AddSingleton<IAuthenticationService, JwtIdentityAuthenticationService>();
            services.AddSingleton<IAuthorizationService, FakeAuthorizationService>();
            services.AddControllers();
        }

Pronto, basta executar novamente a aplicação com F5, ir no Postman e clicar em Send.

O resultado será agora um JWT válido.

{
    "authenticated": true,
    "created": "2019-11-25 21:32:33",
    "expiration": "2019-11-26 14:12:33",
    "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6WyI4NDNjZDIwYS1jNjcyLTRhZDYtYTg4Yi05NTVjOTgzYTNkMjQiLCI4NDNjZDIwYS1jNjcyLTRhZDYtYTg4Yi05NTVjOTgzYTNkMjQiXSwianRpIjoiOTEwN2MxYjYxN2VjNDM2YjgwZDA5Mjk3MGVjN2ZlMzgiLCJOYW1lIjoiTmFtZSB0ZXN0IiwiSWQiOiI4NDNjZDIwYS1jNjcyLTRhZDYtYTg4Yi05NTVjOTgzYTNkMjQiLCJuYmYiOjE1NzQ3MTc1NTMsImV4cCI6MTU3NDc3NzU1MywiaWF0IjoxNTc0NzE3NTU0LCJpc3MiOiJGU0wiLCJhdWQiOiJGU0wifQ.GN1gp5kLRcXaiRuQbTlvY9kgJa4wKiMfg36ymbFfmz8jSHJeS5lQz6ojQK2unH_n_kQWckL3Re0c23P2jnWd8G9fKTkJrSQMqGja8b9lbjhuisAi4gf3hOnrXjCE_jcYg17kAwY4NBBo6loU3Iv6XAvR1XJo_2ljg8whNB0g9_dwxHRSqyUbq_bv1lCZFjkMrbth5wz33XEeseXijvxuSP4VFDbVPaJiWm4tNSNWNsUSgLOMkQKwUtQVzhYWzfWDWGsOXgivo4p6xjW4Huh_TVZxLxhfYluTxjfvOvWvrq5Ik-x51j_CkQufzahmGVWsReNSqCH_2in_xBfGsl7hsw",
    "message": "OK",
    "success": false,
    "data": null
}

O próximo passo é configurar o ASP.NET CORE para autorizar endpoints privados usando JWT.


# JWT – Configurando Autorização para JWT

Antes de fazer a configuração no ASP.NET CORE para autorizar endpoint usando JWT, vamos ver como ficar um endpoint privado de exemplo UserController para retornar dados do usuário logado:

    [Route("api/user")]
    [ApiController]
    [Authorize("Bearer")]
    public sealed class UserController : ControllerBase
    {
        private readonly ILoggedUserService _loggedUserService;

        public UserController(
            ILoggedUserService loggedUserService)
        {
            _loggedUserService = loggedUserService;
        }

        [HttpGet]
        public IActionResult Get()
        {
            var user = _loggedUserService.GetLoggedUser<MyLoggedUser>();

            return Ok(user);
        }
    }

Repare que no endpoint, existe um atributo maroto na classe UserController, no caso o Authorize.

É esse atributo que deixa o endpoint privado, e só aceitará JWT do tipo Bearer Token.

Dessa vez não iremos criar uma classe Fake que implemente ILoggedUserService e sim uma classe real, para trazer os dados do usuário logado.

    public sealed class IdentityLoggedUserService : ILoggedUserService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public IdentityLoggedUserService(
            IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public BaseResult<T> GetLoggedUser<T>()
            where T : class, IUser
        {
            var identity = _httpContextAccessor.HttpContext?.User?.Identity as ClaimsIdentity;
            var result = new BaseResult<T>();
            
            if (identity?.IsAuthenticated ?? false)
            {
                result.Data = FromJson<T>(identity?.FindFirst("Data")?.Value);
                result.Success = result.Data != null;
            }
            
            return result;
        }

        private T FromJson<T>(
            string json)
        {
            if (json == null || json.Length == 0)
                return default;

            return JsonConvert.DeserializeObject<T>(
                json, new JsonSerializerSettings()
                {
                    NullValueHandling = NullValueHandling.Ignore
                });
        }
    }

A classe IdentityLoggedUserService simplesmente aciona o Identity, verifica se o usuário está logado, e se estiver traz todas as informações do usuário logado.

Nunca esqueça que é necessário registrar o Dependency Injection no Startup.cs para IHttpContextAccessor e ILoggedUserService.

        public void ConfigureServices(
            IServiceCollection services)
        {
            services.AddHttpContextAccessor(); // here
            services.AddSingleton<IAuthenticationService, JwtIdentityAuthenticationService>();
            services.AddSingleton<IAuthorizationService, FakeAuthorizationService>();
            services.AddSingleton<ILoggedUserService, IdentityLoggedUserService>(); // here
            services.AddControllers();
        }

Por fim, a configuração AddAuthentication e AddAuthorization para aceitar JWT em endpoints que utilizem o atributo [Authorize(“Bearer”)]:

        public void ConfigureServices(
            IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.AddSingleton<IAuthenticationService, JwtIdentityAuthenticationService>();
            services.AddSingleton<Service.IAuthorizationService, FakeAuthorizationService>();
            services.AddSingleton<ILoggedUserService, IdentityLoggedUserService>();
            services.AddControllers();

            services.AddAuthentication(opt => // here
            {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(top =>
            {
                opt.TokenValidationParameters.IssuerSigningKey = siginingConfiguration.Key;
                opt.TokenValidationParameters.ValidAudience = "FSL";
                opt.TokenValidationParameters.ValidIssuer = "FSL";
                opt.TokenValidationParameters.ValidateIssuerSigningKey = true;
                opt.TokenValidationParameters.ValidateLifetime = true;
                opt.TokenValidationParameters.ClockSkew = TimeSpan.Zero;
            });

            services.AddAuthorization(auth => // here
            {
                auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                    .RequireAuthenticatedUser().Build());
            });
        }


# JWT – Testando o endpoint privado

Abra o Postman e acione o endpoint de login novamente.

Copie o valor da tag accessToken.

Ainda em modo debug F5, crie um novo request GET para o endpoint user, informe Authentication Berear colocando o accessToken que você copiou e clique em Send.

JWT: Customizando o Identity no ASP.NET CORE 3.0


# JWT – Melhorias gerais

Os métodos ToJson e FromJson são genéricos e são ótimos exemplos para criarmos Extension Methods.

    public static class StringExtension
    {
        public static T FromJson<T>(
            this string json)
        {
            if (json == null || json.Length == 0)
                return default;

            return JsonConvert.DeserializeObject<T>(
                json, new JsonSerializerSettings()
                {
                    NullValueHandling = NullValueHandling.Ignore
                });
        }

        public static string ToJson<T>(
            this T obj)
        {
            if (obj == null)
            {
                return null;
            }

            return JsonConvert.SerializeObject(obj,
                 new JsonSerializerSettings()
                 {
                     NullValueHandling = NullValueHandling.Ignore
                 });
        }
    }

Após a criação desses Extensions, as linhas de código ficaram conforme abaixo:

    new Claim("Data", user.ToJson())
    identity?.FindFirst("Data")?.Value.FromJson<T>()

Outra coisa que incomoda muito são as Magic Strings e Magic Numbers explícitos no código fonte, como tempo de expiração e outras informações usadas nas classes JwtIdentityAuthenticationService e Startup.cs.

Esse é um caso clássico de deixarmos isso em arquivo de configuração, que no caso do ASP.NET CORE é o arquivo appsettings.json.

Vamos usar os mecanismos já existentes no ASP.NET CORE para ler as parametrizações no arquivo appsettings.json.

O primeiro passo é criar uma classe com cada propriedade que queremos dinamizar, colocar essas informações no arquivo appsettings.json e fazer uma pequena configuração para que possamos lê-las de qualquer lugar.

    public sealed class TokenConfiguration
    {
        public const string Policy = "Bearer";
        public string ValidAudience { get; set; }
        public string ValidIssuer { get; set; }
        public int ExpirationInSeconds { get; set; }
        public bool ValidateLifetime { get; set; }
        public bool ValidateIssuerSigningKey { get; set; }
    }

Próximo passo é dinamizar as informações que estão hardcoded e levá-las ao arquivo appsettings.json:

    {
       "TokenConfiguration": {
          "ValidAudience": "FSL",
          "ValidIssuer": "FSL",
          "ValidateIssuerSigningKey": true,
          "ValidateLifetime": true,
          "ExpirationInSeconds": 60000
      },
      "Logging": {
          "LogLevel": {
              "Default": "Information",
              "Microsoft": "Warning",
              "Microsoft.Hosting.Lifetime": "Information"
          }
      },
      "AllowedHosts": "*"
   }

O penúltimo passo é fazer uma pequena configuração no ASP.NET CORE mapeando a classe TokenConfiguracao com o arquivo appsettings.json.

Arquivo Startup.cs, método ConfigureServices:

var token = new TokenConfiguration();
new ConfigureFromConfigurationOptions<TokenConfiguration>(Configuration.GetSection(typeof(TokenConfiguration).Name)).Configure(token);
services.AddSingleton(token);

O último passo é substituir as Magic Strings e Magic Numbers na classes mencionadas anteriormente.

        public void ConfigureServices(
            IServiceCollection services)
        {
            var token = new TokenConfiguration();
            new ConfigureFromConfigurationOptions<TokenConfiguration>(Configuration.GetSection("TokenConfiguration")).Configure(token);
            services.AddSingleton(token);

            var siginingConfiguration = new SigningConfiguration();
            services.AddSingleton(siginingConfiguration);

            services.AddHttpContextAccessor();
            services.AddSingleton<IAuthenticationService, JwtIdentityAuthenticationService>();
            services.AddSingleton<Service.IAuthorizationService, FakeAuthorizationService>();
            services.AddSingleton<ILoggedUserService, IdentityLoggedUserService>();
            services.AddControllers();

            services.AddAuthentication(opt =>
            {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(top =>
            {
                opt.TokenValidationParameters.IssuerSigningKey = siginingConfiguration.Key;
                opt.TokenValidationParameters.ValidAudience = token.ValidAudience; // dynamic
                opt.TokenValidationParameters.ValidIssuer = token.ValidIssuer; // dynamic
                opt.TokenValidationParameters.ValidateIssuerSigningKey = token.ValidateIssuerSigningKey; // dynamic
                opt.TokenValidationParameters.ValidateLifetime = token.ValidateLifetime; // dynamic
                opt.TokenValidationParameters.ClockSkew = TimeSpan.Zero;
            });

            services.AddAuthorization(auth =>
            {
                auth.AddPolicy(TokenConfiguration.Policy, new AuthorizationPolicyBuilder() // dynamic
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                    .RequireAuthenticatedUser().Build());
            });
        }
    public sealed class JwtIdentityAuthenticationService : IAuthenticationService
    {
        private readonly SigningConfiguration _signingConfiguration;
        private readonly TokenConfiguration _tokenConfiguration;

        public JwtIdentityAuthenticationService(
            SigningConfiguration signingConfiguration,
            TokenConfiguration tokenConfiguration)
        {
            _signingConfiguration = signingConfiguration;
            _tokenConfiguration = tokenConfiguration;
        }

        public AuthenticationResult Authenticate(
            IUser user)
        {
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
                new Claim(JwtRegisteredClaimNames.UniqueName, user.Id),
                new Claim("Data", user.ToJson())
            };

            var identity = new ClaimsIdentity(
                new GenericIdentity(user.Id, "Login"),
                claims);

            var created = DateTime.UtcNow;
            var expiration = created + TimeSpan.FromSeconds(_tokenConfiguration.ExpirationInSeconds); // dynamic
            var handler = new JwtSecurityTokenHandler();
            var securityToken = handler.CreateToken(new SecurityTokenDescriptor
            {
                Issuer = _tokenConfiguration.ValidIssuer, // dynamic
                Audience = _tokenConfiguration.ValidAudience, // dynamic
                SigningCredentials = _signingConfiguration.SigningCredentials,
                Subject = identity,
                NotBefore = created,
                Expires = expiration
            });

            var dateFormat = "yyyy-MM-dd HH:mm:ss";
            var result = new AuthenticationResult
            {
                Success = true,
                Authenticated = true,
                Created = created.ToString(dateFormat),
                Expiration = expiration.ToString(dateFormat),
                AccessToken = handler.WriteToken(securityToken),
                Message = "OK"
            };

            return result;
        }
    }

Para cada endpoint privado precisamos colocar o atributo [Authorize(“Berear”)] na classe ou método. Para não ter que ficar repetindo esse atributo podemos criar uma classe customizada:

    public sealed class MyAuthorizeAttribute : AuthorizeAttribute
    {
        public MyAuthorizeAttribute() : base(TokenConfiguration.Policy)
        {

        }
    }

Então, no endpoint user ficará assim:

    [Route("api/user")]
    [ApiController]
    [MyAuthorize] // here
    public sealed class UserController : ControllerBase


# JWT – Outras configurações

O ideal é que o resultado JSON das API’s não retornem valores nulos, então basta fazer uma simples configuração no Startup.cs.

services
   .AddMvc()
   .SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
   .AddJsonOptions(opt =>
   {
       opt.JsonSerializerOptions.IgnoreNullValues = true;
   });

Talvez sua API tenha que ser consumida a partir de outras aplicações web que estão hospedadas em outros domínios (www), para isso precisamos habilitar o CORS.

Método ConfigureServices em Startup.cs:

services.AddCors();

Método Configure em Startup.cs:

app.UseCors(option => option.AllowAnyOrigin());


# JWT – Considerações finais

Vendo assim o passo a passo pode parecer confuso, mas não é, depois que você passa entender como funciona tudo fica mais simples.

Além do Identity e JWT, nesse artigo foi demonstrado várias técnicas e Design Patterns.

Essa é uma solução para customizar Identity usando JWT no ASP.NET CORE.

Se você conhece outras, compartilhe e comente.

Muito obrigado.

Créditos e materiais de apoio:
Renato Groffe | Maccoratti | Tahir Naushad | Microsoft

Artigos sobre ASP.NET CORE:

Crie seu Framework em ASP.NET CORE 3 e Blazor
.NET Core para Desenvolvedores .NET
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0
Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0


Faça download completo do código fonte no github.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.
Revisado por:
Profissional de TI, com longa experiência na área, tendo trabalho em grande empresas como Itau, CI&T, Linx. Atualmente é CTO de uma empresa de tecnologia.


Apaixonado por tecnologia e sempre disposto a encarar novos desafios, atualmente trabalho focado em aplicações web e mobile com a plataforma .NET, e me aventurando nas diversas linguagens, desafios e experiências que a área nos proporciona.


Paulistano, 21 anos apaixonado por tecnologia, trabalho com desenvolvimento de software atuando com .NET / Xamarin / AngularJS / ASP.NET CORE / MVC.