IdentityServer - Protegendo uma API c/credenciais do cliente


  Este tutorial vai mostrar como proteger uma API usando as credenciais do cliente usando o IdentityServer em um cenário básico.

Este tutorial se baseia na documentação do Duende IdentityServer com pequenas adaptações e ajustes.

O Duende IdentityServer é um framework que implementa os protocolos OpenID Connect e OAuth 2.0 e que pode ser usado com a ASP .NET Core.

O OpenID Connect (OIDC) é uma camada simples de protocolo de identidade e autenticação construída sobre o protocolo OAuth que permite que aplicativos (normalmente chamados de clientes) verifiquem a identidade dos usuários finais.

O OAuth é um padrão aberto para autorização que fornece acesso delegado seguro, o que significa que um aplicativo (ou cliente) pode executar ações ou acessar recursos em um servidor de recursos em nome de um usuário, sem que o usuário compartilhe suas credenciais com o aplicativo.

Neste tutorial vamos criar uma solução contendo 3 projetos:

  1. Um projeto Identity Server
  2. Um projeto Web API protegido que requer autenticação
  3. Um projeto Cliente que acessa a API

O funcionamento é bem simples:  cliente vai solicitar um token de acesso do IdentityServer usando seu ID e segredo do cliente e, em seguida, usará o token para obter acesso à API.

A Autenticação significa o processo usado para determinar se um usuário é quem ele afirma ser. Uma vez autenticado, a Autorização determina quais recursos um determinado usuário pode acessar e o que ele têm permissão para fazer com esses recursos.

Recursos usados:

Instalando os templates e criando a solução e o projeto IdentityServer

Vamos iniciar instalando os  templates prontos do IdentityServer usando a ferramenta de linha de comando - CLI usando o comando em um terminal de comandos :

dotnet new --install Duende.IdentityServer.Templates

Com os templates instalados podemos iniciar a criação da nossa solution e do projeto IdentityServer.

Crie uma pasta onde deseja salvar o projeto e a seguir emita comando:

dotnet new sln -n DemoIS

Com isso criamos uma solução na pasta.

Agora crie uma pasta src nesta pasta e emita o comando :

dotnet new isempty -m IdentityServer

Ao final desta etapa teremos criado na pasta src o projeto IdentityServer contendo:

Vamos incluir o projeto IdentityServer criado no arquivo de solução DemoIS.

Para isso vamos retornar para pasta do projeto e digitar o comando:

dotnet sln add ./src/IdentityServer/IdentityServer.csproj

Podemos abrir a solução e o projeto no VS Code a partir da pasta do projeto digitando : code .

Definindo um Escopo de API e definindo um cliente

O escopo é um recurso central do OAuth que permite expressar a extensão ou o escopo do acesso. Os clientes solicitam escopos quando iniciam o protocolo, declarando qual escopo de acesso desejam.

O IdentityServer então precisa decidir quais escopos incluir no token. Só porque o cliente pediu algo não significa que ele deve receber! Existem abstrações internas, bem como pontos de extensibilidade que você pode usar para tomar essa decisão.

Por fim, o IdentityServer emite um token para o cliente, que então usa o token para acessar as APIs. As APIs podem verificar os escopos incluídos no token para tomar decisões de autorização.

Os escopos não têm estrutura imposta pelos protocolos - eles são apenas strings separadas por espaço. Isso permite flexibilidade ao projetar os escopos usados ​​por um sistema.

Vamos iniciar criando um escopo que representa o acesso completo à sua API.

Temos no projeto IdentityServer o arquivo Config contendo uma configuração mínima criada pelo template e vamos adicionar uma nova ApiScope à propriedade ApiScopes conforme mostrado a seguir:

A próxima etapa é configurar um aplicativo cliente que você usará para acessar a API.

Neste momento o cliente não terá um usuário interativo e será autenticado com o IdentityServer usando um segredo do cliente. Para isso vamos incluir o código a seguir no arquivo Config:

...

