ASP .NET Core -  Usando o IdentityServer4


Neste artigo veremos como usar o IdentityServer4 com a ASP .NET Core.

Eu já apresentei o IdentityServer4 neste artigo : ASP .NET Core - Apresentando o IdentityServer4

E hoje vou mostrar como usar o IdentityServer4(IS4) em uma aplicação ASP .NET Core Web API mas antes vou fazer um apanhado geral dos conceitos e responsabilidades relacionados com o IS4.

O que é o IdentityServer4 ?

O IdentityServer4 (IS4) é a versão mais recente do IdentityServer que é um framework OpenID Connect e OAuth 2.0 gratuito que podemos usar com a ASP.NET Core.  A ideia principal ao usar o IS4 é centralizar o provedor de autenticação.

Assim vamos supor que você tenha 3 APIs ou microservicos que precisam da segurança da autenticação. Neste cenário você não precisa ter que definir a lógica de autenticação em cada aplicativo. Em vez disso, usando o IS4 você consegue centralizar o Controle de Acesso para que cada API/microserviço seja protegida pelo IdentityServer de forma centralizada.

Outro recurso existente é quando um cliente (aplicativo Web) deseja acessar uma API protegida, o IdentityServer4 gera e valida tokens de acesso para tornar isso possível.

Como o Identity Server funciona ?

É bem simples :

- Os usuários usam os 'clientes', como uma aplicação ASP .NET Core MVC, para acessar os dados;
- Os usuários serão autenticados pelo
IdentityServer para usar o cliente;
- Depois que os usuários são autenticados para usar o Cliente, o cliente envia uma solicitação ao Recurso da API;
- Tanto o Cliente quanto os Recursos da API são protegidos por uma única entidade, o IdentityServer;
- O cliente pode solicitar um token de acesso com o qual ele pode acessar as respostas da API;
- O IS4 também pode ser para emitir os tokens de segurança para os clientes;

Desta forma, estamos centralizando o Mecanismo de Autenticação em um único servidor.

Quais as responsabilidades do IdentityServer4 ?

O Identity Server é uma solução de segurança completa para seus projetos. Aqui estão seus principais recursos e responsabilidades :

Como começar com o IdentityServer4 ?

Existem diversas maneiras de iniciar projetos com o IdentityServer4.

A mais fácil é rápida é usar templates prontos onde você instala os templates IdentityServer4 usando a ferramenta de linha de comando - CLI - e escolhe um template para criar de forma automática um projeto.

Vou mostrar os comandos que podemos usar para instalar os templates e criar um projeto com o IS4 usando a CLI :

dotnet new -i IdentityServer4.Templates

md testeis4
cd testeis4

md src
cd src


dotnet new is4empty -n IdentityServer
dotnet new is4ui -n IdentityServer


cd ..

dotnet new sln -n TesteIS4

dotnet sln add .\src\IdentityServer\IdentityServer.csproj

O primeiro comando : dotnet new -i IdentityServer4.Templates

Vai restaurar e instalar os templates que podemos usar para criar projeto com o IdentityServer4.

Após executar este comando emita o comando dotnet new para ver os novos templates instalados:

A seguir vamos criar a pasta testeis4 e dentro desta pasta vamos criar a pasta src e nesta pasta usar os templates is4empty e is4ui  com o comando dotnet new que vai criar um projeto padrão básico com o IdentityServer4 :



Ao final podemos entrar na pasta do projeto e visualizar a sua estrutura :

Para criar um projeto com uma implementação do IS4 em um projeto ASP .NET Core usando configurações e Usuário na memória basta executar o seguinte comando :   dotnet new is4inmem

Essa seria a maneira mais direta e fácil de criar um projeto com o IS4 integrado mas neste artigo eu não vou usar essa abordagem pois ela oculta grande parte da complexidade e você acaba não sabendo o que realmente acontece nos bastidores.

Vamos implementar o servidor a partir do zero e, assim você fica familiarizado com seu funcionamento, e estará pronto para usar esses templates com mais conhecimento.

Criando projetos e usando o IdentityServer4

Nesta etapa vamos iniciar a integração do IdentityServer com a ASP .NET Core realizando as seguintes tarefas:

  1. Criar um projeto Host com o IdentityServer4 com usuários e armazenamento em memória;
  2. Criar um projeto ASP .NET Core Web API, que é o recurso que vamos proteger;
  3. Criar um projeto Web que irá consumir a API;

