.NET

26 mai, 2020

ASP.NET Core 3.1 — IdentityServer4 — In-memory (Parte 1)

Publicidade

Você já deve ter ouvido falar sobre o IdentityServer4, talvez usado o SaaS da Auth0, visto a palestra no .NET Conf 2019 que ocorreu em Setembro/2019 ou até mesmo a palestra Identity 101 how username password got so complicated em Outubro/2019 aqui em Florianópolis/SC sobre o assunto. Este assunto é muito vasto, de complexidade média e possui diversos caminhos que podem ser seguidos, diante disso preparamos uma série detalhada que apoia na solução de problemas do dia a dia das empresas no que diz respeito à autenticação de usuários, autorização de API’s e outros assuntos relacionados.

Para começar, vamos ver as configurações básicas do IdentityServer4 funcionando sem banco de dados, com uma classe ou arquivo de configuração e entender as melhores formas de usá-lo, sem deixar de lado o uso do ASP.NET Core 3.1.

SDK

Você precisa fazer o download SDK, para isso clique aqui e escolha a melhor opção conforme o sistema operacional que você está utilizando.

Já nas novidades do .NET Core 3.0 foram anunciadas algumas alterações referente as versões. Sempre que for atualizado, por exemplo da versão x.x.101 para a x.x.102 a 101 será removida, mas mantida caso surja uma versão x.x.200.

Estrutura de pastas

Sugiro que você siga a estrutura de pastas no GitHub, isso vai trazer algumas vantagens mais adiante, principalmente para configuração do Docker (Compose).

Criação do projeto

Para criar o projeto, siga as imagens a seguir:

Na imagem anterior, caso você tenha o Docker instalado, mantenha a opção Enable Docker Support habilitada e no combo deixe na opção Linux, ambas as configurações estão do lado direito na seção Advanced.

Nesta imagem deixe as configurações do Visual Studio como exemplificado, isso vai fazer com que os containers e as imagens não sejam criados na abertura do projeto. Mesmo que sejam ações executadas em background, neste momento apenas vão ocupar espaço.

NuGet Package

Através do NuGet, adicione o pacote IdentityServer4.

Statup.cs

Configure o arquivo Startup.cs conforme abaixo.

using IdentityServer4.Configuration;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Security.Cryptography.X509Certificates;

namespace IdentityProvider
{
    public class Startup
    {
        private readonly IConfiguration _configuration;
        private readonly IWebHostEnvironment _webHostEnvironment;
        public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
        {
            _configuration = configuration;
            _webHostEnvironment = webHostEnvironment;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            var identityServer = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
            });

            identityServer.AddInMemoryIdentityResources(_configuration.GetSection(nameof(IdentityResources)));
            identityServer.AddInMemoryApiResources(_configuration.GetSection(nameof(ApiResource)));
            identityServer.AddInMemoryClients(_configuration.GetSection(nameof(Client)));

            if (_webHostEnvironment.IsDevelopment())
                identityServer.AddDeveloperSigningCredential();

            services.AddAuthentication();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");

                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
                app.UseHttpsRedirection();
            }

            app.UseStaticFiles();

            app.UseRouting();
            app.UseIdentityServer();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }
}

As principais configurações são do IdentityServer para o Startup.cs são:

using IdentityServer4.Configuration;
using IdentityServer4.Models;

var identityServer = services.AddIdentityServer(options =>
 {
 options.Events.RaiseErrorEvents = true;
 options.Events.RaiseInformationEvents = true;
 options.Events.RaiseFailureEvents = true;
 options.Events.RaiseSuccessEvents = true;
 });

identityServer.AddInMemoryIdentityResources(_configuration.GetSection(nameof(IdentityResources)));

 identityServer.AddInMemoryApiResources(_configuration.GetSection(nameof(ApiResource)));

 identityServer.AddInMemoryClients(_configuration.GetSection(nameof(Client)));

if (_webHostEnvironment.IsDevelopment())
 identityServer.AddDeveloperSigningCredential();

services.AddAuthentication();

app.UseIdentityServer();
app.UseAuthorization();

Depois disso, vamos ao que interessa, entender como tudo isso funciona.

In-memory

O IdentityServer não precisa obrigatoriamente de um banco de dados para funcionar, ele pode funcionar apenas em memória, embora esta configuração em ambiente de produção não seja recomendada, para aplicações pequenas eu não vejo nenhum problema, inclusive já trabalhei em projetos de baixa complexidade e que precisavam de autorização para as API’s que a configuração In-memory foi suficiente, até porque dependendo da situação ter mais um banco de dados, toda a questão de segurança envolvida, demoraria mais para gerar uma entrega e desta forma ficaria mais simples.

Uma grande vantagem do IdentityServer é que, mesmo em memória, caso a aplicação seja reiniciada no IIS, ou um container, o token gerado não será invalidado nem perdido, isso se deve ao fato dele expirar em um período específico (padrão) de 3600 segundos e o payload ser gerado através de criptografia RSA de 2048 bits que não muda quando o serviço é reiniciado, veremos mais sobre essa questão na criptografia nos próximo tópico.

