O envio de uma senha de uso único (OTP, na sigla em inglês) por SMS é um fluxo comum para verificação de número de telefone, recuperação de conta, autenticação de aumento de privilégios e confirmação de pagamento. Alternar entre um navegador e um app de mensagens SMS para memorizar ou copiar e colar um código de vários dígitos gera atrito para o usuário, aumenta as taxas de desistência e deixa os usuários vulneráveis a phishing em domínios falsificados.
A combinação de marcação semântica <form>, <input autocomplete="one-time-code" inputmode="numeric">, formatação de SMS vinculada à origem (@domain #code) e a API WebOTP (OTPCredential) cria um fluxo de verificação em camadas e com aprimoramento progressivo:
- Extração e envio automáticos: os navegadores compatíveis com a API WebOTP verificam a origem em relação à linha
@domain #codeno SMS, preenchem o<input>e acionam a verificação com um único toque. - Preenchimento automático de teclado vinculado à origem: navegadores e sistemas operacionais compatíveis com
autocomplete="one-time-code"(como o Safari no iOS, iPadOS e macOS) analisam a mensagem@domain #codee sugerem o código na barra de preenchimento automático do teclado apenas quando o domínio corresponde. - Entrada manual sem complicações: os usuários que inserem um código de outro dispositivo recebem um teclado virtual numérico via
inputmode="numeric"sem os riscos de usabilidade de<input type="number">.
Considerações sobre segurança
A OTP por SMS verifica a posse de um número de telefone e oferece uma verificação de segurança conveniente, mas o transporte de SMS e os números de telefone podem estar sujeitos a troca de chip, reciclagem de números e interceptação. Sempre use o preenchimento automático do lado do cliente e a API WebOTP com o formato de SMS @domain #code vinculado à origem para que o navegador e a verificação do SO confirmem se o domínio de destino corresponde antes de sugerir ou enviar o código. Para a autenticação principal resistente a phishing, prefira chaves de acesso a códigos baseados em SMS (consulte Orientação sobre chaves de acesso).
Marcação de formulário semântico
Envolva o controle de verificação em um elemento semântico <form> com method="POST" e um <button type="submit"> explícito. Sempre use um único elemento <input> em vez de dividir os dígitos em várias caixas.
- Use
type="text"cominputmode="numeric": nunca usetype="number"para códigos OTP. Uma senha de uso único é uma string de identificador que pode conter zeros à esquerda (como012345), não uma quantidade contável. O uso detype="number"adiciona botões de incremento/decremento, pode remover zeros à esquerda e permite mudanças acidentais de valor com a roda de rolagem. Evite tambémtype="tel", que é uma solução legada destinada a números de telefone completos. Combinetype="text"cominputmode="numeric"para apresentar um teclado virtual somente com dígitos em dispositivos móveis. - Adicione
autocomplete="one-time-code": instrui os navegadores e sistemas operacionais a sugerir códigos de verificação por SMS recebidos na barra de preenchimento automático quando o<input>estiver em foco. - Coloque dicas de instrução acima do
<input>: associe um<label>visível usandoforeide coloque todas as instruções de formato (vinculadas poraria-describedby) visualmente acima do<input>para que os popovers de preenchimento automático e os teclados virtuais de dispositivos móveis nunca cubram as instruções. - Não divida dígitos em vários elementos
<input>: padrões de OTP com várias caixas (seis entradasmaxlength="1"separadas) interrompem o preenchimento automático nativo deautocomplete="one-time-code"em muitos navegadores, impedem a cópia e colagem e complicam a navegação do leitor de tela. Se um design visual segmentado for necessário, estilize um único<input>comletter-spacinge uma fonte monoespaçada.
<form id="otp-form" action="/verify-otp" method="POST">
<div class="form-group">
<label for="otp-input">Verification code</label>
<!-- Place the hint above the input so autofill popovers and mobile keyboards do not obscure it -->
<p id="otp-format-hint" class="field-hint">
Enter the 6-digit code sent to your phone.
</p>
<!-- MANDATORY: Use a single input with type="text", inputmode="numeric", and autocomplete="one-time-code" -->
<input
type="text"
id="otp-input"
name="otp"
inputmode="numeric"
autocomplete="one-time-code"
pattern="\d{6}"
maxlength="6"
aria-describedby="otp-format-hint"
required>
</div>
<button type="submit">Verify code</button>
</form>
/* Optional: Style a single input to visually space digits without splitting the DOM element */
#otp-input {
font-family: ui-monospace, monospace;
font-size: 1.25rem; /* Example value: must be at least 1rem (16px) to prevent mobile browser auto-zoom */
letter-spacing: 0.35em;
min-height: 48px;
padding: 0.5rem 0.75rem;
box-sizing: border-box;
}
Formato de mensagem SMS vinculado à origem
Para que a API WebOTP e o preenchimento automático de autocomplete="one-time-code" vinculado ao domínio do Safari verifiquem se o código pertence ao seu site, o serviço de SMS de back-end precisa formatar a última linha da mensagem de texto com o domínio de origem prefixado por @ e a OTP prefixada por #:
Your verification code is 123456.
@example.com #123456
Aplique as seguintes regras de formatação à mensagem SMS enviada:
- Inclua primeiro um texto legível por humanos: sempre comece o SMS com uma frase clara e legível por humanos que contenha o código para que os usuários que o digitarem manualmente em outro dispositivo possam ler com facilidade.
- Coloque
@<domain> #<otp>na última linha: o caractere@precisa ser o primeiro da linha final, sem linhas extras ou texto de assinatura depois dele. - Use apenas o domínio hospedado: não inclua um esquema de URL (
https://ouhttp://), uma porta (:8080) ou um caminho (/verify) no token@domain. - Separe os tokens com um único espaço: coloque exatamente um espaço entre
@<domain>e#<otp>e nunca inclua espaços em branco no domínio ou entre#e o código.
Como integrar a API WebOTP ao seu fluxo de verificação
A API WebOTP (OTPCredential) exige um contexto seguro (https:// na produção ou http://localhost durante o desenvolvimento local). Em vez de colar um script DOMContentLoaded desconectado que consulta o DOM e chama form.submit(), integre navigator.credentials.get() diretamente ao ciclo de vida e ao gerenciador de envio da sua interface de verificação:
- Vincule o ciclo de vida da solicitação à etapa ativa da OTP: comece a detectar a OTP quando a entrada de verificação for renderizada e estiver ativa (por exemplo, quando a etapa da OTP for montada depois que o usuário solicitar um código SMS e sempre que o usuário clicar no botão "Reenviar código").
- Gerenciar um
AbortControllerem todos os caminhos de saída: transmita umAbortSignal(signal: ac.signal) paranavigator.credentials.get(). Chame oac.abort()quando:- O usuário envia o formulário manualmente (ou envia um código preenchido por
autocomplete="one-time-code"). - Um novo código SMS é solicitado ("Reenviar código"), cancelando o controlador anterior antes de criar um novo.
- O componente, a caixa de diálogo ou a rota de verificação é desmontado.
- O usuário envia o formulário manualmente (ou envia um código preenchido por
- Acionar o gerenciador de envio do aplicativo (
form.requestSubmit()): quandonavigator.credentials.get()for resolvido comotp, definaotpInput.value = otp.codee chameform.requestSubmit()(ou invoque a função de verificação diretamente). NÃO chameform.submit(), porqueform.submit()ignora os listeners de eventossubmit(manipuladoresevent.preventDefault()/fetch()) e a validação de restrição nativa.
const otpForm = document.getElementById('otp-form');
const otpInput = document.getElementById('otp-input');
let otpAbortController = null;
function stopSmsOtpListener() {
if (otpAbortController) {
otpAbortController.abort();
otpAbortController = null;
}
}
async function startSmsOtpListener() {
// Feature-detect WebOTP support
if (!otpInput || !('OTPCredential' in window)) {
return;
}
// Cancel any previous pending WebOTP request (e.g., when re-sending an SMS code)
stopSmsOtpListener();
const ac = new AbortController();
otpAbortController = ac;
try {
const otp = await navigator.credentials.get({
otp: { transport: ['sms'] },
signal: ac.signal,
});
if (!otp) return;
otpInput.value = otp.code;
// Trigger standard form submission so submit event listeners and validation run
if (otpForm) {
otpForm.requestSubmit();
}
} catch (err) {
// Expected when aborted by manual submission, resend, unmount, or user dismissal
if (err && err.name === 'AbortError') {
return;
}
console.error('WebOTP verification error:', err);
}
}
otpForm.addEventListener('submit', async (event) => {
// MANDATORY: Abort any in-flight WebOTP request when the form is submitted
stopSmsOtpListener();
// Application-specific submission logic (e.g., AJAX fetch or native POST)
event.preventDefault();
const formData = new FormData(otpForm);
// await fetch(otpForm.action, { method: 'POST', body: formData });
});
// Start listening when the OTP verification step becomes active
startSmsOtpListener();
Verificação de iframe entre origens
O recebimento de uma OTP por SMS em um <iframe> entre origens é restrito por padrão e é usado principalmente para fluxos de confirmação de pagamento incorporados (como a verificação 3D Secure, em que shop.example incorpora um desafio do emissor de bank.example). Para oferecer suporte à WebOTP em um <iframe> entre origens, são necessárias três configurações coordenadas:
Vincule as duas origens na última linha do SMS: liste primeiro o domínio de incorporação de nível superior (
@shop.example), seguido por#<otp>, um espaço e o domínio do iframe incorporado (@bank.example):Your verification code is 123456. @shop.example #123456 @bank.exampleEnvie o cabeçalho de resposta HTTP
Permissions-Policy: delegueotp-credentialsà origem do iframe entre origens nos cabeçalhos HTTP do documento pai:Permissions-Policy: otp-credentials=(self "https://bank.example")Adicione
allow="otp-credentials"ao elemento<iframe>: conceda explicitamente a política de permissão na tag<iframe>na página de incorporação e inclua um atributotitledescritivo para leitores de tela:
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
No documento <iframe> veiculado por https://bank.example, execute a mesma marcação <form> e o mesmo ciclo de vida de verificação navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) mostrados acima.
Estratégias de fallback
Substituição da API WebOTP
Se os navegadores de destino não forem compatíveis com a API WebOTP (OTPCredential), não carregue um polyfill de JavaScript de terceiros, porque a leitura de mensagens SMS recebidas requer integração no nível do sistema operacional. Em vez disso, use o aprimoramento progressivo de três níveis integrado à marcação <input>:
- Experiência de aprimoramento progressivo: proteja a chamada
navigator.credentials.get({ otp: { transport: ['sms'] } })por trás deif ('OTPCredential' in window). QuandoOTPCredentialnão está disponível:- Os navegadores compatíveis com
autocomplete="one-time-code"(como o Safari no iOS 14+, iPadOS e macOS com retransmissão de SMS do iCloud) analisam o SMS@domain #codee apresentam o código na barra de preenchimento automático do teclado do sistema quando o<input>está em foco. - Todos os outros navegadores usam a entrada manual no único
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>, exibindo um teclado numérico em dispositivos móveis e enviando pelo<button type="submit">.
- Os navegadores compatíveis com
Substituição da política de permissões
Se o navegador não for compatível com a delegação de otp-credentials usando Permissions-Policy e <iframe allow="otp-credentials">, a verificação incorporada <form> dentro do <iframe> entre origens vai ser degradada de forma otimizada para sugestões de teclado autocomplete="one-time-code" e entrada numérica manual sem gerar exceções não processadas.
Suporte a inputmode e AbortController
O atributo HTML inputmode e o AbortController (AbortSignal) são compatíveis com navegadores mais recentes e não exigem código polyfill ou substituto.