Como implementar a autenticação de token em Next.js usando JWTs

A autenticação de token é uma estratégia popular usada para proteger aplicativos da Web e móveis contra acesso não autorizado. No Next.js, você pode utilizar os recursos de autenticação fornecidos pelo Next-auth.

Alternativamente, você pode optar por desenvolver um sistema de autenticação personalizado baseado em token usando JSON Web Tokens (JWTs). Ao fazer isso, você garante mais controle sobre a lógica de autenticação; essencialmente, customizando o sistema para atender precisamente aos requisitos do seu projeto.

Configurar um projeto Next.js

Para começar, instale Next.js executando o comando abaixo em seu terminal.

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

Este guia utilizará Next.js 13, que inclui o diretório do aplicativo.

A seguir, instale essas dependências em seu projeto usando npm, o Node Package Manager.

 npm install jose universal-cookie 

Jose é um módulo JavaScript que fornece um conjunto de utilitários para trabalhar com JSON Web Tokens enquanto o cookie universal dependency fornece uma maneira simples de trabalhar com cookies do navegador nos ambientes do lado do cliente e do lado do servidor.

Crie a interface do usuário do formulário de login

Abra o diretório src/app, crie uma nova pasta e nomeie-a como login. Dentro desta pasta, adicione um novo arquivo page.js e inclua 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 funcional da página de login que renderizará um formulário de login simples no navegador para permitir que os usuários insiram um nome de usuário e uma senha.

A instrução use client no código garante que um limite seja declarado entre o código somente servidor e somente cliente no diretório do aplicativo.

Neste caso, é utilizado para declarar que o código da página de login, principalmente a função handleSubmit, só é executada no cliente; caso contrário, Next.js gerará um erro.

  Como fazer uma máquina de escrever ou animação de linha de comando no PowerPoint

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

 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");
    }
 };

Para gerenciar a lógica de autenticação de login, esta função captura as credenciais do usuário do formulário de login. Em seguida, ele envia uma solicitação POST para um endpoint da API, passando os detalhes do usuário para verificação.

Se as credenciais forem válidas, indicando que o processo de login foi bem-sucedido, a API retornará um status de sucesso na resposta. A função manipuladora usará então o roteador Next.js para navegar o usuário até uma URL especificada, neste caso, a rota protegida.

Definir o ponto final da API de login

Dentro do diretório src/app, crie uma nova pasta e nomeie-a como api. Dentro desta pasta, adicione um novo arquivo login/route.js e inclua o código abaixo.

 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: "https://www.makeuseof.com/",
    });
    return response;
  }
  return NextResponse.json({ success: false });
}

A principal tarefa desta API é verificar as credenciais de login passadas nas solicitações POST usando dados simulados.

Após a verificação bem-sucedida, ele gera um token JWT criptografado associado aos detalhes do usuário autenticado. Por fim, envia uma resposta bem-sucedida ao cliente, incluindo o token nos cookies de resposta; caso contrário, ele retornará uma resposta de status de falha.

Implementar lógica de verificação de token

A etapa inicial na autenticação do token é gerar o token após um processo de login bem-sucedido. A próxima etapa é implementar a lógica para verificação de token.

  Por que o Wi-Fi usa a mesma frequência das microondas?

Essencialmente, você usará a função jwtVerify fornecida pelo módulo Jose para verificar os tokens JWT passados ​​com solicitações HTTP subsequentes.

No diretório src, crie um novo arquivo libs/auth.js e inclua 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 pode verificar efetivamente se o token fornecido é válido e, em última análise, autorizar as solicitações dos usuários.

Crie o arquivo .env no diretório raiz e adicione uma chave secreta exclusiva da seguinte maneira:

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

Crie uma rota protegida

Agora, você precisa criar uma rota à qual apenas usuários autenticados possam ter acesso. Para fazer isso, crie um novo arquivo protected/page.js no diretório src/app. Dentro deste arquivo, adicione o seguinte código.

 export default function ProtectedPage() {
    return <h1>Very protected page</h1>;
  }

Crie um gancho para gerenciar o estado de autenticação

Crie uma nova pasta no diretório src e nomeie-a como hooks. Dentro desta pasta adicione um novo arquivo useAuth/index.js e inclua o código abaixo.

 "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 gancho 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, em seguida, define os detalhes do usuário autenticado para o estado de autenticação.

Ao fazer isso, permite que outros componentes acessem e utilizem as informações do usuário autenticado. Isso é essencial para cenários como fazer atualizações de UI com base no status de autenticação, fazer solicitações de API subsequentes ou renderizar conteúdo diferente com base nas funções do usuário.

  Sobre o Método do Caminho Crítico (CPM) de A a Z para Gerenciamento de Projetos

Nesse caso, você usará o gancho para renderizar conteúdo diferente na rota inicial com base no estado de autenticação de um usuário.

Uma abordagem alternativa que você pode considerar é lidar com o gerenciamento de estado usando o Redux Toolkit ou empregar uma ferramenta de gerenciamento de estado como o Jotai. Esta abordagem garante que os componentes possam obter acesso global ao estado de autenticação ou a qualquer outro estado definido.

Vá em frente e abra o arquivo app/page.js, exclua o código Next.js padrão e adicione o código a seguir.

 "use client" ;

import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
  const auth = useAuth();
  return <>
           <h1>Public Home Page</h1>
           <header>
              <nav>
                {auth ? (
                   <p>logged in</p>
                ) : (
                  <Link href="https://wilku.top/login">Login</Link>
                )}
              </nav>
          </header>
  </>
}

O código acima utiliza o gancho useAuth para gerenciar o estado de autenticação. Ao fazer isso, ele renderiza condicionalmente uma página inicial pública com um link para a rota da página de login quando o usuário não está autenticado e exibe um parágrafo para um usuário autenticado.

Adicione um middleware para impor acesso autorizado a rotas protegidas

No diretório src, crie um novo arquivo middleware.js e adicione o código abaixo.

 import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";

const AUTH_PAGES = ["https://wilku.top/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: ["https://wilku.top/login", "/protected/:path*"] };

Este código de middleware atua como um guarda. Ele verifica se os usuários que desejam acessar páginas protegidas estão autenticados e autorizados a acessar as rotas, além de redirecionar os usuários não autorizados para a página de login.

Protegendo aplicativos Next.js

A autenticação de token é um mecanismo de segurança eficaz. No entanto, não é a única estratégia disponível para proteger as suas aplicações contra acesso não autorizado.

Para fortalecer os aplicativos contra o cenário dinâmico da segurança cibernética, é importante adotar uma abordagem de segurança abrangente que aborde de forma holística possíveis brechas e vulnerabilidades de segurança para garantir uma proteção completa.