.NET

19 abr, 2018

Criando um servidor de identidade utilizando o Identity Server 4 e o ASP.NET Core 2 – Parte 02

Publicidade

Introdução

Conforme vimos na primeira parte deste artigo, entendemos brevemente o conceito de como funciona o protocolo oAuth 2, o OpenID Connect e o Identity Server 4. Então, sem mais delongas, vamos para a melhor parte, que é colocar tudo isso na prática.

Show me the Code!

Primeiro vamos criar um novo projeto do tipo ASP.NET Core Web Application (File > New Project). Chamaremos ele de IdentityServer, e na tela seguinte escolheremos a opção Web Application (Model-View-Controller), conforme a figura 1, abaixo:

Figura 1

Feito isso, vamos adicionar o Identity Server 4 utilizando o Nuget. Para isso, clique com o botão direito na opção Dependencies e selecione a opção Manage Nuget Packages. Em seguida, adicione o pacote do Identity Server 4, conforme figura abaixo:

Figura 2

Agora, para facilitar nosso desenvolvimento, faremos o download do projeto Quickstart UI repo. Esse projeto nos dá uma interface pronta com todos os tipos de autenticação previamente desenvolvidos. Vamos agora copiar as pastas QuickStart, Views e wwwroot baixadas dentro do nosso projeto para que possamos, em seguida, iniciar o desenvolvimento do nosso servidor de identidade.

Depois de copiar todos os arquivos, poderemos deletar do nosso projeto as pastas Controllers e Models, pois não a utilizaremos mais. Com todas essas alterações feitas, seu projeto deve estar igual ao da Figura 3:

Figura 3

Agora vamos definir uma porta para que o nosso projeto execute. Para isso, botão direito em cima do projeto e clique em properties (se for Visual Studio for Mac, clique em options ao invés de properties), depois clique em build, e no campo App URL, substitua a porta pra 5000 (ou qualquer outra da sua escolha – você só precisa se lembrar dela depois), e então salve o projeto. Feito isso, vamos criar uma classe chamada Config.cs e vamos implementar nossas configurações (comentários no próprio código para melhor entendimento):

using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;

namespace IdentityServer
{
    public class Config
    {
        // Aqui vamos definir os resources que serão utilizados no nosso servidor
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email()
            };
        }

        // Aqui definimos qual Client (aplicação) que poderá acessar nosso servidor de identidade.
        public static IEnumerable<Client> GetClients()
        {
            // Credenciais da Aplicação
            return new List<Client>
            {
                // OpenID Connect
                new Client
                {
                    // O Nome ÚNICO da nossa aplicação autorizada no nosso servidor de autoridade
                    ClientId = "iMastersApp",
                    
                    // Nome de exibição da nossa aplicação
                    ClientName = "iMasters Application",
                    
                    //Tipo de autenticação permitida
                    AllowedGrantTypes = GrantTypes.Implicit,

                    //Url de redicionamento para quando o login for efetuado com sucesso.
                    RedirectUris = { "http://localhost:5001/signin-oidc" },

                    //Url de redirecionamento para quando o logout for efetuado com sucesso.
                    PostLogoutRedirectUris = { "http://localhost:5001/signout-callback-oidc" },

                    //Escopos permitidos dentro da aplicação
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email
                    }
                }
            };
        }

        // TestUser é uma classe de exemplo do proprio IdentityServer, aonde configuramos basicamente login/senha e as claims de exibição.
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    SubjectId = "1",
                    Username = "imasters",
                    Password = "imasters",

                    Claims = new List<Claim>
                    {
                        new Claim("name", "Portal iMasters"),
                        new Claim("website", "https://imasters.com.br"),
                        new Claim("email", "contato@imasters.com")
                    }
                }
            };
        }
    }
}

O próximo passo agora é configurar o Startup.cs da nossa aplicação para que ele fique igual abaixo:

using IdentityServer4.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace IdentityServer
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            // Configuração do Identity Server In-Memory
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryClients(Config.GetClients())
                .AddTestUsers(Config.GetUsers());
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            //Habilitando o uso do Identity Server no nosso projeto
            app.UseIdentityServer();

            //Habilitando o uso de arquivos estáticos (Html, Css e etc) do nosso projeto
            app.UseStaticFiles();

            //Habilitando o uso de rota no projeto
            app.UseMvcWithDefaultRoute();
        }
    }
}

Feito isso, configuramos o nosso servidor de Identidade, e agora podemos criar uma aplicação para testarmos a nossa autenticação. Então basicamente vamos seguir os mesmos passos que fizemos anteriormente ao criar um novo projeto do tipo ASP.NET Core Web Application, porém, esse vamos chamar de iMastersApp. Mais uma vez nossa pasta Models não será utilizada em nosso exemplo, então podemos deletá-la do nosso projeto.

