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:
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> // usuário não iterativo, usa o clientid/secret para autenticacao // segredo para autenticacao // escopos que o cliente pode acessar ... |
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 builder.Services.AddIdentityServer() return builder.Build(); if (app.Environment.IsDevelopment()) // uncomment if you want to add a UI app.UseIdentityServer(); // uncomment if you want to add a UI 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") options.TokenValidationParameters = new TokenValidationParameters ... app.UseHttpsRedirection(); |
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: