Na Parte 1 falei sobre a configuração do IdentityServer4 para que funcione em memória, esta forma possui alguns benefícios de curto prazo para testar e validar o processo de desenvolvimento dos serviços e para que você não dependa de um banco de dados.
Como já comentei próprio IdentityServer sugere que você não use a configuração in-memory em produção e sim passe a utilizar a configuração em store como é chamado, ou seja, utilizando um banco de dados onde os dados possam ser persistidos. Então, vamos utilizar o SQLServer para ser a nossa store.
No meu caso eu estou utilizando docker para poder ter um banco de dados disponível de forma rápida e prática em ambiente de desenvolvimento. Os DBA’s reforçam que você nunca deve ter um banco de dados SQLServer em produção dentro de um container, porém para desenvolvimento é uma prática natural e muito ágil pela facilidade e velocidade de configuração.
Docker
Não entrarei muito em detalhes sobre o docker, mas se você quiser usá-lo você tem duas opções:
- Se você tiver Windows Home instalado, vai precisar usar o docker como se fosse serviços virtualizados pelo VirtualBox por exemplo;
- Para Windows Pro, você pode baixar o docker desktop vai facilitar muito a sua vida e é o meu caso.
Eu estou utilizando o docker com containers em Linux, pois as imagens são menores.
Instalando SQLServer com Docker
Basta você abrir o PowerShell como administrador e digitar o comando a seguir:
docker run -e ‘ACCEPTEULA=Y’ -e ‘SAPASSWORD=Password!’ -p 1433:1433 -d — restart unless-stopped -v sqlserver-volume:/var/opt/mssql mcr.microsoft.com/mssql/server:2017-latest
O comando acima realiza o download da última versão do SQLServer Docker e cria o volume ‘sqlserver-volume’ automaticamente caso não exista.
O comando --restart unless-stopped
é para caso o servidor reinicie (ou seu PC), ao retornar inicializa o container automaticamente. Mais detalhes aqui
O uso do volume permite a persistência dos dados caso o container do SQLServer seja eventualmente removido ou recriado em outra versão. Caso você remover o container e criá-lo novamente, o volume vai apoiar na manutenção da base de dados que já existia, sem perder as informações.
- Usuário: sa
- Senha: Password!
- Porta: 1433, esta já é a porta padrão do SQLServer
Através do Visual Studio, você tem o SQL Server Object Explorer, adicione um novo servidor com as configurações conforme a tela abaixo e pronto, você se conectará ao banco de dados que foi criado.

Funcionalidades
As funcionalidades disponíveis no IdentityServer estão divididas em duas áreas: configuration store e operational store. Ambas podem ser usadas de forma independente ou juntas dependendo das necessidades.
Estas funcionalidades são utilizadas para que os dados possam ser persistidos e utilizados a partir do suporte do EntityFramework em uma base de dados.
Configuration Store
Esta configuração está relacionada ao armazenamento das informações que representam: client, identity resource, API resource, ou CORS e a classe de contexto é a ConfigurationDbContext.
Operational Store
Esta configuração está relacionada ao armazenamento das informações que representam: authorization grants, consents e tokens (refresh e reference) e a classe de contexto é a PersistedGrantDbContext.
Statup
A configuração do arquivo startup.cs fica conforme a seguir.
using IdentityProvider.Seeds;
using IdentityServer4.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
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;
});
if (_webHostEnvironment.IsDevelopment())
identityServer.AddDeveloperSigningCredential();
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);
});
});
services.AddAuthentication();
}
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.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
}
}
appsettings
Agora que não estamos mais trabalhando com o IdentityServer em memória, podemos limpar o arquivo de configurações e colocarmos a string de conexão com o SQLServer conforme a seguir.
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=IdentityServer;User ID=sa;Password=Password!"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"WEBSITE_LOAD_CERTIFICATES": "*"
}
Seed
Nada adianta termos um banco de dados que não possui dados, então criei alguns métodos de extensão (extension methods) que são chamados lá no Startup.cs no método Configuration. Estes métodos vão criar o banco de dados e já adicionar os dados que estavam antes no arquivo de configurações. O ideal seria termos uma tela para cadastrar os dados, mas para acelerarmos este processo e termos um resultado mais palpável esta forma é mais simples neste momento.
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
namespace IdentityProvider.Seeds
{
public static class IdentityServerSeed
{
public static void RunMigration(this IApplicationBuilder app)
{
using var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope();
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>().Database.Migrate();
}
public static void SeeClient(this IApplicationBuilder app)
{
using var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
if (!context.Clients.Any())
{
var client = new Client
{
ClientId = "payment-client",
ClientName = "Client Credentials for Payment API",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("payment-secret".Sha256())
},
AllowedScopes =
{
"payment"
},
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true,
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true
};
context.Clients.Add(client.ToEntity());
context.SaveChanges();
}
}
public static void SeedApiResource(this IApplicationBuilder app)
{
using var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
if (!context.ApiResources.Any())
{
var apiResource = new ApiResource("payment", "Payment API");
context.ApiResources.Add(apiResource.ToEntity());
context.SaveChanges();
}
}
}
}
Grant Type — Client Credentials
Vamos falar um pouco sobre o grant type utilizado pelo client que a API de Pagamento usa para permitir o acesso do token do escopo payment.
Este grant type é o mais simples e mais utilizado na Machine to Machine Communication. O token é gerado em nome de um client sem que haja relação com um usuário da aplicação, usuário aqui significa um ser humano que acessa uma tela de login e informa seu usuário e senha para se logar.
Quando o token é garado, as únicas informações necessárias são o ID e a senha (secret) e com o token pertencendo ao escopo payment é possível realizar o acesso à API de Pagamento.
Migrations
Para iniciar o banco de dados, precisamos das famosas migrations, para isso basta executar os comandos a seguir dentro do visual studio através do Package Manager Console, para isso você vai precisar que os pacotes abaixo estejam instalados:
Microsoft.EntityFramework Microsoft.EntityFramework.Tools
Add-Migration Initial -Context ConfigurationDbContext Update-Database -Context ConfigurationDbContext
Add-Migration Initial -Context PersistedGrantDbContext Update-Database -Context PersistedGrantDbContext
Execução
Depois de ter feito tudo isso, rode o projeto do IdentityServer, pode levar alguns segundos até ele criar a base de dados para aí sim salvar as informações e você pode voltar lá na Parte 3, buscar o token que permite acessar a API payment que foi configurado e chamar a API de Pagamento através do Postman conforme as imagens de exemplo.
Caso você tenha dúvidas, eu disponibilizei o código no GitHub em um branch chamado identityserver-sqlserver.
Continua na Parte 5.