Antes de começarmos a programar nossa Aplicação, vamos primeiramente configurar a porta em que a aplicação vai rodar, e seguindo os mesmos do nosso servidor de identidade, vamos alterar a porta, porém dessa vez vamos apontar para a porta 5001 (observe que no código do Config.cs, apontamos o Login e Logout da aplicação para a porta 5001 também).

Agora podemos começar a programar a nossa aplicação, então primeiramente vamos abrir o arquivo Startup.cs (comentários no próprio código para melhor entendimento):

using System.IdentityModel.Tokens.Jwt;
using IdentityServer4;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace iMastersApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            //Exibe as claims de maneira mais "amigável"
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            //Adciona o serviço de autenticação
            services.AddAuthentication(options =>
            {
                //Nosso esquema default será baseado em cookie
                options.DefaultScheme = "Cookies";

                //Como precisamos recuperar os dados depois do login, utilizamos o OpenID Connect que por padrão utiliza o escopo do Profile
                options.DefaultChallengeScheme = "oidc";
            })
                .AddCookie("Cookies")
                .AddOpenIdConnect("oidc", options =>
                {
                    options.SignInScheme = "Cookies";
                    
                    //Aponta para o nosso servidor de autenticação
                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;
                    
                    //Nome da nossa aplicação que tentará se autenticar no nosso servidor de identidade
                    //Observe que ela possui o mesmo nome da app que liberamos no nosso servidor de identidade
                    options.ClientId = "iMastersApp";
                    options.SaveTokens = true;
                    
                    //Adicionamos o scopo do e-mail para utilizarmos a claim de e-mail.
                    options.Scope.Add(IdentityServerConstants.StandardScopes.Email);
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //Habilitando o uso da autenticação do Identity Server no nosso projeto
            app.UseAuthentication();

            //Habilitando o uso de arquivos estáticos (Html, Css e etc) do nosso projeto
            app.UseStaticFiles();

            //Habilitando o uso de rota no projeto
            app.UseMvcWithDefaultRoute();
        }
    }
}

O próximo passo agora será configurarmos nosso HomeController.cs:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;

namespace MvcClient.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        //Somente será acessada se o usuário estiver logado
        [Authorize]
        public IActionResult Secure()
        {
            return View();
        }

        public async Task Logout()
        {
            //Limpa o cookie para finalizar o logout
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }

        public IActionResult Error()
        {
            return View();
        }
    }
} 

E por fim, vamos criar nossa página que vai exibir os dados caso o usuário seja válido e se logue corretamente. Para isso, crie o arquivo Secure.cshml dentro da pasta Views > Home.

@{
    ViewData["Title"] = "Logon efetuado com sucesso.";
}
<h2>@ViewData["Title"]</h2>

<h3>Claims autorizadas pelo usuário</h3>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl> 

Feito isso vamos colocar nosso Visual Studio para executar os dois projetos simultaneamente, e vamos fazer o login com os dados de acesso que criamos no nosso servidor de identidade. Se tudo deu certo você obterá uma tela igual a figura 4 abaixo:

Figura 4

Agora é só selecionar quais informações deseja exibir e na próxima tela será exibido o resultado de ac.

Bônus

Como bônus, vou implementar um Resource customizado, pois nem sempre temos as Claims que desejamos “prontas”, então mãos a obra!

Primeiramente vamos alterar o método GetIdentityResources() que se encontra no arquivo Config.cs do projeto IdentityServer.

public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            var customProfile = new IdentityResource(
                name: "custom.profile",
                displayName: "Custom Profile",
                claimTypes: new[] { "customclaim" }
            );

            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                customProfile
            };
        } 

Depois, vamos alterar a propriedade AllowedScopes:

AllowedScopes = {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.Email,
                    "custom.profile"
                 } 

Agora vamos adicionar nossa Claim customizada nas Claims do usuário:

Claims = new List<Claim> {
                new Claim("name", "Portal iMasters"),
                new Claim("website", "https://imasters.com.br"),
                new Claim("email", contato@imasters.com                                   new Claim("customclaim", "Minha claim customizada")
             } 

E por ultimo, vamos abrir o arquivo Startup.cs do projeto iMastersApp e colocar a linha abaixo logo após a inclusão do scopo do e-mail:

options.Scope.Add("custom.profile");

Executando o projeto novamente e fazendo o login corretamente, note que apareceu o nosso Resource customizado nas opções de exibição, conforme mostra a figura 5 abaixo:

Figura 5

Bom, pessoal, era isso que eu queria mostrar. Para quem quiser o código completo ele está no meu GitHub.

Muito obrigado e até a próxima!