public static IEnumerable<Client> Clients =>   new List<Client>
{
        new Client
        {
            ClientId = "client",

            // usuário não iterativo, usa o clientid/secret para autenticacao
           
AllowedGrantTypes = GrantTypes.ClientCredentials,

            // segredo para autenticacao
           
ClientSecrets =
            {
                new Secret("secret".Sha256())
            },

            // escopos que o cliente pode acessar
           
AllowedScopes = { "api1" }
        }

    };
}

...

Os clientes podem ser configurados com muitas opções.

O nosso cliente definido neste exemplo possui:

- Um ClientId, que identifica o aplicativo para IdentityServer para que ele saiba qual cliente está tentando se conectar a ele;
- Um segredo, que você pode imaginar como a senha do cliente;
- A lista de escopos que o cliente pode solicitar;

Observe que o escopo permitido aqui corresponde ao nome do ApiScope que definimos anteriormente.

Configurando o IdentityServer

As definições de escopo e cliente são carregadas em HostingExtensions.cs. O template criou um método ConfigureServices que já está carregando os escopos e clientes. Você pode dar uma olhada para ver como isso é feito. Observe que o modelo adiciona algumas coisas que não são usadas neste exemplo.

A seguir temos o código do método ConfigureServices mínimo necessário para o nosso exemplo:

using Serilog;

namespace IdentityServer;

internal static class HostingExtensions
{
    public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
    {
      
 // uncomment if you want to add a UI
        //builder.Services.AddRazorPages();

        builder.Services.AddIdentityServer()
            .AddInMemoryIdentityResources(Config.IdentityResources)
            .AddInMemoryApiScopes(Config.ApiScopes)
            .AddInMemoryClients(Config.Clients);

        return builder.Build();
    }

    public static WebApplication ConfigurePipeline(this WebApplication app)
    {
        app.UseSerilogRequestLogging();
   

        if (app.Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // uncomment if you want to add a UI
        //app.UseStaticFiles();
        //app.UseRouting();

           

        app.UseIdentityServer();

        // uncomment if you want to add a UI
        //app.UseAuthorization();
        //app.MapRazorPages().RequireAuthorization();

        return app;
    }
}

Obs: Tive que alterar a porta de 5001 para 5002 pois no meu ambiente estava com conflito.

Este código configura o nosso IdentityServer e se você executar o projeto e navegar para https://localhost:5002/.well-known/openid-configuration em seu navegador, deverá ver o documento de descoberta.

O documento de descoberta é um endpoint padrão no OpenID Connect e no OAuth. Ele é usado por seus clientes e APIs para recuperar dados de configuração necessários para solicitar e validar tokens, login e logout, etc.

Para executar o projeto digite o comando a seguir na pasta do projeto: dotnet run

Criando o projeto Web API

Abra o um terminal de comandos e vamos criar um projeto Web API e a seguir adicionar este projeto à nossa solução.

Assim na pasta src vamos criar um projeto chamado Api :  dotnet new webapi -n Api

A seguir vamos retroceder um nível e incluir o projeto na solução:  dotnet sln add ./src/Api/Api.csproj

Podemos visualizar a estrutura da solution conforme mostra a figura:

Agora vamos adicionar JWT Bearer Authentication ao pipeline ASP.NET da API.

Queremos autenticar usuários de nossa API usando tokens emitidos pelo projeto IdentityServer e para isso, vamos incluir o middleware de autenticação ao pipeline do pacote nuget Microsoft.AspNetCore.Authentication.JwtBearer no projeto Api.

Este middleware vai fazer o seguinte:

Digite seguinte comando para incluir o pacote:

dotnet add ./src/Api/Api.csproj package Microsoft.AspNetCore.Authentication.JwtBearer

Agora vamos incluir os serviços de autenticação JWT Bearer à coleção de serviços para permitir a injeção de dependência (DI) e configurar o Bearer como o esquema de autenticação padrão.

Inclua no arquivo Program do projeto Api o código a seguir:

...

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.
Authority = "https://localhost:5002";