SigningCredential

O IdentityServer obrigatoriamente precisa de um certificado digital para funcionar. Em modo Development na primeira execução, você vai perceber que ele gera um arquivo chamado tempkey.rsa onde está a chave de criptografia RSA de 2048 bits e assimétrica, muito melhor que uma chave simétrica.

Para publicação em ambiente de produção (diferente de Developmentobrigatoriamente você vai precisar de um certificado digital (*.pfx) e esse detalhe as vezes é o que atrapalha bastante, então vamos ver na parte 2 como utilizar o OpenSSL para gerar um certificado digital do tipo A1 (*.pfx) que funcionará em ambiente de produção, como configurar e se sua aplicação for hospedada no Azure também vai dar certo.

Resources (recursos)

A primeira ação que devemos tomar ao usar o IdentityServer é definir quais são os recursos que nós queremos proteger, você não precisa pensar em tudo de uma única vez. Podem ser informações do usuário (como dados do perfil), bem como e-mail e até mesmo o acesso às API’s. Na documentação do IdentityServer também tem mais informações.

IdentityResources

No IdentityResources encontraremos informações como ID do usuário, nome, email e a qual empresa ele pertence por exemplo. O IdentityResources possui um nome único que o identifica e você pode associar a ele as Claims, elas serão incluídas no token do usuário. Uma aplicação “cliente” vai utilizar o parâmetro chamado scope para ter acesso ao IdentityResource correspondente através de uma request.
Você pode criar os seus próprios IdentityResources, com as claims que você desejar.
Todos estes detalhes vamos entender melhor nos próximos capítulos.

ApiResources

Para que uma aplicação “cliente” possa realizar uma request para uma API é necessário criar a ApiResources. Para permitir o acesso é necessário que a ApiResources possua uma escopo. O escopo é como se fosse uma permissão para a API, por exemplo, uma determinada API foi criada somente para ler os dados de um Data Warehouse que é alimentado eventualmente pela implementação de CQRS, ou seja, pelo escopo definido essa API não possuirá acesso para escrita. Este exemplo é bem pertinente pois, ele separa a responsabilidade de execução de comandos e realização de queries em um banco de dados.

Para esta configuração já temos uma ApiResources chamado “payment” que vai ser utilizado para configurar uma API que terá a função de realizar o pagamento online, temos isso nas próximas partes do projeto.

Clients

Os Clients representam as aplicações que solicitam um token para o IdentityServer. Esta solicitação de token obrigatoriamente precisa que seja informado um ID e uma senha.
Apenas para adiantar o assunto, existem diversas formas de se configurar um Client, que é chamado de Grant Type, ou seja, o tipo de permissão que permite a solicitação do token por uma determinada aplicação.

appsettings.json

Neste arquivo de configuração, já estão pré-definidas as configurações dos Resources e de um Client e no arquivo Startup.cs as informações são carregadas para o funcionamento In-memory do IdentityServer.
Configure o arquivo appsettings.json conforme a seguir.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "WEBSITE_LOAD_CERTIFICATES": "*",
  "IdentityResources": [
    {
      "Name": "openid",
      "DisplayName": "Your user identifier",
      "Required": true,
      "UserClaims": [
        "sub"
      ]
    },
    {
      "Name": "profile",
      "DisplayName": "User profile",
      "Description": "Your user profile information (first name, last name, etc.)",
      "Emphasize": true,
      "UserClaims": [
        "name",
        "family_name",
        "given_name",
        "middle_name",
        "preferred_username",
        "profile",
        "picture",
        "website",
        "gender",
        "birthdate",
        "zoneinfo",
        "locale",
        "updated_at"
      ]
    }
  ],
  "ApiResource": [
    {
      "Name": "payment",
      "DisplayName": "Payment API",
      "Scopes": [
        {
          "Name": "payment"
        }
      ]
    }
  ],
  "Client": [
    {
      "ClientId": "payment-client",
      "ClientName": "Client Credentials for Payment API",
      "ClientSecrets": [
        {
          "Value": "y2gaG7K9dHgxfMSWh8tZ96Lwd2wYUcwP/5wARxek5c4="
        }
      ],
      "AllowedGrantTypes": [
        "client_credentials"
      ],
      "AllowedScopes": [
        "payment"
      ]
    }
  ]
}

Funcionamento

Através do Postman realize o POST conforme imagem a seguir e verifique se o resultado é parecido com o json que é retornado.

A imagem anterior mostra que o IdentityServer está configurado e funcionando.

A imagem anterior mostra como buscar um token. Este exemplo já simula a busca do token que permite acessar a API payment que iremos criar mais adiante.

Em caso de dúvida você pode baixar o projeto que está do GitHub.

PS: Este projeto está em constante evolução, então pode haver diferenças entre o código do gist e o código real.

Continua em ASP.NET Core 3.1 — IdentityServer4 — Segurança (Parte 2)