Programação Por Equipe DevBras

Capa do artigo: Login com provedor de identidade: mostre a tela dele e trate o erro na volta

Login com provedor de identidade: mostre a tela dele e trate o erro na volta

Por que a senha não deve ser digitada no seu domínio quando quem autentica é o IdP, e como tratar cancelamento, erro e loop de redirecionamento no retorno com ASP.NET Core e OpenID Connect.

O portal de pedidos B2B usa o provedor de identidade da empresa para autenticar clientes. O time de design achou feio sair do portal para fazer login e pediu um formulário de e-mail e senha dentro da própria página. O desenvolvedor atendeu: o formulário postava a senha para o portal, que a repassava ao provedor. Ficou bonito. Duas semanas depois, o segundo fator de autenticação foi ativado no provedor e o login do portal parou de funcionar para todo mundo.

A resposta curta: se o provedor de identidade autentica, a pessoa digita a senha na tela do provedor, no domínio dele. Seu sistema redireciona com o fluxo Authorization Code (com PKCE), recebe o código de volta e só cria a sessão depois de validar o token. A personalização visual se faz no provedor, não copiando a tela. E o retorno precisa tratar cancelamento e erro com uma página clara, sem loop de redirecionamento.

Por que copiar a tela do provedor é pior do que parece

O fluxo em que o aplicativo recebe usuário e senha e troca por token tem nome: Resource Owner Password Credentials. A boa prática atual de segurança do OAuth, publicada pela IETF, diz que ele não deve ser usado, e o rascunho do OAuth 2.1 o deixa de fora. Os motivos aparecem todos no portal do exemplo.

  • A senha passa pelo seu servidor: qualquer log, proxy ou falha no portal expõe a credencial corporativa da pessoa.
  • Segundo fator, senha expirada, conta bloqueada e consentimento são telas do provedor. Seu formulário não sabe desenhá-las e quebra quando o provedor muda.
  • Login único deixa de existir: quem já está autenticado no provedor digita a senha de novo.
  • Você treina a pessoa a cair em phishing: ela aprende que é normal digitar a senha corporativa em qualquer site com a logo certa.

Saber, na tela, quem está pedindo a senha é parte da segurança. O endereço na barra do navegador tem que ser o do provedor. O guia IdentityServer não é um formulário de login com nome bonito trata do lado de quem opera o provedor. Aqui o foco é o aplicativo cliente.

Como montar o login redirecionado em ASP.NET Core

  1. Configure cookie para a sessão do aplicativo e OpenID Connect para o desafio.
  2. Use ResponseType = "code" com PKCE.
  3. Guarde o segredo do cliente fora do código, em configuração protegida.
  4. Aponte AccessDeniedPath para uma página própria, para o caso em que a pessoa recusa ou cancela.
  5. Trate qualquer outra falha no retorno com OnRemoteFailure, registrando o motivo sem token.
  6. A página de entrada do portal é só um botão que inicia o desafio, com o nome do provedor escrito.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(options =>
{
    options.Authority = "https://login.exemplo.com.br";
    options.ClientId = "portal-pedidos";
    options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
    options.ResponseType = "code";
    options.UsePkce = true;
    options.Scope.Add("email");
    options.AccessDeniedPath = "/login/cancelado";
    options.Events = new OpenIdConnectEvents
    {
        OnRemoteFailure = context =>
        {
            var logger = context.HttpContext.RequestServices
                .GetRequiredService<ILogger<Program>>();
            logger.LogWarning("Falha no retorno do login: {Motivo}", context.Failure?.Message);
            context.Response.Redirect("/login/falhou");
            context.HandleResponse();
            return Task.CompletedTask;
        }
    };
});

O HandleResponse() diz ao middleware que você já cuidou da resposta. Sem ele, a exceção sobe e a pessoa vê uma página de erro 500 com cara de bug. O log guarda a mensagem da falha, que costuma bastar para diagnóstico, e nunca o código, o token ou os cabeçalhos completos. O mesmo cuidado vale para o resto do sistema, como explica log sem senha.

Entrar, cancelar e falhar

A entrada é explícita e diz quem vai autenticar:

app.MapGet("/entrar", () => Results.Challenge(
    new AuthenticationProperties { RedirectUri = "/pedidos" },
    new[] { OpenIdConnectDefaults.AuthenticationScheme }));
<h1>Portal de pedidos</h1>
<p>O acesso usa a sua conta corporativa. Você vai ser levado à página de login da Empresa Exemplo e voltará para cá.</p>
<a href="/entrar">Entrar com a conta Empresa Exemplo</a>

O destino depois do login é fixo. Se você quiser devolver a pessoa à página onde ela estava, aceite só caminhos locais, ou vai criar um redirecionamento aberto, como mostra redirect que obedece ao parâmetro next.

As páginas /login/cancelado e /login/falhou precisam estar fora de qualquer [Authorize]. Cada uma explica o que houve em uma frase e oferece duas saídas: "Tentar de novo", que leva a /entrar, e "Voltar ao início". Nada de redirecionar sozinho.

Por que o loop acontece

O loop clássico tem quatro tempos. O retorno do provedor falha, por exemplo porque o cookie de correlação não voltou numa navegação entre sites. O aplicativo trata a falha mandando a pessoa para a página inicial. A página inicial exige login e dispara o desafio. O provedor, que já tem sessão, devolve na hora, e o retorno falha de novo. Em segundos, o navegador desiste com "muitos redirecionamentos".

A página de falha fora de proteção quebra o ciclo. O motivo da falha, no log, diz o que corrigir. Quando o problema só aparece em um navegador, a causa costuma ser cookie e política SameSite, assunto de login OIDC que só falha num browser.

Personalizar sem copiar

A vontade do design era legítima: a pessoa não deveria sentir que saiu para outro lugar. A solução está no provedor. Os principais permitem logo, cores e textos por aplicativo cliente, e o IdentityServer, em particular, tem a interface de login inteira sob controle de quem o hospeda. Peça ao time do provedor um tema com a identidade do portal. A senha continua sendo digitada no domínio certo, e o segundo fator funciona porque a tela é a dele.

Erros comuns

  • Formulário de senha no aplicativo repassando ao provedor: a credencial passa pelo seu servidor e o segundo fator quebra o login.
  • Não tratar cancelamento: a pessoa clica em "Cancelar" no provedor e recebe erro 500.
  • Página de erro protegida por login: a falha vira loop de redirecionamento.
  • Registrar o token ou a query string inteira do retorno no log: quem lê o log pode sequestrar a sessão.
  • Misturar login local e externo na mesma tela sem rótulo: a pessoa não sabe qual senha digitar nem para quem.
  • Destino pós-login vindo da URL sem validação: o login vira trampolim para site falso.

Checklist

  • A senha é digitada só no domínio do provedor.
  • O fluxo é Authorization Code com PKCE.
  • A sessão do aplicativo nasce só depois do token validado pelo middleware.
  • Cancelar no provedor leva a uma página clara, sem erro 500.
  • Falhas no retorno caem numa página fora de proteção, com "tentar de novo".
  • O log registra o motivo da falha, sem token nem código.
  • O destino depois do login é fixo ou validado como caminho local.

Abra o login do seu sistema, vá até a tela do provedor e clique em "Cancelar" ou volte pelo navegador: o que aparece para a pessoa, e dá para saber dali como tentar de novo?

Avalie este artigo

Referência: https://identityserver4.readthedocs.io/en/latest/topics/signin.html