πŸ” Login com JWT no Next.js

Estrutura completa de autenticaΓ§Γ£o no Next.js 15/16

ContinuaΓ§Γ£o direta da aula 06.JWT (Backend) β€” agora implementando o lado do cliente

App Router Server Actions Cookie httpOnly Middleware

πŸ”— ConexΓ£o com a aula de JWT (Backend)

Na aula anterior vimos que o JWT Γ© um crachΓ‘ digital assinado:

eyJhbGciOi....eyJzdWIiOi....SflKxwRJ...

1. Header
Algoritmo e tipo
{"alg":"HS256","typ":"JWT"}
2. Payload
Claims do usuΓ‘rio
{"sub":"1","role":"admin"}
3. Signature
Garante autenticidade
HMAC-SHA256

Hoje: como o Next.js consome esse token para fazer login, manter sessΓ£o e proteger rotas.

🎯 Objetivos da aula

  • βœ… Entender onde armazenar o JWT com seguranΓ§a no navegador
  • βœ… Criar uma tela de login com React Server Components + Server Actions
  • βœ… Chamar a API Node/Express com JWT (a aula 06.JWT do backend)
  • βœ… Guardar o token em cookie httpOnly
  • βœ… Proteger rotas com Middleware (Next 15) / Proxy (Next 16)
  • βœ… Enviar o token em requisiΓ§Γ΅es autenticadas
  • βœ… Implementar logout e tratamento de token expirado
  • βœ… Aplicar as boas prΓ‘ticas de seguranΓ§a da aula de JWT

❓ O grande dilema: onde guardar o JWT?

❌ localStorage / sessionStorage
  • AcessΓ­vel por qualquer JS da pΓ‘gina
  • VulnerΓ‘vel a XSS (roubo do token)
  • NΓ£o Γ© enviado automaticamente
βœ… Cookie httpOnly + Secure
  • JavaScript nΓ£o consegue ler o token
  • Enviado automaticamente nas requisiΓ§Γ΅es
  • Protegido contra XSS + HTTPS
⚠️ Cookie sem httpOnly
  • Melhor que localStorage
  • Mas ainda legΓ­vel via JS
  • Usar sΓ³ para dados nΓ£o sensΓ­veis

DecisΓ£o da aula: o JWT serΓ‘ gravado em cookie httpOnly pelo Next.js β€” inacessΓ­vel ao JavaScript do navegador.

πŸ—οΈ Arquitetura da soluΓ§Γ£o

🌐 Navegador
FormulΓ‘rio de login
βž€ POST ➜
β–² Next.js 15/16
Server Action login()
➁ POST /auth/login ➜
βš™οΈ API Express + JWT
Valida e assina o token
🌐 Navegador
Cookie httpOnly gravado
βž‚ GET /dashboard ➜
πŸ›‘οΈ Middleware / Proxy
Verifica cookie e protege rotas
βžƒ Authorization ➜
βš™οΈ API Express + JWT
Valida assinatura (aula 06)

πŸ“ Estrutura do projeto (App Router)

next-auth-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”œβ”€β”€ page.tsx                  ← pΓ‘gina pΓΊblica
β”‚   β”œβ”€β”€ login/
β”‚   β”‚   └── page.tsx              ← tela de login
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”œβ”€β”€ layout.tsx            ← layout protegido
β”‚   β”‚   └── page.tsx              ← Γ‘rea logada
β”œβ”€β”€ actions/
β”‚   └── auth.ts                   ← Server Actions: login/logout
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ api.ts                    ← cliente HTTP (axios/fetch)
β”‚   └── session.ts                ← leitura do JWT no servidor
β”œβ”€β”€ middleware.ts                 ← Next 15  (Next 16: proxy.ts)
β”œβ”€β”€ next.config.ts
└── package.json

βš™οΈ 1. Criando o projeto

npx create-next-app@latest next-auth-app
cd next-auth-app

# HTTP client (opcional β€” o fetch nativo tambΓ©m resolve)
npm install axios

