Magic link ou OTP: qual escolher para o login sem senha

Comparativo direto entre magic link e código OTP para login sem senha: UX, segurança, custo e mobile, com os cenários em que cada um vence e como combiná-los.

Capa do artigo Magic link ou OTP: qual escolher para o login sem senha

Tirar a senha do login é uma das decisões de produto com melhor retorno: menos esquecimento, menos tickets de suporte, menos credenciais reutilizadas e vazadas. A dúvida que aparece logo em seguida é magic link ou OTP: enviar um link que autentica com um clique ou um código numérico que a pessoa digita?

Os dois provam a mesma coisa, que a pessoa controla um e-mail ou um telefone, mas se comportam de forma bem diferente na prática: no celular, dentro de um app nativo, em quem usa e-mail corporativo com filtros agressivos e no custo por envio.

Abaixo, o funcionamento de cada um, um comparativo direto, os cenários em que cada opção vence, como combinar as duas e um resumo de implementação com código.

Como funciona cada um

Magic link

O servidor gera um token aleatório longo, guarda o hash dele com prazo de validade e envia por e-mail um link que carrega esse token. Ao clicar, o navegador abre uma rota que confere o token, invalida-o e cria a sessão. A pessoa não digita nada.

OTP (código de uso único)

O servidor gera um código curto, em geral de 6 dígitos, e envia por WhatsApp, SMS ou e-mail. A pessoa digita o código na tela em que já está. O servidor compara, respeita um limite de tentativas e, se conferir, cria a sessão. Os detalhes de geração e validação estão em o que é OTP e como funciona o código de verificação.

A diferença central está em onde a autenticação termina. No magic link, ela termina no dispositivo que abriu o link. No OTP, termina no dispositivo que estava na tela de login, seja ele qual for.

Magic link ou OTP: comparativo de UX, segurança, custo e mobile

CritérioMagic linkOTP
Esforço do usuárioUm clique, sem digitarLer e digitar 6 dígitos
CanalE-mail (por SMS ou WhatsApp, um link vira isca de phishing)WhatsApp, SMS ou e-mail
DispositivoExige o mesmo: clicar no celular não loga o desktopFunciona em qualquer dispositivo ou app
App nativoPrecisa de deep link ou universal link configuradoCampo de texto; auto-preenchimento com SMS
Tempo até logarDepende da entrega do e-mail: segundos a minutosWhatsApp e SMS chegam em segundos
PhishingTreina o usuário a clicar em links de e-mailO código não leva a lugar nenhum; o risco é repassá-lo a alguém
Força brutaToken longo, adivinhação inviávelCódigo curto, exige limite de tentativas
Scanners de e-mailPodem "clicar" e consumir o link antes da pessoaNão afetam
CustoE-mail, em geral o canal mais baratoWhatsApp e SMS custam por envio; OTP por e-mail custa o mesmo que o link

Dois pontos da tabela merecem destaque. O primeiro é o dispositivo: o magic link resolve bem o caso "estou no navegador, abro o e-mail na mesma máquina", mas quebra quando a pessoa lê o e-mail no celular e esperava entrar no desktop. O segundo são os scanners corporativos: ferramentas de segurança de e-mail abrem links para inspecioná-los, e um link de uso único pode ser consumido sem que a pessoa tenha clicado.

Infográfico comparando magic link e OTP no login sem senha: magic link autentica com um clique mas exige o mesmo dispositivo; OTP funciona em qualquer app e chega em segundos pelo WhatsApp; combine os dois conforme o contexto

Quando o magic link vence

O magic link brilha quando o e-mail já é a identidade do produto e o uso é, na maior parte, em desktop:

  • SaaS e ferramentas B2B em que o login é ocasional e a pessoa está no computador com o e-mail aberto ao lado.
  • Convites e primeiro acesso: o link leva direto para a tela certa, já autenticado, sem pedir que a pessoa crie nada.
  • Produtos de baixo risco e baixo volume, como newsletters pagas, painéis de leitura e áreas de membros.
  • Equipes que não querem lidar com telefone: sem número, sem custo por envio, sem exposição a SMS pumping.

