Este es un tutorial practico aprenderemos a implementar una autenticacion sencilla en Nextjs que use JSON Web Tokens (JWT) y las guarde en Cookies. Ademas crearemos tanto el codigo de Backend (API) como el de Frontend desde el mismo proyecto de Nextjs.
Creacion de Proyecto en Nextjs
npx create-next-app nextjs-cookies-jwt
Instalacion del modulo jsonwebtoken
npm i cookie jsonwebtoken
variables de entorno
Luego crearemos en un archivo .env las siguientes variables de entorno
SECRET=mysecretkey
NODE_ENV=development
API REST
Tipicamente cuando creamos una aplicacion web el backend y el frontend van separados, pero como en Nextjs es muy comun desarrollar ambos, aqui tambien podemos tener nuestra REST API
crearemos primero un archivo en /api/auth/login con el siguiente codigo:
import { sign } from "jsonwebtoken";
import { serialize } from "cookie";
export default function loginHandler(req, res) {
const { email, password } = req.body;
if (email === "admin@local.local" && password === "admin") {
// create a token
const token = sign(
{
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // expire in 30 days
email,
},
"secret"
);
// create a cookie
const serialized = serialize("myTokenName", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 1000 * 60 * 60 * 24 * 30,
path: "/"
});
// set the cokie in header
res.setHeader("Set-Cookie", serialized);
// sen a response with the cookie in a header
return res.status(200).json({
message: "Login successful",
});
}
return res.status(401).json({ error: "Invalid credentials" });
}
Con este codigo creamos un token y lo establecemos en una cookie para enviarlo al frontend.
Este mismo proceso se puede hacer tambien desde otro backend hecho en Python, Java, Go, etc. Lo importante es entender que la API de backend debe enviar el token en una cookie establecida en el header
Frontend
Ahora instalemos en nuestro proyecto axios para poder enviar las peticiones desde el Frontend de React.
npm i axios
Creemos el siguiente componente que nos permite enviar una peticion POST con los datos del usuario
import axios from "axios";
import { useState } from "react";
function Home() {
const [credentials, setCredentials] = useState({
email: "",
password: "",
});
const handleSubmit = async (e) => {
e.preventDefault();
const user = await axios.post("/api/auth/login", credentials);
console.log(user);
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="email"
onChange={(e) =>
setCredentials({
...credentials,
email: e.target.value,
})
}
/>
<input
type="password"
placeholder="password"
onChange={(e) =>
setCredentials({
...credentials,
password: e.target.value,
})
}
/>
<button>Save</button>
</form>
</div>
);
}
export default Home;
esto te permitira ver en la seccin cookies del navegador la cookie generada por el backnd.
usando la cookie en el backend
api/profile.js
import jwt from "jsonwebtoken";
export default function profileHandler(req, res) {
const { myTokenName } = req.cookies;
if (!myTokenName) {
return res.status(401).json({ error: "Not logged in" });
}
const { email } = jwt.verify(myTokenName, "secret");
return res.status(200).json({ email });
}
enviando la cookie desde el frontend
const getProfile = async () => {
const profile = await axios.get("/api/profile");
console.log(profile);
}
Cerrar Sesion de Usuario (Backend)
En esta seccion vas a aprender a hacer Logout o cerrar sesion de la cuenta de un usuario.
Cuando creamos el token este se ha guardado en el navegador, asi que lo que tenemos que hacer es hacer otra peticion en el servidor y decirle que elimine la cookie.
De hecho, la forma de eliminarlo es estableciendo su tiempo a 0, y el valor de la cookie a un valor nulo.
import { serialize } from "cookie";
export default function logoutHandler(req, res) {
const { myTokenName } = req.cookies;
if (!myTokenName) {
return res.status(401).json({ error: "Not logged in" });
}
const serialized = serialize("myTokenName", null, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 0,
path: "/",
});
res.setHeader("Set-Cookie", serialized);
return res.status(200).json({
message: "Logout successful",
});
}
Cerrar Sesion de Usuario (Frontend)
const logout = async () => {
const logout = await axios.get("/api/auth/logout");
console.log(logout);
}
Protected Routes
import { NextResponse } from "next/server";
import { jwtVerify } from "jose";
const secret = process.env.SECRET;
export async function middleware(request) {
const jwt = request.cookies.get("myTokenName");
if (request.nextUrl.pathname.includes("/dashboard")) {
console.log(request.nextUrl.pathname);
console.log({jwt});
if (jwt === undefined) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const { payload } = await jwtVerify(
jwt,
new TextEncoder().encode('secret')
);
console.log({ payload });
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
return NextResponse.next();
}
// export const config = {
// matcher: '/',
// }
if (request.nextUrl.pathname.includes("/login")) {
console.log(jwt)
if (jwt) {
try {
await jwtVerify(jwt, new TextEncoder().encode('secret'));
return NextResponse.redirect(new URL("/dashboard", request.url));
} catch (error) {
return NextResponse.next();
}
}
}
codigo completo del middleware
import { NextResponse } from "next/server";
import { jwtVerify } from "jose";
const secret = process.env.SECRET;
export async function middleware(request) {
const jwt = request.cookies.get("myTokenName");
if (request.nextUrl.pathname.includes("/login")) {
console.log(jwt)
if (jwt) {
try {
await jwtVerify(jwt, new TextEncoder().encode('secret'));
return NextResponse.redirect(new URL("/dashboard", request.url));
} catch (error) {
return NextResponse.next();
}
}
}
if (request.nextUrl.pathname.includes("/dashboard")) {
console.log(request.nextUrl.pathname);
console.log({ jwt });
if (jwt === undefined) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const { payload } = await jwtVerify(
jwt,
new TextEncoder().encode("secret")
);
console.log({ payload });
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
return NextResponse.next();
}
// export const config = {
// matcher: '/',
// }