Programação Por Equipe DevBras

Capa do artigo: Do exemplo do IdentityServer para a produção: o que revisar antes de publicar

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

  1. Atualize o produto. Saia do IdentityServer4 para uma versão mantida, antes de qualquer outro ajuste.
  2. Separe autoridade e aplicação. O servidor de identidade tem domínio próprio, como login.exemplo.com.br, e a loja fica em loja.exemplo.com.br.
  3. Configure cada cliente com o mínimo: um fluxo, URLs exatas, escopos pequenos, tempos de vida curtos.
  4. Tire segredos do código e carregue da configuração.
  5. Troque usuários de teste e chave de desenvolvimento.
  6. 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.
  7. 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 localhost do 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á AddTestUsers nem AddDeveloperSigningCredential na 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?

Avalie este artigo

Referência: https://github.com/brunohbrito/AspNetCore-IdentityServer4-Example