Programação Por Equipe DevBras

Capa do artigo: Login OIDC em JavaScript que falha na volta: redirect URI, CORS e state por ambiente

Login OIDC em JavaScript que falha na volta: redirect URI, CORS e state por ambiente

Cadastre a URI de retorno exata de cada ambiente, libere o CORS do endpoint de token para a origem certa, sirva a página de callback e evite o erro de state perdido no oidc-client-ts.

Em desenvolvimento, o login do painel funciona. Em homologação, o botão "Entrar" leva para o IdentityServer, a pessoa digita a senha e volta para uma página de erro dizendo que o pedido é inválido. Em produção, a volta acontece, mas o console mostra No matching state found in storage e a tela fica em branco. O código é o mesmo nos três ambientes. A resposta curta: quase toda falha de cliente JavaScript no OIDC está na volta, não no botão, e vem de três configurações que precisam bater com o cliente cadastrado no provedor de identidade: a URI de retorno exata de cada ambiente, a origem liberada no CORS para a troca do código pelo token, e a mesma origem no início e no fim do fluxo, para a biblioteca encontrar o state que guardou.

Se o sintoma é especificamente "erro de rede" no console, o guia erro de rede no oidc-client vai direto ao CORS. Se só falha num navegador, veja login OIDC que falha num browser só. Este guia monta a configuração completa por ambiente, que evita os dois.

O caminho de ida e volta, e onde cada peça quebra

No fluxo Authorization Code com PKCE, que é o recomendado para aplicação no navegador, a biblioteca gera um state e um verificador de código, guarda os dois no storage do navegador e redireciona para o provedor com o redirect_uri configurado. O provedor autentica a pessoa e só redireciona de volta se aquele redirect_uri estiver cadastrado para o cliente, com comparação exata. Na volta, a página de callback lê o code e o state da URL, procura o state no storage e faz um POST para o endpoint de token, do navegador direto para o provedor, o que exige CORS.

Cada etapa tem a sua falha típica:

  • O provedor recusa voltar: o redirect_uri enviado não é idêntico a nenhum cadastrado. No IdentityServer, o log registra o redirect_uri inválido com o valor recebido.
  • A volta cai num 404: a rota /callback existe no Vue Router, mas o servidor web não devolve o index.html para ela.
  • O state não é encontrado: o fluxo começou numa origem e terminou em outra, o storage foi limpo, ou o callback foi processado duas vezes.
  • Erro de rede na troca do token: a origem do app não está liberada no CORS do cliente.

Por que "comparação exata" pega tanta gente

Para o provedor, https://painel.exemplo.com.br/callback, https://painel.exemplo.com.br/callback/ e http://painel.exemplo.com.br/callback são três URIs diferentes. Também são diferentes http://localhost:5173 e http://127.0.0.1:5173, e qualquer troca de porta. Para o navegador, as duas formas de localhost são origens diferentes, cada uma com o seu storage: se o fluxo começa em uma e volta para a outra, o state não está lá.

A tentação é cadastrar curinga para cobrir tudo. Não faça isso. URI de retorno aberta permite que um atacante peça ao provedor para entregar o código de autorização num endereço controlado por ele. A lista exata é a proteção, não a burocracia.

Como configurar por ambiente

  1. Liste as origens reais de cada ambiente: esquema, host e porta. No exemplo, http://localhost:5173, https://homolog.painel.exemplo.com.br e https://painel.exemplo.com.br.
  2. Prefira um cliente por ambiente no provedor. O cliente de produção não deve aceitar retorno para localhost.
  3. Cadastre a URI de retorno, a de pós-logout e a origem do CORS de cada cliente, exatamente como o navegador vai usar.
  4. Derive o redirect_uri da origem atual no front, em vez de escrever o endereço fixo em cada build.
  5. Configure o servidor web para devolver o index.html na rota de callback.
  6. Processe o callback uma única vez e troque a URL depois, para recarregar a página não repetir o processamento.
  7. Teste numa janela anônima em cada ambiente, sem storage antigo nem extensões.

O cliente no IdentityServer

new Client
{
    ClientId = "painel-spa-homolog",
    ClientName = "Painel (homologação)",
    AllowedGrantTypes = GrantTypes.Code,
    RequirePkce = true,
    RequireClientSecret = false,

    RedirectUris = { "https://homolog.painel.exemplo.com.br/callback" },
    PostLogoutRedirectUris = { "https://homolog.painel.exemplo.com.br/" },
    AllowedCorsOrigins = { "https://homolog.painel.exemplo.com.br" },

    AllowedScopes = { "openid", "profile", "api.pedidos" }
}

