Login silencioso no OIDC que vira loop de reload: uma tentativa e depois a tela de login
Faça a renovação silenciosa da sessão tentar uma vez, trate login_required indo para o login visível, impeça loops de redirecionamento e entenda por que o iframe falha quando o navegador bloqueia cookies de terceiros.
Segunda, 8h05. A equipe de atendimento abre o painel depois do fim de semana e vê a tela piscar: carrega, recarrega, carrega de novo. No provedor de identidade, o log mostra centenas de requisições por minuto vindas do mesmo grupo de IPs. A sessão no provedor tinha expirado no sábado, o login silencioso falhava, e o código de tratamento recarregava a página, que tentava o login silencioso de novo. A resposta curta: login silencioso é uma tentativa, não uma estratégia de retry. Tente renovar uma vez; se o provedor responder login_required ou equivalente, mande a pessoa para a tela de login visível uma única vez, com mensagem; se o erro for outro, mostre o erro e registre o motivo, sem recarregar nada.
Este guia parte de um cliente OIDC já configurado. Se o login normal ainda falha na volta, resolva primeiro com o guia de redirect certo no cliente OIDC.
O que é o login silencioso e por que ele falha
O access token de uma aplicação no navegador tem vida curta, por exemplo uma hora. Para não pedir senha a cada hora, a biblioteca tenta obter um token novo sem mostrar nada. O método clássico abre um iframe invisível apontando para o endpoint de autorização com prompt=none. Se a pessoa ainda tem sessão no provedor, ele responde com um código novo no iframe, e a biblioteca troca pelo token. Se não tem, o provedor não pode mostrar a tela de login dentro de um iframe invisível, e responde com um erro definido pela especificação do OpenID Connect: login_required, interaction_required ou consent_required.
Esses erros não são falha técnica; são a resposta correta dizendo "precisa da pessoa". Nenhum retry muda isso. Repetir só gera carga no provedor e uma tela que nunca para.
Há um segundo motivo de falha que cresceu nos últimos anos. O iframe aponta para o domínio do provedor, que é outro site em relação ao seu app. Para o iframe saber que a pessoa está logada, o navegador precisa enviar o cookie de sessão do provedor num contexto de terceiro. Navegadores com proteção contra rastreamento, como Safari e Firefox, bloqueiam ou isolam esses cookies por padrão, e o resultado é login_required mesmo com a pessoa logada no provedor em outra aba. Por isso a renovação por iframe é cada vez menos confiável, e muitos provedores e bibliotecas passaram a usar refresh token na renovação de aplicações no navegador.
As três respostas possíveis e o que fazer com cada uma
- Sucesso: token novo, a aplicação segue sem a pessoa perceber.
- Precisa de interação (
login_required,interaction_required,consent_required): leve para o login visível, uma vez, preservando a página em que a pessoa estava. - Qualquer outro erro (timeout, redirect inválido, iframe que não carrega): é configuração ou rede. Mostre uma mensagem, registre o motivo e não tente de novo em loop. Redirect errado não se cura com retry.
Como implementar sem loop
- Uma renovação em andamento por vez. Se dez chamadas de API percebem o token vencido ao mesmo tempo, todas esperam a mesma promessa.
- Classifique o erro pelo código. Interação necessária leva ao login; o resto vira mensagem.
- Limite o redirecionamento para o login. Guarde no
sessionStoragea hora do último envio; se foi há poucos segundos, pare e mostre erro em vez de redirecionar de novo. - Nunca use
location.reload()como tratamento de erro de autenticação. Recarregar repete o mesmo caminho que acabou de falhar. - Registre uma linha por falha, com o código do erro e sem token.
- Se o provedor permitir, use refresh token para a renovação de aplicações no navegador, com rotação, e mantenha o iframe só como alternativa.
Código com oidc-client-ts
// src/sessao.js
import { userManager } from './auth';
const PRECISA_INTERACAO = ['login_required', 'interaction_required', 'consent_required'];
let renovacao = null;
export function renovarSessao() {
if (!renovacao) {
renovacao = userManager.signinSilent().finally(() => { renovacao = null; });
}
return renovacao;
}
function irParaLoginUmaVez() {
const ultimo = Number(sessionStorage.getItem('ultimoEnvioLogin') || 0);
if (Date.now() - ultimo < 30000) {
return { erro: 'Não foi possível manter a sessão. Feche a aba e entre novamente.' };
}
sessionStorage.setItem('ultimoEnvioLogin', String(Date.now()));
return userManager.signinRedirect({ state: { volta: window.location.pathname } });
}
export async function garantirSessao() {
const user = await userManager.getUser();
if (user && !user.expired) return user;
try {
return await renovarSessao();
} catch (erro) {
const codigo = erro && erro.error;
if (PRECISA_INTERACAO.includes(codigo)) return irParaLoginUmaVez();
console.warn('Renovação silenciosa falhou:', codigo || (erro && erro.message));
return { erro: 'Falha ao renovar a sessão. Avise o suporte se continuar.' };
}
}
O state passado ao signinRedirect volta no usuário depois do login, e a página de callback pode usá-lo para devolver a pessoa onde ela estava. Ao concluir o login com sucesso, limpe a chave ultimoEnvioLogin. A janela de 30 segundos é um exemplo: o importante é existir um freio. Se você usa a renovação por iframe, a página indicada em silent_redirect_uri precisa existir, ser leve e chamar signinSilentCallback(); ela também precisa estar cadastrada como URI de retorno no provedor.
Renovação automática sem martelar o provedor
Se a renovação automática da biblioteca estiver ligada, ela tenta renovar pouco antes de o token vencer. Ligue um ouvinte de erro para saber quando ela falhou, e não tente de novo por conta própria:
userManager.events.addSilentRenewError((erro) => {
console.warn('Renovação automática falhou:', erro.message);
avisoSessao.value = 'Sua sessão vai expirar. Salve o que estiver fazendo.';
});
Avisar antes é melhor que perder o formulário preenchido. A pessoa salva, e o próximo request que receber 401 leva ao login de forma controlada.
O que olhar no lado do servidor
No log do provedor, uma rajada de requisições de autorização com prompt=none do mesmo cliente é o sintoma do loop; vale um alerta para isso. Na sua API, o token vencido deve resultar em 401 limpo, que o front trata com a função acima, e não em 500 ou em redirecionamento HTML que o fetch não entende. E lembre que o login silencioso não substitui o encerramento de sessão: sair precisa encerrar a sessão no provedor também, senão a próxima renovação silenciosa loga a pessoa de volta sem ela querer. O guia sessão que revoga trata disso.
Erros comuns
- Recarregar a página quando a renovação falha. O mesmo caminho falha de novo, a tela pisca sem parar e o provedor recebe uma rajada.
- Tratar
login_requiredcomo erro temporário. Retry não cria sessão; só adia a tela de login e gera carga. - Uma renovação por chamada de API. Dez requisições simultâneas abrem dez iframes ou dez trocas de token.
- Esconder erro de configuração no loop. Redirect inválido aparece como "sessão instável" e ninguém corrige a causa.
- Contar com o iframe em todos os navegadores. Com cookies de terceiros bloqueados, a renovação falha para quem usa Safari ou Firefox, mesmo logado.
- Logar o token ao registrar a falha. A credencial vai para o log, que tem retenção longa e acesso amplo.
Checklist
- Com a sessão do provedor expirada, o app vai para o login uma vez e para ali.
- Existe um freio que impede dois redirecionamentos para o login em poucos segundos.
- Só uma renovação roda por vez, compartilhada pelas chamadas simultâneas.
- Erros que não são de interação aparecem como mensagem e linha de log, sem retry.
- A renovação foi testada no Safari e no Firefox, não só no Chrome.
- O provedor tem alerta para rajada de requisições com
prompt=none. - Nenhum log de falha de renovação contém token.
Expire a sessão do provedor num ambiente de teste e abra o seu app: ele para numa tela de login, ou fica recarregando até alguém fechar a aba?
Referência: https://stackoverflow.com/questions/41591371/silent-sign-in-using-oidc-client-with-identity-server-4