login()
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
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.
App Router Β· cookies() assΓncrono Β· middleware.ts
proxy.ts substitui middleware.ts Β· Turbopack padrΓ£o Β· React 19
AtenΓ§Γ£o: desde o Next.js 15,
cookies(),headers()eparamssΓ£o assΓncronos β sempre useawait.
app/login/page.tsximport { 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).
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); }
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');
}
httpOnly passo a passo 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.
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.tsfoi renomeado paraproxy.tsβ a lΓ³gica continua exatamente a mesma.
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.
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>;
}
O navegador chama /api/perfil do Next.js, que repassa o Authorization para a API β a URL do backend nunca fica exposta no cliente.
'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');
}
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.
// 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.)
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!).
06.JWT FaΓ§a
httpOnly + secure + sameSiteexp curto (minutos) Evite
localStorageJWT_SECRET para o frontend β ele vive sΓ³ no backendalg do header recebidoββββββββββββ 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
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)
httpOnly Γ© o lugar seguro para o JWT no Next.jsAuthorization: Bearer <token>401)? Limpa o cookie e volta ao /login#C22820 e #5C2034) pode ser aplicada via CSS variablesA aula
06.JWT(Backend) + esta aula (Frontend) = sistema de autenticaΓ§Γ£o completo!
/dashboard com middleware/proxysub, nome, role)/dashboard