Do exemplo do IdentityServer para a produção: o que revisar antes de publicar
Revise o que todo exemplo de IdentityServer traz de inseguro (segredo no código, chave de desenvolvimento, usuários de teste, redirect de localhost) e prove com testes que a porta está fechada.
O time clonou um exemplo de IdentityServer do GitHub numa segunda, trocou o logotipo na terça e publicou na quarta. O login funcionava, o token aparecia, o painel abria. Três meses depois, numa revisão de segurança, alguém descobriu que o usuário alice com senha alice ainda entrava em produção, que o segredo do cliente era a palavra secret no código-fonte e que o servidor assinava tokens com uma chave de desenvolvimento gerada num arquivo dentro da pasta da aplicação.
A resposta curta: o exemplo ensina o fluxo, não é a configuração que você publica. Antes de ir para produção, troque cinco coisas: usuários de teste por uma base real, segredos no código por segredos no cofre, chave de assinatura de desenvolvimento por chave persistida e com rotação, redirects de localhost por URLs exatas de produção, e fluxos antigos (implicit, password) por Authorization Code com PKCE. Depois, prove com testes negativos que o que deveria falhar falha.
Por que um servidor de identidade não é um formulário de login
Um formulário de login confere senha e cria uma sessão na própria aplicação. Um servidor de identidade OpenID Connect emite tokens que outras aplicações aceitam como prova de quem a pessoa é e do que ela pode acessar. Cada erro de configuração dele vale para todos os sistemas que confiam nele.
O exemplo de tutorial precisa funcionar na primeira execução, no notebook de qualquer pessoa. Por isso ele aceita http://localhost, guarda tudo em memória, gera uma chave descartável e traz usuários prontos. Cada uma dessas facilidades vira uma porta aberta quando o mesmo código atende a internet.
Há também a questão da versão. O IdentityServer4, que aparece na maioria dos exemplos antigos, deixou de receber correções no fim de 2022. A continuação é o Duende IdentityServer, que tem licença comercial; confira as condições no site da Duende antes de decidir. Alternativas comuns são o OpenIddict, o Keycloak ou um provedor gerenciado. Rodar um servidor de identidade sem correções de segurança é o primeiro item a resolver.
O que o exemplo típico traz e o que fazer com cada item
A configuração de tutorial costuma se parecer com isto:
builder.Services.AddIdentityServer()
.AddInMemoryClients(Config.Clients)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddTestUsers(TestUsers.Users)
.AddDeveloperSigningCredential();
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:5002/signin-oidc" },
AllowedScopes = { "openid", "profile", "api1" }
}
AddTestUsers: usuários fixos com senhas conhecidas. Troque por um repositório real de usuários, como o ASP.NET Core Identity, com senha protegida por hash.AddDeveloperSigningCredential: gera uma chave em arquivo local, pensada para desenvolvimento. Em produção, a chave de assinatura precisa ser persistida, protegida e trocada periodicamente, com publicação da nova antes de aposentar a antiga."secret".Sha256(): segredo no código-fonte, visível para quem lê o repositório. Ele vem da configuração, alimentada pelo cofre.GrantTypes.Implicit: fluxo desaconselhado pelas recomendações atuais de segurança do OAuth, porque o token trafega na URL. Use Authorization Code com PKCE.- Redirect com
http://localhost: em produção, só a URL exata do site, com HTTPS. - Escopo amplo como
api1: dá acesso à API inteira. Divida em escopos pelo que cada cliente realmente faz.
Como preparar a configuração de produção, passo a passo
- Atualize o produto. Saia do IdentityServer4 para uma versão mantida, antes de qualquer outro ajuste.
- Separe autoridade e aplicação. O servidor de identidade tem domínio próprio, como
login.exemplo.com.br, e a loja fica emloja.exemplo.com.br. - Configure cada cliente com o mínimo: um fluxo, URLs exatas, escopos pequenos, tempos de vida curtos.
- Tire segredos do código e carregue da configuração.
- Troque usuários de teste e chave de desenvolvimento.
- Tenha configuração por ambiente. O cliente de desenvolvimento aceita
localhost; o de produção, não. São clientes diferentes, não o mesmo com uma lista longa. - Rode os testes negativos descritos mais abaixo.
O cliente da loja, como vai para produção
public static IEnumerable<Client> Clients(IConfiguration config) =>
[
new Client
{
ClientId = "loja-web",
ClientName = "Loja (site)",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
ClientSecrets = { new Secret(config["Clients:LojaWeb:Secret"]!.Sha256()) },
RedirectUris = { "https://loja.exemplo.com.br/signin-oidc" },
PostLogoutRedirectUris = { "https://loja.exemplo.com.br/signout-callback-oidc" },
AllowedScopes = { "openid", "profile", "pedidos.leitura" },
AllowOfflineAccess = false,
AccessTokenLifetime = 900
}
];
public static IEnumerable<ApiScope> ApiScopes =>
[
new ApiScope("pedidos.leitura", "Consultar pedidos")
];
O AccessTokenLifetime é em segundos: 900 são 15 minutos. AllowOfflineAccess = false significa que esse cliente não recebe refresh token; ligue apenas se a aplicação realmente precisa renovar sem a pessoa estar presente. Por que um token curto importa está em JWT que expira.
Do lado da loja (ASP.NET Core)
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.exemplo.com.br";
options.ClientId = "loja-web";
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code";
options.UsePkce = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("pedidos.leitura");
options.SaveTokens = true;
});
O segredo chega por variável de ambiente ou arquivo montado, nunca dentro da imagem, como mostra segredo fora da imagem. Se o cliente for uma SPA sem backend, não há segredo nenhum, e o caminho é o de login no navegador com PKCE.
Testes negativos: prove que a porta fecha
Teste positivo mostra que o login funciona. O que protege é o teste negativo. Peça uma autorização com um redirect_uri de outro domínio:
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" \
"https://login.exemplo.com.br/connect/authorize?client_id=loja-web&response_type=code&scope=openid&redirect_uri=https://atacante.exemplo.net/cb&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256"
A resposta pode ser uma página de erro ou um redirecionamento para a página de erro do próprio servidor de identidade. O que não pode aparecer é atacante.exemplo.net no destino. Repita trocando response_type=code por response_type=token (o fluxo implícito tem de ser recusado), tente o login com alice/alice e peça um escopo que o cliente não tem. Todos têm de falhar.
Erros comuns
- Usuários de teste em produção. Qualquer pessoa que leu o mesmo tutorial sabe a senha.
- Chave de desenvolvimento assinando tokens. A chave fica num arquivo sem proteção e muda se o container for recriado, invalidando todos os tokens emitidos.
- Redirect com curinga ou lista "para facilitar". Um domínio esquecido na lista vira o destino do código de autorização de outra pessoa.
- Mesmo cliente para desenvolvimento e produção. O
localhostdo desenvolvedor fica aceito no cliente que atende clientes reais. - Escopo único para tudo. Um token vazado de um painel de leitura consegue escrever na API.
- Confundir identidade com permissão. O ID token diz quem entrou, não o que pode fazer, como explica ID token não é autorização.
Checklist
- O servidor de identidade é uma versão que ainda recebe correções de segurança.
- Não há
AddTestUsersnemAddDeveloperSigningCredentialna configuração de produção. - Nenhum segredo de cliente está no repositório.
- Cada cliente de produção usa Authorization Code com PKCE e URLs de redirect exatas com HTTPS.
- Clientes de desenvolvimento e produção são registros separados.
- Escopos refletem o que cada cliente faz, e tokens de acesso têm vida curta.
- Os testes de redirect estranho, fluxo implícito e usuário de teste falham como esperado.
Abra agora a configuração do cliente de produção no seu servidor de identidade: algum RedirectUris ainda aponta para localhost ou para o domínio do tutorial?
Referência: https://github.com/brunohbrito/AspNetCore-IdentityServer4-Example