Criando um CRUD com .NET 5, MySQL, Angular 11, Keycloak, Phometheus e Grafana — Parte 2
Esse artigo faz parte de uma série onde criamos uma solução completa de cadastro de produtos, com separação entre API e Frontend, autenticação e monitoramento. Caso não tenha visto a parte 1, ela pode ser encontrada aqui. Nesse artigo vamos configurar o Keycloak, criando um reaml e um client que serão usados pela nossa API para autenticar usuários. Será configurada a API para validar os tokens recebidos no servidor Keycloak. Também vamos configurar o Postman para que possamos testar nossa autenticação.
Subindo uma instancia do Keycloak com Docker
Para ter o servidor de autenticação funcionando em nossas máquinas precisamos criar um banco de dados para usuários e uma instância do keycloak com uma porta exposta para o nosso host, essa configuração deve ser adicionada no arquivo docker-compose.yml:
[...]
db_auth:
image: postgres:13
container_name: db_auth
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: quay.io/keycloak/keycloak:12.0.4
container_name: keycloak
environment:
DB_VENDOR: POSTGRES
DB_ADDR: db_auth
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin123
ports:
- 8080:8080
depends_on:
- db_auth
restart: always
Agora podemos subir nossa infraestrutura com:
docker-compose up --build
Depois de criada a aplicação podemos abrir o visual studio code nessa pasta:
code .
Podemos acessar o portal de administração no endereço http://keycloak:8080/auth/admin, o usuário e senha de administração está configurada no docker-compose.yml. No nosso caso está como usuário admin e senha admin123.
Configurando o Keycloak
Primeiro precisamos criar um reaml, que de forma bem abstrata pode ser encarado como um repositório de usuários. Essa criação pode ser feita em:
O nome do nosso realm vai ser products_api:
Não precisamos alterar nenhuma informação na aba General. Na aba Login vamos precisar habilitar o User Registration e desabilitar o Login with Email, ficando da seguinte maneira:
Podemos salvar.
Agora vamos criar um cliente chamado api, isso pode ser feito clicando em Clients no menu lateral esquerdo e depois em create.
Podemos salvar.
Precisamos adicionar a url http://localhost:4200/* (URL que nossa aplicação angular vai ter) ao campo Valid Redirect URIs e * ao campo Web Origins (Para não termos problemas com CORS):
Exportando nossas configurações do Keycloak
Sempre que destruímos nosso container essa configuração é perdida, para evitar ter que fazer essa configuração a cada execução, vamos exportar nosso realm para um arquivo.
Para exportar o realm acessamos a opção Export no menu lateral esquerdo.
Vamos marcar as opções Export groups and roles e a opção Export Clients:
Podemos exportar e guardar esse arquivo.
Importando automaticamente o reaml
Para automatizar o processo de criação do realm criaremos uma pasta chamada docker na raiz do nosso repositório. Dentro dessa pasta vamos criar uma outra pasta chamada keycloak e dentro dela vamos colocar o arquivo de configuração do reaml que baixamos no passo anterior.
O caminho final do arquivo dentro do nosso repositório deve ser:
docker\keycloak\realm-export.json
Depois disso precisaremos alterar a configuração do nosso keycloak dentro do arquivo docker-compose.yml:
[...]
keycloak:
image: quay.io/keycloak/keycloak:12.0.4
container_name: keycloak
volumes:
- ./docker/keycloak:/opt/jboss/keycloak/imports
environment:
KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm-export.json
DB_VENDOR: POSTGRES
DB_ADDR: db_auth
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin123
ports:
- 8080:8080
depends_on:
- db_auth
restart: always
No trecho acima adicionamos um volume para compartilhar com o container a pasta criada anteriormente e usamos a variável de ambiente KEYCLOAK_IMPORT para informar ao keycloak que arquivo deve ser importado na criação do serviço.
Configurando nossa maquina para uso do keycloak
O servidor do keycloak precisa ter o mesmo endereço tanto para o frontend que vai obter o token no navegador, quanto para a api que vai validar o token dentro do docker. No nosso caso, dentro do ambiente do docker, nosso container responde pelo nome de keycloak, pois esse é o nome do nosso serviço. Fora do docker, quando acessamos o servidor usamos o endereço localhost:8080.
Essa diferença de nome de host faz com que os tokens não possam ser validados. Para corrigir isso precisaremos criar um endereçamento keycloak no nosso host, adicionando ao arquivo C:\Windows\System32\drivers\etc\hosts a seguinte entrada:
# Para usar o keycloak via docker
127.0.0.1 keycloak
Dessa forma fazemos com que seja redirecionado para nosso ip de loopback o host “keycloak”.
Podemos testar se tudo funcionou acessando a url http://keycloak:8080/auth/admin, que deve levar para o mesmo servidor que configuramos anteriormente.
Configurando a API .NET para validar tokens no keycloak
Nesse passo vamos configurar nosso controller para exigir autenticação e vamos fazer com que nossa API valide os tokens recebidos no keycloak. Para isso vamos começar indo até a parta Products.API com o terminal e instalando as seguintes dependências:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 5.0.5
Vamos também precisar criar nossa configuração de JWT nos arquivos appsettings.json e appsettings.Development.json:
{
"Jwt": {
"Authority": "http://keycloak:8080/auth/realms/products_api",
"Audience": "account"
},
[...]
}
Nesse caso vamos usar o mesmo host para desenvolvimento e produção, pois já equalizamos isso na configuração do arquivo hosts. Nosso próximo passo está no arquivo Startup.cs, vamos precisar adicionar as seguintes instruções no método ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = Configuration["Jwt:Authority"];
o.Audience = Configuration["Jwt:Audience"];
o.RequireHttpsMetadata = false;
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync(c.Exception.ToString());
}
};
});
[...]
}
Também precisamos configurar nosso app para usar autenticação no método Configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseContext context)
{
app.UseAuthentication();
[...]
}
Também precisamos anotar a classe ProductsController com AutorizeAttribute:
[ApiController]
[Route("[controller]")]
[Authorize]
public class ProductsController : ControllerBase
{
[...]
}
Para que nossa documentação reflita nosso modelo de autenticação, precisamos dentro de ConfigureServices adicionar as instruções AddSecurityDefinition e AddSecurityRequirement no método AddSwaggerGen:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Products.API", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"Put **_ONLY_** your JWT Bearer token on textbox below!",
Name = "JWT Authentication",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
Por padrão a página que descreve nossa API só é exibida em desenvolvimento, para fins didáticos vamos retirar essa restrição. Para isso vamos retirar as instruções UseSwagger e UseSwaggerUI da condição que verifica se é um ambiente de desenvolvimento no método Configure:
[...]
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Products.API v1"));
[...]
Ao acessar http://localhost:5000/swagger/ podemos ver nossa documentação já com o botão Authorize onde podemos configurar nosso token:
Habilitando CORS
Precisamos configurar nossa API para receber requisições de domínios distintos (CORS), para fins didáticos vamos permitir requisições de qualquer domínio. Essa configuração é feita na classe Startup nos métodos ConfigureServices e Configure:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
[...]
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment DatabaseContext context)
{
app.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
[...]
}
Testando nossa autenticação com Postman
Para testar nossa aplicação vou utilizar o postman, mas primeiro precisamos subir nosso ambiente novamente:
docker-compose up --build
Já dentro do postman, vamos fazer uma requisição do tipo GET para http://localhost:5000/products e receberemos um status 401, indicando que não estamos autorizados:
Precisamos configurar o postman para obter um token no keycloak e usar esses token na requisição, isso pode ser feito indo a aba Authorization e selecionando o tipo de autenticação OAuth 2.0.
Eu preenchi essa tela com os seguintes parâmetros:
Header Prefix: Bearer
Token Name: API
Grant Type: Authorization Code
Callback URL: http://localhost:4200/callback
Authorize using browser: false
Auth URL: http://keycloak:8080/auth/realms/products_api/protocol/openid-connect/auth
Access Token URL:http://keycloak:8080/auth/realms/products_api/protocol/openid-connect/token
Client ID: api
Client Secret: <vazio>
Scope: email
State: <vazio>
Client Authentication: Send as Basic Auth header
Agora já podemos clicar no botão Get new access token, fazendo isso vamos nos deparar com a tela de login do nosso sistema:
Como ainda não temos um usuário, podemos clicar no botão Register.
Ao fim do registro o Postman vai mostrar o token recebido:
Podemos clicar no botão Use Token e enviar novamente nossa requisição:
Pronto, recebemos um status 200 que indica que a operação foi efetuada com sucesso!
Assim finalizamos nossa segunda parte da construção do sistema, na próxima parte vamos criar nossa aplicação angular para consumir nossa API.
Gostou? Tem alguma sugestão ou dúvida? Deixa ai nos comentários!
Segue o link do repositório no estado atual do projeto:
Comments