Há uma ressalva de implementação: não consuma o token no GET da rota. Faça a página do link exibir um botão "Confirmar login" que envia um POST. Assim, um scanner que abre o link não invalida o token, e a pessoa ainda consegue entrar.

Quando o OTP vence

O OTP vence sempre que o fluxo acontece no celular ou precisa ser rápido, o que, no Brasil, costuma significar WhatsApp:

  • Apps mobile: o código chega como notificação, o campo aceita colar ou preencher automaticamente, e a pessoa nunca sai do app.
  • Checkout e cadastro com telefone: o número já é o identificador, e o código via WhatsApp chega em segundos.
  • Login em dispositivo diferente do canal: a pessoa lê o código no celular e digita no desktop, na TV ou no totem.
  • Público com e-mail corporativo filtrado: nada de link para scanner consumir nem de mensagem presa em quarentena.
  • Alto volume com conversão medida: cada segundo de espera e cada troca de aplicativo custa cadastro.

O OTP também é a base natural do passwordless no WhatsApp, que combina a identidade do telefone com um canal que o usuário já abre dezenas de vezes por dia. O fluxo completo está em login sem senha com código pelo WhatsApp.

O preço dessa escolha é a disciplina de segurança: um código de 6 dígitos só é seguro com poucas tentativas por código, cooldown no reenvio e limites por telefone. Quem usa uma API de OTP recebe esses controles prontos; quem implementa por conta própria precisa construí-los antes de ir para produção.

Como combinar os dois sem complicar

Na prática, poucos produtos precisam decidir entre magic link ou OTP de forma exclusiva. Três combinações funcionam bem:

  1. Escolha pela identidade digitada. Se a pessoa informou e-mail, envie magic link; se informou telefone, envie OTP por WhatsApp com fallback para SMS. A tela de entrada é a mesma, e a lógica muda no servidor.
  2. Link e código na mesma mensagem. O e-mail traz o botão para quem está no mesmo dispositivo e um código de 6 dígitos para quem está em outro. Se um scanner consumir o link, o código continua válido. Coloque o código no assunto para ler na notificação.
  3. Magic link no desktop, OTP no app. O produto web usa link; o aplicativo usa código, já que deep links adicionam fricção e pontos de falha.

Para que as combinações não virem três sistemas diferentes, modele uma única tabela de "desafios de login" com tipo (magic_link ou otp), hash, prazo, tentativas e estado. A emissão da sessão depois do sucesso é idêntica nos dois casos.

Implementação resumida

Os dois fluxos cabem em poucas linhas quando a lógica de desafio é compartilhada. Primeiro, o magic link em Node.js:

// magic-link.js — gerar, enviar e confirmar
import crypto from "node:crypto";

const hash = (t) => crypto.createHash("sha256").update(t).digest("hex");

export async function enviarMagicLink(usuario) {
  const token = crypto.randomBytes(32).toString("base64url"); // 256 bits de entropia
  await db.desafios.criar({
    usuarioId: usuario.id,
    tipo: "magic_link",
    hash: hash(token),
    expiraEm: Date.now() + 15 * 60_000,
  });
  await enviarEmail(usuario.email, `https://app.exemplo.com.br/login/confirmar?t=${token}`);
}

// a página do link mostra um botão que faz POST; o GET não consome nada
app.post("/login/confirmar", async (req, res) => {
  const desafio = await db.desafios.consumir(hash(String(req.body.t))); // apaga e retorna, em transação
  if (!desafio || desafio.expiraEm < Date.now()) return res.status(400).json({ erro: "link_invalido" });
  await criarSessao(res, desafio.usuarioId);
  res.json({ ok: true });
});

