Autenticação JWT em Next.js: Guia Completo com Exemplo Prático

Foto do autor

By luis

Implementando Autenticação com Token JWT em Next.js

A autenticação baseada em token é uma técnica amplamente utilizada para proteger aplicações web e mobile contra acessos não autorizados. Em Next.js, é possível usar funcionalidades de autenticação fornecidas pelo Next-auth.

Uma alternativa é construir um sistema de autenticação personalizado com tokens JWT (JSON Web Tokens). Isso oferece maior controle sobre a lógica de autenticação, permitindo adaptar o sistema às necessidades exatas do seu projeto.

Configuração Inicial de um Projeto Next.js

Para começar, crie um novo projeto Next.js usando o comando abaixo no seu terminal:

npx create-next-app@latest next-auth-jwt --experimental-app

Este tutorial usará o Next.js 13, que inclui o diretório `app`.

Em seguida, instale as seguintes dependências usando o npm:

npm install jose universal-cookie

A biblioteca Jose é um conjunto de ferramentas JavaScript para lidar com JSON Web Tokens, enquanto universal-cookie facilita o uso de cookies em ambientes de cliente e servidor.

Criando a Interface de Login

Dentro do diretório `src/app`, crie uma nova pasta chamada `login`. Dentro dessa pasta, crie um arquivo `page.js` e adicione o código abaixo:

 "use client";
import { useRouter } from "next/navigation";
export default function LoginPage() {
    return (
      <form onSubmit={handleSubmit}>
        <label>
          Username:
          <input type="text" name="username" />
        </label>
        <label>
          Password:
          <input type="password" name="password" />
        </label>
        <button type="submit">Login</button>
      </form>
    );
}

O código acima cria um componente de página de login com um formulário simples que permite aos usuários inserir um nome de usuário e senha.

A diretiva `use client` assegura a distinção entre o código de servidor e cliente dentro do diretório `app`. Neste caso, ela garante que a lógica da página de login, incluindo `handleSubmit`, seja executada apenas no navegador, evitando erros do Next.js.

Agora, vamos definir o código da função `handleSubmit`. Dentro do componente funcional, adicione:

const router = useRouter();
const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const username = formData.get("username");
    const password = formData.get("password");
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ username, password }),
    });
    const { success } = await res.json();
    if (success) {
      router.push("/protected");
      router.refresh();
    } else {
      alert("Login failed");
    }
  };

Essa função lida com a lógica de autenticação, capturando as credenciais do usuário no formulário. Em seguida, envia uma requisição POST para um endpoint de API, passando os detalhes para verificação.

Se as credenciais forem válidas, a API retorna um status de sucesso. A função então usa o roteador do Next.js para redirecionar o usuário para uma rota específica, nesse caso, `/protected`.

Criando o Endpoint da API de Login

Dentro do diretório `src/app`, crie uma nova pasta chamada `api`. Dentro dela, crie uma pasta `login` e, dentro desta, um arquivo `route.js`. Adicione o seguinte código:

import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";
export async function POST(request) {
  const body = await request.json();
  if (body.username === "admin" && body.password === "admin") {
    const token = await new SignJWT({
      username: body.username,
    })
      .setProtectedHeader({ alg: "HS256" })
      .setIssuedAt()
      .setExpirationTime("30s")
      .sign(getJwtSecretKey());
    const response = NextResponse.json(
      { success: true },
      { status: 200, headers: { "content-type": "application/json" } }
    );
    response.cookies.set({
      name: "token",
      value: token,
      path: "/",
    });
    return response;
  }
  return NextResponse.json({ success: false });
}

Esta API tem a função de verificar as credenciais de login recebidas nas requisições POST usando dados de exemplo.

Se a verificação for bem-sucedida, um token JWT criptografado é gerado, contendo informações do usuário autenticado. Uma resposta de sucesso é enviada ao cliente, incluindo o token nos cookies de resposta. Caso contrário, uma resposta de falha é retornada.

Implementando a Lógica de Verificação do Token

O processo de autenticação por token começa com a geração do token após um login bem-sucedido. A próxima etapa é implementar a lógica para a verificação do token.

Para isso, utilizaremos a função `jwtVerify` fornecida pelo módulo Jose para verificar os tokens JWT enviados em requisições HTTP futuras.

Crie um novo arquivo `libs/auth.js` no diretório `src` e adicione o código abaixo:

import { jwtVerify } from "jose";
export function getJwtSecretKey() {
  const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
  if (!secret) {
    throw new Error("JWT Secret key is not matched");
  }
  return new TextEncoder().encode(secret);
}
export async function verifyJwtToken(token) {
  try {
    const { payload } = await jwtVerify(token, getJwtSecretKey());
    return payload;
  } catch (error) {
    return null;
  }
}

