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
- Configure cookie para a sessão do aplicativo e OpenID Connect para o desafio.
- Use
ResponseType = "code"com PKCE. - Guarde o segredo do cliente fora do código, em configuração protegida.
- Aponte
AccessDeniedPathpara uma página própria, para o caso em que a pessoa recusa ou cancela. - Trate qualquer outra falha no retorno com
OnRemoteFailure, registrando o motivo sem token. - 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?
Referência: https://identityserver4.readthedocs.io/en/latest/topics/signin.html