Login OIDC que só falha no Edge: redirect, storage e cookie, nessa ordem
Quando o login OIDC funciona no Chrome e quebra em outro navegador, o culpado quase sempre é a URI de retorno, o storage de outra origem ou um cookie bloqueado. Veja como provar qual.
Segunda de manhã, o suporte abre chamado: "no Edge o painel não entra". No Chrome do desenvolvedor, tudo certo. No Edge da gerente, a tela volta do provedor de identidade para /callback e para em No matching state found in storage, ou entra e derruba a sessão uma hora depois. A resposta curta: navegador não é "incompatível" com OIDC. Quando só um falha, quase sempre é uma de três coisas, nesta ordem de probabilidade: a URI de retorno não é exatamente a cadastrada, o estado do login foi salvo numa origem e lido em outra, ou um cookie foi bloqueado pela proteção de rastreamento.
Antes de trocar de biblioteca ou abrir issue, reproduza no navegador que falha com as ferramentas de rede abertas. Em dez minutos você sabe qual dos três é. O resto deste guia mostra como, usando um caso que atravessa o texto: o painel https://painel.lojaexemplo.com.br, um SPA que autentica em https://id.provedor-exemplo.com com Authorization Code e PKCE.
Por que um login OIDC depende tanto do navegador
O fluxo de login num SPA é uma viagem de ida e volta. Antes de sair, a biblioteca gera state, nonce e o code_verifier do PKCE e guarda isso no storage do navegador. Depois redireciona para o provedor, que autentica a pessoa e devolve para a redirect_uri com um código. Na volta, a biblioteca procura o estado salvo, confere e troca o código por tokens.
Cada pedaço dessa viagem tem uma regra que o navegador aplica sozinho:
- A URI de retorno é comparada pelo provedor caractere por caractere com a cadastrada. Barra no final,
httpem vez dehttps,localhostem vez de127.0.0.1: tudo isso é outra URI. - O storage é por origem.
localStorageesessionStoragedehttps://painel.lojaexemplo.com.brnão são vistos porhttps://www.painel.lojaexemplo.com.br. Se o login sai de uma origem e volta em outra, o estado "some". É exatamente a mensagem de estado não encontrado. - Cookies de terceiro são tratados de forma diferente por cada navegador e por cada configuração. A renovação silenciosa clássica abre o provedor num iframe escondido, e ali o cookie de sessão do provedor é um cookie de terceiro. Com bloqueio de rastreamento mais rígido, o iframe não vê sessão nenhuma e o provedor responde
login_required. - SameSite decide se um cookie da sua aplicação vai junto quando o provedor devolve a pessoa. Os navegadores baseados em Chromium tratam cookie sem atributo
SameSitecomoLax, eLaxnão acompanha um POST vindo de outro site, que é o que acontece comresponse_mode=form_post.
O Edge de hoje usa o mesmo motor do Chrome, então a diferença raramente é o motor. É a configuração: prevenção de rastreamento, janela InPrivate, extensões, política corporativa aplicada pela TI. A máquina da gerente não é a sua.
Como descobrir qual das três causas é
- Abra as ferramentas de desenvolvedor no navegador que falha, aba Rede, com "Preserve log" ligado. Sem isso, os redirecionamentos apagam a evidência.
- Clique em entrar e ache a requisição para
/authorizedo provedor. Copie o parâmetroredirect_urie compare com o cadastrado no cliente do provedor. Cole os dois num editor, um embaixo do outro. Diferença de uma barra já explica o erro. - Compare a origem de saída com a de volta. Se a pessoa abriu
http://painel.lojaexemplo.com.bre o retorno cadastrado éhttps://, o estado foi salvo num storage e procurado em outro. Force HTTPS no servidor antes de o SPA carregar. - Olhe a requisição do callback e os cookies dela. No Chromium, a aba de cookies de cada requisição mostra cookies bloqueados e o motivo. Um aviso de SameSite ali resolve a investigação.
- Se o login funciona e cai depois, filtre a aba Rede pelo iframe de renovação (geralmente uma URL
silent-renewou similar) e veja a resposta.login_requirednesse iframe, com a pessoa logada na aba principal, é cookie de terceiro bloqueado. - Repita em janela InPrivate e com a prevenção de rastreamento no modo Estrito. É assim que parte dos usuários navega. Se falha lá, vai falhar com eles.
A configuração do cliente que aguenta os três problemas
No painel do exemplo, a correção teve três partes: uma única URI de retorno em HTTPS, renovação por refresh token em vez de iframe, e uma saída digna quando a renovação falha. Com oidc-client-ts, o sucessor mantido do antigo oidc-client-js, fica assim:
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
export const userManager = new UserManager({
authority: 'https://id.provedor-exemplo.com',
client_id: 'painel-loja',
redirect_uri: 'https://painel.lojaexemplo.com.br/callback',
post_logout_redirect_uri: 'https://painel.lojaexemplo.com.br/',
response_type: 'code',
scope: 'openid profile offline_access',
automaticSilentRenew: true,
userStore: new WebStorageStateStore({ store: window.sessionStorage })
});
userManager.events.addSilentRenewError(async (erro) => {
console.warn('Renovação da sessão falhou:', erro.message);
await userManager.removeUser();
document.getElementById('aviso-sessao').hidden = false;
});
O escopo offline_access pede refresh token. Quando o provedor entrega um, a biblioteca renova pelo endpoint de token, sem iframe, e o bloqueio de cookie de terceiro deixa de importar. Se o seu provedor não entrega refresh token para SPA, a renovação volta a depender do iframe, e a saída é aceitar que ela pode falhar e pedir login de novo com um aviso claro, não com tela em branco.
A página de callback é curta e precisa tratar erro:
import { userManager } from './auth.js';
userManager.signinRedirectCallback()
.then(() => window.location.replace('/'))
.catch((erro) => {
console.error('Falha no retorno do login:', erro);
document.body.textContent = 'Não foi possível concluir o login. Tente entrar de novo.';
});
E se o backend usa cookie de sessão
Se o login é feito pelo servidor (ASP.NET Core, Node com sessão), o cookie de correlação que o servidor grava antes de mandar a pessoa ao provedor precisa voltar no callback. Com form_post, ele só volta se for SameSite=None, e SameSite=None só é aceito com Secure, ou seja, com HTTPS. Os middlewares atuais já fazem isso; o problema aparece quando alguém sobrescreve a política de cookies para o site inteiro ou serve o app em HTTP atrás de um proxy que não repassa o esquema. O guia de cookie HttpOnly, Secure e SameSite explica cada atributo.
Erros comuns
- Cadastrar a URI de desenvolvimento e esquecer a de produção (ou o contrário). O provedor recusa o retorno e a pessoa vê uma página de erro do provedor, não da sua aplicação.
- Servir o SPA em HTTP e HTTPS ao mesmo tempo. Metade dos acessos salva o estado numa origem e volta na outra, e o erro parece aleatório.
- Depender do iframe de renovação silenciosa como se ele sempre funcionasse. Com bloqueio de cookie de terceiro, a sessão cai a cada expiração do token, e só em alguns navegadores.
- Forçar
SameSite=Laxem todos os cookies do site por recomendação genérica de segurança, sem olhar o callback do login. O resultado é "correlation failed" no retorno. - Trocar de biblioteca antes de olhar a aba Rede. A nova biblioteca recebe a mesma URI errada e falha do mesmo jeito.
- Testar só no seu navegador, com a sua sessão do provedor aberta. Você nunca vê o caminho que a pessoa nova percorre.
Checklist
- A
redirect_urienviada no/authorizeé idêntica, caractere por caractere, à cadastrada no provedor. - O app só responde em HTTPS e redireciona HTTP para HTTPS antes de carregar o JavaScript.
- Existe uma única origem canônica (com ou sem
www, não as duas). - A renovação usa refresh token quando o provedor permite; se usa iframe, a falha tem tratamento visível.
- O callback trata erro e mostra mensagem, em vez de tela em branco.
- O fluxo foi testado em janela InPrivate, com prevenção de rastreamento no modo Estrito, sem sessão prévia no provedor.
- A biblioteca de OIDC é mantida e está numa versão recente.
Se o problema é especificamente a URI de retorno no cliente JavaScript, o guia de redirect certo no cliente OIDC aprofunda. Se a mensagem fala em erro de rede, veja erro de rede no oidc-client. E para o desenho do fluxo em si, Authorization Code com PKCE no navegador.
Abra agora o navegador que falha, faça login com a aba Rede gravando e copie o redirect_uri da requisição: ele é exatamente igual ao que está cadastrado no cliente do seu provedor?
Referência: https://github.com/IdentityModel/oidc-client-js/issues/486