Dev (Back & Front)ARTIGO

ASP.NET Core 3.1 — IdentityServer4 — EF-Identity (Parte 5)

Já mostrei como configurar o IdentityServer4 utilizando o EntityFramework e SQL Server como banco de dados. Agora, veremos como funcionar login e senha,

ASP.NET Core 3.1 — IdentityServer4 — EF-Identity (Parte 5)
Imagem: Alex Tochetto
Image for post

Na Parte 4 eu mostrei como configurar o IdentityServer4 utilizando o EntityFramework e SQL Server como banco de dados, porém até agora não vimos nenhum usuário informando o login e senha para se logar em um sistema baseado em MVC (Model View Controller) por exemplo, para isso vamos seguir um caminho simples e você pode evoluir tudo isso depois, pois o foco não está na elaboração da interface, mas como fazer um login e senha funcionar de forma segura e independente.

Identity

Identity nada mais é do que uma API que possui uma interface e permite o usuário realizar o processo de login, além disso o Identity gerencia usuários, senhas, perfis, rolesclaims (vamos ver isso mais adiante), tokens (olha aqui, já falamos sobre ele em posts anteriores), e-mail, confirmação de cadastro de usuários e outras funções. Cada uma delas permite o suporte ao OIDC (Open ID Connect). Caso você queira mais informações, eu sugiro dar uma olhada na documentação da Microsoft.
Vou utilizar o Identity para facilitar a realização do login do usuário.

Scaffold

Nada adianta ter o Identity sem as telas e para facilitar nossa vida, vamos utilizar o Scaffold para criar a estrutura do Identity dentro do nosso projeto, seguindo as instruções a seguir.

Você vai encontrar maiores informações sobre o Scaffold aqui.

Ajustes de código
  • Se você seguiu os passos anteriores, procure pelo arquivo IdentityHostingStartup.cs, ele geralmente fica em Areas > Identity, você pode apagá-lo ou comentar todo o código dele.
  • Localize o arquivo ApplicationDbContext.cs e mova-o para a raiz do projeto na pasta Data, vai ficar mais organizado neste momento por conta da namespace criada.
  • Dentro da pasta Models na raiz do projeto, crie o arquivo ApplicationUser.cs, ele vai gerar a classe com o mesmo nome, e você apenas acrescente a herança da classe IdentityUser.
Statups.cs

Vou fazer algumas considerações sobre este arquivo:
Temos 2 métodos que configuram o IdentityAddIdentity e AddDefaultIdentity:

  • AddDefaultIdentity: Este método já inicializa alguns serviços automaticamente, como o DefaultUI e o DefaultTokenProviders.
  • AddIdentity: Com este método você pode adicionar os serviços necessários conforme sua necessidade.

O restante das configurações você pode conferir no arquivo disponível conforme gist a seguir.

aspnet
using IdentityProvider.Data;
using IdentityProvider.Models;
using IdentityProvider.Seeds;
using IdentityServer4.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Reflection;
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;
            });

            #region Thumbprint
            if (_webHostEnvironment.IsDevelopment())
                identityServer.AddDeveloperSigningCredential();
            else
                identityServer.AddSigningCredential("4DFF9B8EBB5314B9A62EFA72DA8B4D7658231C05", StoreLocation.CurrentUser, NameType.Thumbprint);
            #endregion

            #region Uso do *.pfx direto
            //if (_webHostEnvironment.IsDevelopment())
            //    identityServer.AddDeveloperSigningCredential();
            //else
            //    identityServer.AddSigningCredential(new X509Certificate2("certificate.pfx", "987654321")); 
            #endregion

            #region Forma de uso maqueado
            //if (_webHostEnvironment.IsDevelopment())
            //    identityServer.AddDeveloperSigningCredential();
            //else
            //    identityServer.AddCustomSigningCredential();
            #endregion

            #region Identity configuration
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(_configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>(config =>
            {
                config.SignIn.RequireConfirmedEmail = true;
            })
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultUI()
            .AddDefaultTokenProviders();

            // esta configuração é para o token gerado e enviado para o usuário para recuperar a senha e para confirmar o e-mail do seu cadastro
            // o token é enviado por e-mail para os dois casos
            services.Configure<DataProtectionTokenProviderOptions>(o => o.TokenLifespan = TimeSpan.FromHours(1));
            #endregion

            #region IdentityServer configuration
            var connectionString = _configuration.GetConnectionString("DefaultConnection");
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

            identityServer.AddConfigurationStore(options =>
            {
                options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
                    sqlServerOptionsAction: sqlOptions =>
                    {
                        sqlOptions.MigrationsAssembly(migrationsAssembly);
                        sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
                    });
            })
            .AddOperationalStore(options =>
            {
                options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
                    sqlServerOptionsAction: sqlOptions =>
                    {
                        sqlOptions.MigrationsAssembly(migrationsAssembly);
                        sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
                    });
            })
            .AddAspNetIdentity<ApplicationUser>();

            services.AddAuthentication();
            #endregion
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.RunMigration();
            app.SeeClient();
            app.SeedApiResource();

            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.UseAuthentication(); // UseAuthentication not needed -- UseIdentityServer add this
            app.UseIdentityServer();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
                endpoints.MapRazorPages();
            });
        }
    }
}
Templates

Caso você ache mais prático seguir um template, o IdentityServer possui os seus, um deles já está integrado com o Identity e facilita o entendimento. Por uma questão de aprendizagem, eu vou mostrar o passo a passo.
Para instalar novos templates, eu sugiro você ver o post ASP.NET Core — Como instalar templates — IdentityServer4 e aí utilizar o comando a seguir para criar o projeto a partir do template que está integrado com o Identity.

dotnet new is4aspid -n IdentityServerAspNetIdentity

Migrations

Precisamos das famosas migrations para criar as tabelas necessárias do Identity, para isso basta executar os comandos a seguir dentro do visual studio através do Package Manager Console, você vai precisar que os pacotes abaixo estejam instalados:

Microsoft.EntityFramework Microsoft.EntityFramework.Tools

Add-Migration Initial_Identity -Context ApplicationDbContext

Update-Database -Context ApplicationDbContext

Caso você queira que a própria execução da aplicação crie as tabelas após criar as migrations basta acrescentar a linha a seguir lá no método de extensão “RunMigration”.

serviceScope.ServiceProvider.GetRequiredService().Database.Migrate();

Banco de dados

Até agora eu só configurei uma string de conexão, ou seja, o IdentityServer e o Identity tem suas tabelas criadas em uma única database, isso não vai criar nenhum impedimento no funcionamento, porém caso você queira separar, também pode ser uma boa prática.

Microsserviço (STS + Users)

Estamos chegando a um ponto onde podemos começar a falar sobre microsserviços e o Identity é encarado como um. Tudo bem, você pode se perguntar “o microsserviço não deveria fazer uma única coisa?”, a resposta é “sim, DEVE”. Mas tudo na vida tem um porém, e no caso do Identity ele não faz somente uma coisa, ele faz várias, porém não teria nenhum sentido se todas as suas funcionalidades fossem independentes, demandaria muito mais tempo para ser desenvolvido, sem contar todo o processo de implantação e gestão de algo deste porte. Mas a parte boa é que ele possui o seu próprio banco de dados que poderia ainda ser separado em até três, dois para o IdentityServer e um para o Identity, o banco de dados do IdentityServer faria toda o controle voltado para Security Token Service (STS) e o Identity para suas funcionalidades.
Esta aplicação que chamei de “Identity Provider” também pode ser configurada para ter alta escalabilidade e/ou disponibilidade.

Execução

Execute o projeto, agora além de poder solicitar um token para acessar a API payment você também tem uma interface web padrão, ela ainda não possui estilos nem nada, pois farei este ajuste na próxima parte.

Caso você tenha dúvidas, eu disponibilizei o código no GitHub em um branch chamado identityserver-identity.

Continua em ASP.NET Core 3.1 — IdentityServer4 — Identity-UI (Parte 6).

MCAD, CSM palestrante em grupos de discussão de C#, AspNetCore e Azure. Com mais de 12 anos de experiência na área de desenvolvimento, análise e implantação de projetos de TI, adquirindo papéis e responsabilidades ao longo do ciclo de vida do desenvolvimento de software. atuação em análise de sistemas e negócios, dedicando-me no auxílio da qualidade e evolução de produtos de software, assim como na organização de atividades e acompanhamento de projetos de pequeno e médio porte, dentro e fora do Brasil.

Ver perfil