Programação Por Equipe DevBras

Capa do artigo: Login silencioso no OIDC que vira loop de reload: uma tentativa e depois a tela de login

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

  1. Uma renovação em andamento por vez. Se dez chamadas de API percebem o token vencido ao mesmo tempo, todas esperam a mesma promessa.
  2. Classifique o erro pelo código. Interação necessária leva ao login; o resto vira mensagem.
  3. Limite o redirecionamento para o login. Guarde no sessionStorage a hora do último envio; se foi há poucos segundos, pare e mostre erro em vez de redirecionar de novo.
  4. Nunca use location.reload() como tratamento de erro de autenticação. Recarregar repete o mesmo caminho que acabou de falhar.
  5. Registre uma linha por falha, com o código do erro e sem token.
  6. 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_required como 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?

Avalie este artigo

Referência: https://stackoverflow.com/questions/41591371/silent-sign-in-using-oidc-client-with-identity-server-4