πŸ”‘ Sem jose, sem JWT_SECRET no Next.js! A validaΓ§Γ£o da assinatura do token acontece somente na API Express β€” a chave secreta nunca sai do backend.

Next.js 15
App Router Β· cookies() assΓ­ncrono Β· middleware.ts
Next.js 16
proxy.ts substitui middleware.ts Β· Turbopack padrΓ£o Β· React 19

πŸ’‘ AtenΓ§Γ£o: desde o Next.js 15, cookies(), headers() e params sΓ£o assΓ­ncronos β€” sempre use await.

πŸ–₯️ 2. Tela de login β€” app/login/page.tsx

import { login } from '@/actions/auth';

export default function LoginPage() {
  return (
    <main style={{ maxWidth: 380, margin: '80px auto' }}>
      <h1>Entrar na plataforma</h1>

      <form action={login}>
        <label htmlFor="email">E-mail</label>
        <input id="email" name="email" type="email" required />

        <label htmlFor="senha">Senha</label>
        <input id="senha" name="senha" type="password" required />

        <button type="submit">Entrar</button>
      </form>
    </main>
  );
}

🎨 Visual sugerido: fundo claro, tΓ­tulo em vermelho Unipar, botΓ£o com gradiente #C22820 β†’ #5C2034 (mesma identidade da aula de Backend).

🎨 EstilizaΓ§Γ£o com a identidade Unipar β€” globals.css

:root {
  --unipar-vermelho: #C22820;   /* vermelho Unipar */
  --unipar-vinho:    #5C2034;   /* vinho Unipar  */
}

body {
  background: #f5f5f5;
  color: #272425;
  font-family: system-ui, sans-serif;
}