Repare que AllowedCorsOrigins recebe a origem, sem caminho e sem barra no fim, enquanto RedirectUris recebe a URI completa. RequireClientSecret = false porque código no navegador não guarda segredo; a proteção da troca vem do PKCE. Os clientes de desenvolvimento e produção seguem o mesmo molde, cada um com as suas URIs.

O front com oidc-client-ts

// src/auth.js
import { UserManager } from 'oidc-client-ts';

const origem = window.location.origin;

export const userManager = new UserManager({
  authority: import.meta.env.VITE_OIDC_AUTHORITY,
  client_id: import.meta.env.VITE_OIDC_CLIENT_ID,
  redirect_uri: origem + '/callback',
  post_logout_redirect_uri: origem + '/',
  response_type: 'code',
  scope: 'openid profile api.pedidos'
});

let callbackEmAndamento = null;

export function concluirLogin() {
  if (!callbackEmAndamento) {
    callbackEmAndamento = userManager.signinRedirectCallback();
  }
  return callbackEmAndamento;
}
// componente da rota /callback
onMounted(async () => {
  try {
    await concluirLogin();
    router.replace('/');
  } catch (erro) {
    console.error('Falha ao concluir login:', erro.message);
    mensagem.value = 'Não foi possível concluir o login. Tente entrar de novo.';
  }
});

O redirect_uri derivado de window.location.origin garante que o fluxo volta para a mesma origem em que começou. O que muda entre ambientes, a autoridade e o id do cliente, vem das variáveis do Vite. A promessa guardada em callbackEmAndamento impede o segundo processamento, comum quando o componente é montado duas vezes; o segundo signinRedirectCallback não acharia mais o state, que é removido depois do primeiro uso. O router.replace tira o code e o state da barra de endereço.

O servidor web devolvendo o callback

Com o Vue Router em modo histórico, /callback não é um arquivo. No nginx, a regra comum é a abaixo; no IIS ou em outros servidores, o equivalente é uma regra de reescrita para o index.html.

location / {
    try_files $uri $uri/ /index.html;
}

Como diagnosticar quando ainda falha

Abra o DevTools com a aba Network em modo de preservar log e faça o login inteiro. Na requisição para o endpoint de autorização, copie o parâmetro redirect_uri e compare caractere por caractere com o cadastrado. Na volta, confira se a URL tem code e state, ou error e error_description, que dizem o motivo. Na requisição POST para o endpoint de token, veja se ela falhou por CORS. E no log do provedor, procure a entrada do mesmo horário: ele quase sempre diz qual configuração não bateu. Para entender quem é o provedor e o que ele resolve, veja IdentityServer não é login caseiro.

Erros comuns

  • Cadastrar uma URI só e usar em todos os ambientes. Homologação e produção recebem erro de redirect inválido depois da senha.
  • Barra no fim, porta ou esquema diferente do cadastrado. A comparação exata recusa, e o erro parece mistério porque "é a mesma URL".
  • Curinga ou lista aberta de retorno. O código de autorização pode ser entregue a um endereço controlado por outra pessoa.
  • Esquecer o CORS da origem do app. A troca do código pelo token falha como erro de rede, e a culpa vai para a biblioteca.
  • Começar em localhost e voltar em 127.0.0.1. São origens diferentes, com storages diferentes, e o state não é encontrado.
  • Processar o callback duas vezes ou não ter rota no servidor. O segundo processamento não acha o state, e a falta de reescrita devolve 404.

Checklist

  • Cada ambiente tem o seu cliente, ou pelo menos as suas URIs exatas cadastradas.
  • O cliente de produção não aceita retorno para localhost.
  • URI de retorno, pós-logout e origem de CORS estão cadastradas para cada ambiente.
  • O redirect_uri do front é derivado da origem atual.
  • A rota de callback devolve o index.html no servidor web.
  • O callback é processado uma vez e a URL é limpa depois.
  • O login foi testado numa janela anônima em cada ambiente.

Copie o redirect_uri da última requisição de login em homologação e compare com o que está cadastrado no provedor: os dois são idênticos, inclusive na barra final e na porta?

Avalie este artigo

Referência: https://docs.identityserver.io/en/latest/quickstarts/4_javascript_client.html