Autenticação em React: login com MFA em um app React

Um cliente, um hook de sessão, um formulário de login que lida com segundo fator e uma rota protegida, em cerca de cem linhas.

Gleisson MatosFundador e CEO2 min de leitura
Nesta página

Este guia adiciona autenticação a um app React existente com @nulvless/nulvless-js. No final você terá uma sessão que sobrevive a recargas, um formulário de login que lida com autenticação multifator e uma rota que só renderiza para usuários autenticados.

Tudo aqui roda no navegador com a chave publicável. Para checagens de permissão que o usuário não pode pular, combine com a sua própria API, como descrito no guia de Node.

Construa com seu agente

Um prompt pronto para Claude Code, Cursor, ChatGPT ou qualquer agente de código. Ele aponta o agente para este guia e para as docs, para que siga a API real em vez de adivinhar.

Adicione login com autenticação multifator ao meu app React usando a Nulvless.

Siga estas páginas. Elas documentam a API real, então não invente nomes de métodos:
- https://www.nulvless.com/pt/llms.mdx/blog/react-sign-in-with-mfa/content.md
- https://www.nulvless.com/pt/llms.mdx/docs/frameworks/react/content.md
- https://www.nulvless.com/pt/llms.mdx/docs/sdk/auth/content.md

Requisitos:
- Instale @nulvless/nulvless-js e crie um único cliente em src/lib/nulvless.ts. Leia a URL do emissor e a chave publicável de VITE_NULVLESS_URL e VITE_NULVLESS_KEY.
- Acompanhe a sessão com um hook useSession que escuta os eventos signed-in, signed-out e unauthorized.
- O formulário de login precisa tratar data.challenge e mostrar uma etapa de código que chama auth.verifyChallenge.
- Envolva as rotas privadas em um componente Protected.
- Só a chave publicável pode ir para o navegador. Nunca coloque uma chave secreta no código do frontend.

Antes de começar, me peça a URL do emissor e a chave publicável do meu ambiente develop.
ou abra no

Antes de começar

Você precisa de um projeto no console e da URL do emissor e da chave publicável do ambiente em que vai trabalhar. Use develop enquanto desenvolve: é um armazenamento separado, então os usuários de teste criados aqui nunca chegam à produção.

npm install @nulvless/nulvless-js

Crie um único cliente

Coloque o cliente em um módulo próprio e importe em todo lugar. A chave publicável é segura no bundle: ela só faz o que o usuário autenticado já poderia fazer.

src/lib/nulvless.ts
import { createClient, Storage } from '@nulvless/nulvless-js';

export const nulvless = createClient(
  import.meta.env.VITE_NULVLESS_URL,
  import.meta.env.VITE_NULVLESS_KEY,
  { storage: Storage.Local, trackUser: true },
);

Storage.Local mantém as pessoas conectadas entre visitas. trackUser permite que a plataforma registre o dispositivo e a localização de cada sessão, que é o que o usuário vê depois na lista de sessões ativas.

Acompanhe a sessão com um hook

O SDK emite eventos sempre que a autenticação muda em qualquer parte do app, então um único hook mantém o React sincronizado.

src/lib/use-session.ts
import { useEffect, useState } from 'react';
import type { Session } from '@nulvless/nulvless-js';
import { nulvless } from './nulvless';

export function useSession() {
  const [session, setSession] = useState<Session | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    nulvless.auth.getSession().then(({ data }) => {
      setSession(data);
      setLoading(false);
    });

    const offIn = nulvless.auth.on('signed-in', () => {
      nulvless.auth.getSession().then(({ data }) => setSession(data));
    });
    const offOut = nulvless.auth.on('signed-out', () => setSession(null));
    const offDead = nulvless.auth.on('unauthorized', () => setSession(null));

    return () => {
      offIn();
      offOut();
      offDead();
    };
  }, []);

  return { session, loading };
}

on devolve a própria função de cancelamento, então retorná-las do efeito é toda a limpeza necessária.

Faça login e espere um desafio

Um login pode terminar de duas formas: com uma sessão ou com um desafio, quando o usuário tem um segundo fator cadastrado. O formulário precisa lidar com as duas.

src/SignIn.tsx
import { useState } from 'react';
import type { Challenge } from '@nulvless/nulvless-js';
import { nulvless } from './lib/nulvless';
import { VerifyCode } from './VerifyCode';

export function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [challenge, setChallenge] = useState<Challenge | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setError(null);

    const { data, error } = await nulvless.auth.signIn({ email, password });

    if (error) return setError('Confira seu e-mail e sua senha.');
    if (data.challenge) return setChallenge(data.challenge);
  }

  if (challenge) return <VerifyCode challenge={challenge} />;

  return (
    <form onSubmit={submit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" />
      <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
      {error && <p role="alert">{error}</p>}
      <button type="submit">Entrar</button>
    </form>
  );
}

Quando não há desafio, não resta nada a fazer: a sessão já foi salva e o evento signed-in já chegou ao seu hook.

Um componente para todo código

O mesmo componente coleta o código de MFA, de um login por código único ou de um pedido de sudo, porque o próprio desafio diz a qual fluxo pertence.

src/VerifyCode.tsx
import { useState } from 'react';
import type { Challenge } from '@nulvless/nulvless-js';
import { nulvless } from './lib/nulvless';

export function VerifyCode({ challenge }: { challenge: Challenge }) {
  const [code, setCode] = useState('');
  const [error, setError] = useState<string | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();

    const { error } = await nulvless.auth.verifyChallenge({
      token: challenge.token,
      code,
    });

    if (error) setError('Esse código não funcionou.');
  }

  return (
    <form onSubmit={submit}>
      <p>Enviamos um código para {challenge.identifier}</p>
      <input value={code} onChange={(e) => setCode(e.target.value)} inputMode="numeric" />
      {error && <p role="alert">{error}</p>}
      <button type="submit">Confirmar</button>
    </form>
  );
}

challenge.identifier já chega mascarado, como u***@example.com, então pode ser exibido na tela.

Proteja uma rota

src/Protected.tsx
import { useSession } from './lib/use-session';
import { SignIn } from './SignIn';

export function Protected({ children }: { children: React.ReactNode }) {
  const { session, loading } = useSession();

  if (loading) return null;
  if (!session) return <SignIn />;

  return children;
}

Envolva qualquer parte do app em <Protected>. Quando a sessão termina, seja porque o usuário saiu, porque expirou ou porque foi revogada de outro dispositivo, o hook recebe o evento e a rota volta para o formulário de login.

Sair

<button onClick={() => nulvless.auth.signOut()}>Sair</button>

Passe 'others' para encerrar todas as outras sessões e manter esta, ou 'global' para encerrar todas.

Próximos passos

Continue lendo