Com OTP, a parte difícil (gerar, entregar, expirar e contar tentativas) pode ficar com uma API. O exemplo abaixo usa a aut2fa, que escolhe o canal entre os permitidos na aplicação e devolve só o id do envio, nunca o código:

// otp.js — enviar e verificar com a chave guardada no servidor
import crypto from "node:crypto";

const API = "https://api.aut2fa.com";
const headers = {
  Authorization: `Bearer ${process.env.AUT2FA_API_KEY}`,
  "Content-Type": "application/json",
};

export async function enviarOtp(telefone) {
  const r = await fetch(`${API}/v1/send`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
    body: JSON.stringify({ to: telefone, locale: "pt-BR" }),
  });
  if (r.status === 429) throw new Error(`Aguarde ${r.headers.get("Retry-After")} s`);
  const { id, expires_at } = await r.json(); // guarde o id na sessão de login
  return { id, expires_at };
}

export async function verificarOtp(sendId, codigo) {
  const r = await fetch(`${API}/v1/send/${sendId}/verify`, {
    method: "POST",
    headers,
    body: JSON.stringify({ code: String(codigo).trim() }),
  });
  const { valid, verification, attempts_left } = await r.json();
  return { valid, verification, attempts_left }; // prossiga só se valid === true
}

Se o seu canal principal for SMS com outro provedor, o guia de verificação por SMS OTP com Node.js mostra o mesmo desenho com envio e validação feitos por você, incluindo o armazenamento do código.

Nos dois fluxos, o passo final é igual: emitir a sessão. Em APIs com token, o padrão de JWT com access e refresh token resolve bem, com access token de vida curta e refresh token em cookie httpOnly. Nunca crie a sessão antes de o desafio ser consumido com sucesso.

Alguns cuidados valem para ambos: token e código nunca aparecem em logs; o desafio é apagado na mesma transação em que é aceito; a resposta ao pedido de envio é idêntica exista ou não a conta, para não revelar quem é cliente.

Perguntas frequentes

Magic link é mais seguro que OTP?

Depende do ataque. Contra adivinhação, o magic link vence por ter um token longo. Contra phishing, o OTP vence porque não ensina o usuário a clicar em links. Na prática, um OTP com limite de 5 tentativas e cooldown é tão seguro quanto um link bem implementado.

Posso mandar magic link por WhatsApp ou SMS?

Tecnicamente sim, mas é má ideia. Links em mensagens de texto são o formato preferido de golpes, e muitos usuários aprenderam a não clicar. Nesses canais, envie código; reserve o link para o e-mail.

Quanto tempo o link ou o código devem valer?

Link: 10 a 15 minutos, por causa dos atrasos de entrega do e-mail. OTP por WhatsApp ou SMS: alguns minutos bastam, já que a mensagem chega em segundos. Em qualquer caso, só um desafio ativo por usuário e uso único.

O usuário precisa ter conta antes de receber o link ou o código?

Não necessariamente. Muitos produtos unem cadastro e login: se o e-mail ou telefone não existe, a conta é criada depois da confirmação. Só cuide para que a resposta ao envio seja a mesma nos dois casos.

Como lidar com scanners de e-mail que abrem o link?

Não consuma o token no GET. Mostre uma página com um botão de confirmação que faz POST, ou inclua um código de 6 dígitos na mesma mensagem como alternativa.

Conclusão

A resposta para magic link ou OTP depende de onde o usuário está: link para quem vive no e-mail e no desktop; código para quem vive no celular e precisa de velocidade. Como a maioria dos produtos tem os dois públicos, a resposta madura costuma ser combinar: link quando a identidade é o e-mail, código quando é o telefone, e a mesma tabela de desafios por baixo.

Para testar o lado do OTP sem custo, uma aplicação em modo de teste na aut2fa aceita o código 000000 e devolve o mesmo contrato da produção, o que permite fechar o fluxo inteiro antes de gastar o primeiro envio.

Continue lendo