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.
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ério | Magic link | OTP |
|---|---|---|
| Esforço do usuário | Um clique, sem digitar | Ler e digitar 6 dígitos |
| Canal | E-mail (por SMS ou WhatsApp, um link vira isca de phishing) | WhatsApp, SMS ou e-mail |
| Dispositivo | Exige o mesmo: clicar no celular não loga o desktop | Funciona em qualquer dispositivo ou app |
| App nativo | Precisa de deep link ou universal link configurado | Campo de texto; auto-preenchimento com SMS |
| Tempo até logar | Depende da entrega do e-mail: segundos a minutos | WhatsApp e SMS chegam em segundos |
| Phishing | Treina o usuário a clicar em links de e-mail | O código não leva a lugar nenhum; o risco é repassá-lo a alguém |
| Força bruta | Token longo, adivinhação inviável | Código curto, exige limite de tentativas |
| Scanners de e-mail | Podem "clicar" e consumir o link antes da pessoa | Não afetam |
| Custo | E-mail, em geral o canal mais barato | WhatsApp 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.
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:
- 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.
- 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.
- 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.