        options.TokenValidationParameters = new TokenValidationParameters
        {
           
ValidateAudience = false
        };
    });

...

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();


app.MapControllers();

app.Run();

Nota: A validação da audiência está desabilitada aqui porque o acesso à API é modelado apenas com ApiScopes. Por padrão, nenhum público será emitido, a menos que a API seja modelada com ApiResources.

Vamos agora incluir um controlador na API chamado IdentityController:

[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

Este controlador será usado para testar a autorização e visualizar a identidade das reivindicações através da API.

Vamos configure a API para ser executada no endereço https://localhost:6001.

Para isso edite o arquivo launchSettings.json dentro da pasta Properties e altere a configuração de URL do aplicativo para:

Execute o projeto API e navegue até o controlador de identidade em https://localhost:6001/identity em um navegador.

Isso deve retornar um código de status 401, o que significa que sua API requer uma credencial e agora está protegida pelo IdentityServer.

Criando o projeto Cliente

Vamos agora criar um cliente que solicite um token de acesso e então utilize este token para acessar a API.

Nosso cliente será um projeto Console.

Para isso vamos digita o comando a seguir a partir a pasta src: dotnet new console -n Client

A seguir vamos incluir o projeto na solução:  dotnet sln add ./src/Client/Client.csproj

O endpoint do token no IdentityServer implementa o protocolo OAuth e podemos usar HTTP bruto para acessá-lo. No entanto, temos uma biblioteca cliente chamada IdentityModel que encapsula a interação do protocolo em uma API fácil de usar.

Assim vamos inclui  o pacote IdentityModel NuGet no projeto Client digitando o comando:

dotnet add ./src/Client/Client.csproj package IdentityModel

O IdentityModel inclui uma biblioteca de cliente para usar com o endpoint de descoberta. Dessa forma, você só precisa saber o endereço base do IdentityServer - os endereços de endpoint reais podem ser lidos nos metadados.

Adicione o seguinte ao Program.cs do projeto Client:

Em seguida, você pode usar as informações do documento de descoberta para solicitar um token do IdentityServer para acessar api1.

Para isso inclua o código abaixo:

Para enviar o token de acesso à API, você normalmente usa o cabeçalho HTTP Authorization. Isso é feito usando o método de extensão SetBearerToken.

Assim vamos complementar o código da classe Program incluindo trecho de código abaixo:

Você pode complementar o código acima incluindo uma linha de código para não fechar a janela do console.

Vamos usar o Visual Studio 2022 para executar os três projetos e obter o resultado.

Para isso abra o projeto no VS 2022 e na janela de propriedades da Solution configure para executar os projetos conforme abaixo:

Agora é só alegria...

Executando a solução teremos o seguinte resultado na aplicação Client:

Por padrão, um token de acesso conterá declarações sobre o escopo, tempo de vida (nbf e exp), o ID do cliente (client_id) e o nome do emissor (iss).

Autorização na API

Neste momento, a API aceita qualquer token de acesso emitido pelo seu IdentityServer.

Vamos agora adicionar uma Política de Autorização à API que verificará a presença do escopo “api1” no token de acesso. O protocolo garante que esse escopo só estará no token se o cliente solicitar e o IdentityServer permitir que o cliente tenha esse escopo.

Já configuramos o IdentityServer para permitir esse acesso incluindo-o na propriedade allowedScopes.

Adicione o seguinte código na classe Program da API:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiScope", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("scope", "api1")
;
    });
});

Agora podemos aplicar essa política em vários níveis, por exemplo:

Normalmente, definimos a política para todos os controladores onde eles são mapeados em Api\Program.cs:

app.MapControllers().RequireAuthorization("ApiScope");

Assim concluímos esse tutoria onde agora o cliente consegue solicitar um token e pode usar o token para acessar a API.

Pegue o projeto aqui :  duendeis.zip (sem as referências)

'Como, pois, invocarão aquele em quem não creram? e como crerão naquele de quem não ouviram? e como ouvirão, se não há quem pregue?'
Romanos 10:14

Referências:


José Carlos Macoratti