A chave secreta é usada para assinar e verificar os tokens. Ao comparar a assinatura do token decodificado com a assinatura esperada, o servidor consegue verificar a validade do token e, consequentemente, autorizar as requisições dos usuários.

Crie um arquivo `.env` na raiz do projeto e adicione uma chave secreta única, da seguinte maneira:

NEXT_PUBLIC_JWT_SECRET_KEY=sua_chave_secreta

Criando uma Rota Protegida

Agora, precisamos criar uma rota que só possa ser acessada por usuários autenticados. Para isso, crie um arquivo `protected/page.js` dentro do diretório `src/app` e adicione o código:

export default function ProtectedPage() {
    return <h1>Página muito protegida</h1>;
  }

Criando um Hook para Gerenciar o Estado de Autenticação

Crie uma nova pasta chamada `hooks` dentro do diretório `src`. Dentro dela, crie um arquivo `useAuth/index.js` e adicione o seguinte código:

"use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";
export function useAuth() {
  const [auth, setAuth] = React.useState(null);
  const getVerifiedtoken = async () => {
    const cookies = new Cookies();
    const token = cookies.get("token") ?? null;
    const verifiedToken = await verifyJwtToken(token);
    setAuth(verifiedToken);
  };
  React.useEffect(() => {
    getVerifiedtoken();
  }, []);
  return auth;
}

Este hook gerencia o estado de autenticação no lado do cliente. Ele busca e verifica a validade do token JWT presente nos cookies usando a função `verifyJwtToken` e define os detalhes do usuário autenticado no estado de autenticação.

Ao fazer isso, outros componentes podem acessar e usar as informações do usuário autenticado. Isso é essencial para cenários como atualizar a UI com base no status de autenticação, fazer requisições de API ou renderizar conteúdo diferente baseado nas funções do usuário.

Nesse caso, usaremos o hook para renderizar conteúdo diferente na rota inicial, dependendo do estado de autenticação do usuário.

Uma abordagem alternativa seria lidar com o gerenciamento de estado usando Redux Toolkit ou uma ferramenta como Jotai. Essa abordagem garante acesso global ao estado de autenticação.

Abra o arquivo `app/page.js`, remova o código padrão do Next.js e adicione o seguinte:

"use client" ;
import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
  const auth = useAuth();
  return <>
        <h1>Página Inicial Pública</h1>
        <header>
          <nav>
            {auth ? (
                <p>Logado</p>
              ) : (
                <Link href="/login">Login</Link>
            )}
          </nav>
        </header>
  </>
}

Este código usa o hook `useAuth` para gerenciar o estado de autenticação. Ele renderiza uma página inicial pública com um link para a página de login quando o usuário não está autenticado e exibe um parágrafo para usuários autenticados.

Adicionando um Middleware para Proteger Rotas

Crie um arquivo `middleware.js` no diretório `src` e adicione o seguinte código:

import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";
const AUTH_PAGES = ["/login"];
const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));
export async function middleware(request) {
  const { url, nextUrl, cookies } = request;
  const { value: token } = cookies.get("token") ?? { value: null };
  const hasVerifiedToken = token && (await verifyJwtToken(token));
  const isAuthPageRequested = isAuthPages(nextUrl.pathname);
  if (isAuthPageRequested) {
    if (!hasVerifiedToken) {
      const response = NextResponse.next();
      response.cookies.delete("token");
      return response;
    }
    const response = NextResponse.redirect(new URL(`/`, url));
    return response;
  }
  if (!hasVerifiedToken) {
    const searchParams = new URLSearchParams(nextUrl.searchParams);
    searchParams.set("next", nextUrl.pathname);
    const response = NextResponse.redirect(
      new URL(`/login?${searchParams}`, url)
    );
    response.cookies.delete("token");
    return response;
  }
  return NextResponse.next();
}
export const config = { matcher: ["/login", "/protected/:path*"] };

Este middleware atua como um guarda, verificando se usuários que tentam acessar páginas protegidas estão autenticados e autorizados a acessar as rotas, redirecionando usuários não autorizados para a página de login.

Protegendo Aplicações Next.js

A autenticação por token é um método eficaz de segurança. No entanto, não é a única estratégia para proteger aplicações contra acessos não autorizados.

Para fortalecer as aplicações contra o cenário dinâmico da segurança cibernética, é crucial adotar uma abordagem abrangente que aborde possíveis brechas e vulnerabilidades, garantindo uma proteção completa.