Vamos usar o ambiente do .NET 5.0 e o Visual Studio 2019 Community versão 16.8.3.

Abra o VS 2019 Community e crie uma solução em branco usando o template Blank Solution chamada:  AspnIdentityServer4

A seguir vamos incluir um novo projeto ASP .NET Core acionando o menu File-> Add new Project e usando o template ASP.NET Core Empty e informando o nome IdentityServer

Estamos criando um projeto vazio e vamos agora instalar o pacote do IdentityServer4 neste projeto abrindo a janela do Package Manager Console e digitando o comando : Install-Package IdentityServer4

Ao final podemos abrir a janela Solution Explorer e confirmar a inclusão dos pacotes no projeto :

Agora vamos iniciar a configuração do IdentityServer4 que neste artigo esta sendo feita para fins de demonstração.

Vamos criar a classe IdentityConfiguration.cs na raiz do projeto e definir o código abaixo:

using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;
 public class IdentityConfiguration
 {
        public static List<TestUser> TestUsers =>
            new List<TestUser>
            {
                new TestUser
                {
                     SubjectId = "1144",
                     Username = "macoratti",
                     Password = "numsey",
                     Claims =
                     {
                        new Claim(JwtClaimTypes.Name, "Macoratti Net"),
                        new Claim(JwtClaimTypes.GivenName, "Macoratti"),
                        new Claim(JwtClaimTypes.FamilyName, "Net"),
                        new Claim(JwtClaimTypes.WebSite, "http://macoratti.net"),
                     }
              }
        };
        public static IEnumerable<IdentityResource> IdentityResources =>
            new IdentityResource[]
            {
               new IdentityResources.OpenId(),
               new IdentityResources.Profile(),
            };
        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("myApi.read"),
                new ApiScope("myApi.write"),
            };

        public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {
               new ApiResource("myApi")
               {
                   Scopes = new List<string>{ "myApi.read","myApi.write" },
                   ApiSecrets = new List<Secret>{ new Secret("supersecret".Sha256()) }
               }
            };
        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                new Client
                {
                    ClientId = "cwm.client",
                    ClientName = "Client Credentials Client",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    ClientSecrets = { new Secret("secret".Sha256()) },
                    AllowedScopes = { "myApi.read" }
                },
            };
    }
}

Vamos entender o código criado:

1- Definimos um usuário de teste onde os dados estão definidos no código para simplificar o exemplo:

public static List<TestUser> TestUsers =>
            new List<TestUser>
            {
                new TestUser
                {
                     SubjectId = "1144",
                     Username = "macoratti",
                     Password = "numsey",
                     Claims =
                     {
                        new Claim(JwtClaimTypes.Name, "Macoratti Net"),
                        new Claim(JwtClaimTypes.GivenName, "Macoratti"),
                        new Claim(JwtClaimTypes.FamilyName, "Net"),
                        new Claim(JwtClaimTypes.WebSite, "http://macoratti.net"),
                     }
              }
        };

No código atribuímos os valores para nome e senha e definimos algumas claims, e, assim teremos como retorno um TestUser com algumas declarações Json Web Tokens (JWT) definidas previamente.

2- A seguir definimos o Recurso usado.

Os recursos de identidade são dados como userId, email, no. de telefone que são dados exclusivos para uma identidade do usuário.

No código incluímos o OpenId e os Recursos de Perfil ou Profile :

 public static IEnumerable<IdentityResource> IdentityResources =>
            new IdentityResource[]
            {
                 new IdentityResources.OpenId(),
                 new IdentityResources.Profile(),
            };

NotaO OpenID permite que você use uma conta existente para fazer login em vários sites, sem a necessidade de criar novas senhas.

3- A seguir definimos o escopo da API.

Como nossa intenção é proteger uma API, logo esta API pode ter escopos que neste contexto significa o que o usuário autorizado pode fazer.

Nota: Os escopos representam o que um aplicativo cliente pode fazer e são normalmente modelados como recursos, que vêm em dois tipos: Identidade e API.