h1 { color: #5C2034; }

.btn-unipar {
  background: linear-gradient(135deg, #C22820, #5C2034);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 12px 24px;
  font-size: 16px;
  cursor: pointer;
}

.btn-unipar:hover { filter: brightness(1.1); }

⚑ 3. Server Action de login β€” actions/auth.ts

'use server';

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

const API = process.env.API_URL ?? 'http://localhost:3001';

export async function login(_prev: unknown, formData: FormData) {
  const email = formData.get('email');
  const senha = formData.get('senha');

  const res = await fetch(`${API}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, senha }),
  });

  if (!res.ok) {
    return { error: 'E-mail ou senha invΓ‘lidos' };
  }

  const { token } = await res.json();

  const cookieStore = await cookies();          // ← await no Next 15/16
  cookieStore.set('token', token, {
    httpOnly: true,                             // ← JS nΓ£o lΓͺ o token
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 15,                            // ← 15 minutos (exp curto)
  });

  redirect('/dashboard');
}
1️⃣ UsuΓ‘rio envia
email + senha
➜
2️⃣ API valida e retorna
JWT assinado
➜
3️⃣ Server Action grava
cookie httpOnly
➜
4️⃣ Navegador segue
logado automaticamente

⚠️ Importante: cookies().set() sΓ³ funciona em Server Actions e Route Handlers β€” nunca em Server Components comuns (que sΓ£o somente leitura).

βœ… O cookie httpOnly nΓ£o pode ser lido por document.cookie β€” mesmo que um ataque XSS injete JS, o token continua fora de alcance.

πŸ›‘οΈ 5. Protegendo rotas β€” Next 15 vs Next 16

Next.js 15 β†’ middleware.ts

import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const token = req.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.redirect(
      new URL('/login', req.url)
    );
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

Next.js 16 β†’ proxy.ts

import { NextRequest, NextResponse } from 'next/server';

export function proxy(req: NextRequest) {
  const token = req.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.redirect(
      new URL('/login', req.url)
    );
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

πŸ“ No Next.js 16 o arquivo middleware.ts foi renomeado para proxy.ts β€” a lΓ³gica continua exatamente a mesma.

πŸ” 6. Quem valida o token? SΓ³ a API Express!

πŸ”‘ Regra de ouro: o segredo (JWT_SECRET) nunca sai do backend. O Next.js nΓ£o verifica a assinatura β€” ele sΓ³ checa se o cookie existe e repassa o token para a API, que Γ© a ΓΊnica autoridade.

// middleware.ts (Next 15) / proxy.ts (Next 16)
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const token = req.cookies.get('token')?.value;

  // βœ… SΓ³ verificamos a PRESENΓ‡A do token (gate de entrada)
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  // πŸ” Assinatura e expiraΓ§Γ£o? SΓ³ a API Express valida (aula 06)
  return NextResponse.next();
}

⚠️ Nunca copie o JWT_SECRET para o Next.js! Se o token for invΓ‘lido ou estiver expirado, quem descobre Γ© a API (resposta 401) β€” e o Next.js trata o erro limpando o cookie e voltando ao /login.

πŸ“‘ 7. RequisiΓ§Γ΅es autenticadas

import { cookies } from 'next/headers';

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const token = cookieStore.get('token')?.value;

  const res = await fetch(`${process.env.API_URL}/perfil`, {
    headers: { Authorization: `Bearer ${token}` },
    cache: 'no-store',
  });

  const perfil = await res.json();

  return <h1>OlΓ‘, {perfil.nome}!</h1>;
}

OpΓ§Γ£o B β€” Route Handler como proxy interno

O navegador chama /api/perfil do Next.js, que repassa o Authorization para a API β€” a URL do backend nunca fica exposta no cliente.

πŸšͺ 8. Logout

'use server';

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export async function logout() {
  const cookieStore = await cookies();
  cookieStore.delete('token');       // remove o cookie httpOnly
  redirect('/login');
}
βœ… Com cookie httpOnly
O logout acontece no servidor: basta apagar o cookie. Simples e seguro.
❌ Com localStorage
Seria preciso limpar o storage e confiar que o token nΓ£o foi copiado β€” muito mais frΓ‘gil.

πŸ’‘ Reforce a seguranΓ§a: quando a API responder 401 (token invΓ‘lido ou expirado), o Next.js deve limpar o cookie e enviar o usuΓ‘rio de volta ao /login.

⏱️ 9. Token expirado (401)

Request com
token expirado
➜
API Express valida
e responde 401
➜
Next.js limpa
o cookie
➜
UsuΓ‘rio volta
ao /login
// lib/api.ts β€” interceptor do axios
api.interceptors.response.use(
  (res) => res,
  (error) => {
    if (error.response?.status === 401) {
      // token invΓ‘lido/expirado: volta para o login
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

⏱️ Sem Refresh Token por enquanto: quando o token expirar, o usuΓ‘rio faz login novamente. (Futuramente podemos adicionar o fluxo de refresh β€” conceito da aula 06.JWT.)

🧯 10. Erros e experiΓͺncia do usuΓ‘rio

Use useActionState (React 19) para exibir erros sem recarregar a pΓ‘gina:

'use client';

import { useActionState } from 'react';
import { login } from '@/actions/auth';

export default function LoginForm() {
  const [state, formAction, pending] = useActionState(login, null);

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <input name="senha" type="password" required />

      {state?.error && (
        <p style={{ color: '#C22820' }}>{state.error}</p>
      )}

      <button disabled={pending}>
        {pending ? 'Entrando…' : 'Entrar'}
      </button>
    </form>
  );
}

🎨 Erros em vermelho Unipar, estado de carregamento no botão e mensagens claras = boa UX + segurança (não revele se o e-mail existe!).

πŸ”’ 11. SeguranΓ§a β€” casando com a aula 06.JWT

βœ… FaΓ§a

  • Cookie httpOnly + secure + sameSite
  • Access token com exp curto (minutos)
  • Deixar a validaΓ§Γ£o da assinatura sΓ³ na API β€” a chave nΓ£o sai do backend
  • Use HTTPS sempre em produΓ§Γ£o
  • Mensagens de erro genΓ©ricas no login

❌ Evite

  • Guardar JWT em localStorage
  • Levar o JWT_SECRET para o frontend β€” ele vive sΓ³ no backend
  • Confiar no alg do header recebido
  • Colocar dados sensΓ­veis no payload (Γ© sΓ³ codificado, nΓ£o criptografado!)
  • Tokens de longa duraΓ§Γ£o
  • Expor detalhes internos na resposta de erro

πŸ—ΊοΈ 12. Fluxo completo do login

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  email+senha   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  POST /auth/login  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Navegadorβ”‚ ─────────────► β”‚ Next.js       β”‚ ─────────────────► β”‚ API Express  β”‚
β”‚ (form)   β”‚                β”‚ Server Action β”‚                    β”‚ + JWT        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ ◄───────────────── β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚        token assinado
                                    β–Ό
                            grava cookie httpOnly (token)
                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  GET /dashboard β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  Authorization: <JWT> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Navegadorβ”‚ ──────────────► β”‚ Middleware /   β”‚ ─────────────────────► β”‚ API Express  β”‚
β”‚ (cookie) β”‚                 β”‚ Proxy (15/16)  β”‚      (repassa JWT)     β”‚ valida assin. β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ ◄───────────────────── β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚       sΓ³ checa se o cookie existe      β”‚
                                    β”‚                          vΓ‘lido βœ“      β”‚ 401 βœ—
                                    β–Ό                                        β–Ό
                            renderiza Γ‘rea logada              limpa cookie β†’ /login
Backend: aula 06.JWT Frontend: esta aula

πŸ“¦ 13. Estrutura final do projeto

next-auth-app/
 β”œβ”€ app/
 β”‚   β”œβ”€ login/page.tsx          ← tela de login
 β”‚   └─ dashboard/page.tsx      ← Γ‘rea protegida
 β”œβ”€ actions/auth.ts             ← login() e logout()
 β”œβ”€ lib/
 β”‚   β”œβ”€ api.ts                  ← axios/fetch com token
 β”‚   └─ session.ts              ← leitura do cookie no servidor
 β”œβ”€ middleware.ts               ← Next 15
 β”‚   (proxy.ts)                 ← Next 16
 β”œβ”€ .env.local                  ← sΓ³ API_URL (a chave fica no backend!)
 └─ package.json
# .env.local
API_URL=http://localhost:3001

# ⚠ O JWT_SECRET fica APENAS no backend Express (aula 06.JWT)

🏁 Resumo e Conclusão

  • πŸ” Cookie httpOnly Γ© o lugar seguro para o JWT no Next.js
  • ⚑ Server Actions sΓ£o o lugar certo para gravar cookies e chamar a API
  • πŸ”‘ Quem valida o JWT Γ© sΓ³ a API Express β€” a chave secreta nunca sai do backend
  • πŸ›‘οΈ Middleware (15) / Proxy (16) protegem as rotas (sΓ³ checam se o cookie existe)
  • πŸ“‘ RequisiΓ§Γ΅es autenticadas usam o header **Authorization: Bearer <token>
  • πŸšͺ Logout = apagar o cookie no servidor
  • ⏱️ Token expirado (401)? Limpa o cookie e volta ao /login
  • 🎨 A identidade visual Unipar (#C22820 e #5C2034) pode ser aplicada via CSS variables

A aula 06.JWT (Backend) + esta aula (Frontend) = sistema de autenticaΓ§Γ£o completo!

πŸ’‘ Desafio para praticar

  • βœ… Criar a tela de login com a identidade Unipar
  • πŸ›‘οΈ Proteger a rota /dashboard com middleware/proxy
  • πŸ‘€ Exibir os dados do usuΓ‘rio decodificados do JWT (claim sub, nome, role)
  • πŸšͺ Criar o botΓ£o de logout no layout do dashboard
  • πŸ§ͺ Testar: apagar o cookie manualmente e tentar acessar /dashboard
  • πŸ“€ Enviar o projeto no GitHub

πŸ‘ Fim da Aula

Perguntas?