No nosso  exemplo vamos tratar com 2 escopos por enquanto - leitura, gravação. (Read, Write) e também vamos nomear nossa API como myAPI.

  public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("myApi.read"),
                new ApiScope("myApi.write"),
            };

4- A seguir definimos os recursos da API

Neste código definimos a própria API onde damos a ela o nome de myApi e definimos os  escopos suportados junto com o secret, e, aqui , você tem que aplicar um hash neste secret, visto que este código hash será salvo internamente no IdentityServer.

  public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {
               new ApiResource("myApi")
               {
                   Scopes = new List<string>{ "myApi.read","myApi.write" },
                   ApiSecrets = new List<Secret>{ new Secret("supersecret".Sha256()) }
               }
            };

5- Definindo os usuários e a forma de interação

Finalmente, temos que definir quem terá acesso ao nosso recurso protegido, que em nosso caso é myApi.  Para isso vamos fornecer um nome de cliente e uma identificação.

No código estamos definindo GrantType como ClientCredentials sinalizando como o cliente pode interagir com o serviço de token.

 public static IEnumerable<Client> Clients =>
            new Client[]
            {
                new Client
                {
                    ClientId = "cwm.client",
                    ClientName = "Client Credentials Client",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    ClientSecrets = { new Secret("secret".Sha256()) },
                    AllowedScopes = { "myApi.read" }
                },
            };

Registrando o IdentityServer4 na ASP .NET Core

Vamos agora registrar o IdentityServer4 no  contêiner DI da ASP.NET Core definindo no método ConfigureServives da classe Startup.cs o código abaixo onde estamos usando todos os recursos estáticos, clientes e usuários que definimos em nossa classe IdentityConfiguration acima:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace IdentityServer
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddInMemoryClients(IdentityConfiguration.Clients)
                .AddInMemoryIdentityResources(IdentityConfiguration.IdentityResources)
                .AddInMemoryApiResources(IdentityConfiguration.ApiResources)
                .AddInMemoryApiScopes(IdentityConfiguration.ApiScopes)
                .AddTestUsers(IdentityConfiguration.TestUsers)
                .AddDeveloperSigningCredential();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

Definindo armazenamento de configuração na memória e credenciais de assinatura

Como estamos definindo no código as configurações do IdentityServer temos que configurar alguns armazenamento na memória.

Essas configurações são codificadas no projeto HOST e são carregadas apenas uma vez quando o aplicativo é inicializado. Isso é usado principalmente para as fases de desenvolvimento e prototipagem. Esta abordagem também pode ser válida para cenários de produção se a configuração raramente mudar com o tempo.

Outro detalhe é que o IdentityServer precisa de certificados para verificar seu uso. Mas para fins de desenvolvimento e uma vez que não temos nenhum certificado conosco, vamos usar a extensão AddDeveloperSigningCredential().

Nota: Essa extensão cria uma chave temporária no momento da inicialização. A chave gerada será mantida no sistema de arquivos para que permaneça estável entre as reinicializações do servidor (pode ser desativada passando false). Isso funciona e resolve problemas quando os caches de metadados cliente/API ficam fora de sincronia durante o desenvolvimento.

Finalmente no método Configure vamos incluir a linha de código destacada para incluir o middleware do IdentityServer:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
 }

Concluindo esta etapa podemos executar o projeto.

Para isso altere o servidor de IIS Express para o servidor Kestrel no menu do Visual Studio:

Executando o projeto teremos o resultado abaixo  onde vemos o servidor Kestrel inicializar e o navegador abrir exibir a mensagem "Hello World"

Nota: Você também pode rodar no IIS Express. Neste caso a porta vai ser diferente. No meu ambiente a porta é localhost:44306.

Se você chegou até aqui e conseguiu acompanhar e realizar todas as etapas sem erros , parabéns.

Na próxima parte do artigo vamos continuar analisando a documentação do OpenID.

Pegue o projeto aqui:  AspnIdentityServer4.zip

"Porque não nos pregamos a nós mesmos, mas a Cristo Jesus, o Senhor; e nós mesmos somos vossos servos por amor de Jesus.
Porque Deus, que disse que das trevas resplandecesse a luz, é quem resplandeceu em nossos corações, para iluminação do conhecimento da glória de Deus, na face de Jesus Cristo."
2 Coríntios 4:5,6

Referências:


José Carlos Macoratti