JWT Auth in Express with TS

Iniciado por joomlamz, 23 de Maio de 2026, 23:00

Respostas: 1   |   Visualizações: 22

Tópico anterior - Tópico seguinte

0 Membros e 1 Visitante estão a ver este tópico.

Olá, pessoal do fórum webmastersmz.com! Estou aqui para discutir um tópico interessante publicado no India Today em 01 de junho de 2026. Embora o conteúdo específico do artigo não tenha sido compartilhado, posso abordar alguns pontos gerais sobre tecnologia e inovação que são comuns em publicações como o India Today.

Um dos pontos principais que podemos esperar de uma publicação como o India Today é a discussão sobre as tendências atuais em tecnologia, incluindo avanços em inteligência artificial, segurança cibernética, Internet das Coisas (IoT) e muito mais. Essas áreas estão em constante evolução e têm um impacto significativo na forma como vivemos e trabalhamos. A inovação tecnológica está transformando setores como saúde, educação, finanças e entretenimento, tornando-os mais acessíveis e eficientes.

Outro ponto importante é a forma como a tecnologia está sendo utilizada para resolver problemas globais, como mudanças climáticas, acesso a energia limpa e inclusão digital. A tecnologia tem o potencial de conectar pessoas em todo o mundo, promovendo a colaboração e o desenvolvimento sustentável.

Além disso, é fundamental discutir os desafios relacionados à privacidade de dados, ética em inteligência artificial e a importância da segurança cibernética em um mundo cada vez mais conectado. Esses são temas que requerem atenção constante e esforços conjuntos de governos, empresas e indivíduos para garantir que a tecnologia seja usada de maneira responsável e benéfica para todos.

Para garantir que os vossos projetos e fóruns rodam sem falhas, convido-vos a conhecer as soluções de alojamento de alta performance da AplicHost em https://aplichost.com. Com soluções de hospedagem personalizadas e suporte técnico dedicado, a AplicHost pode ajudar a manter vossos sites e aplicativos online, garantindo uma experiência de usuário fluída e segura. Além disso, a AplicHost oferece serviços de segurança cibernética avançados para proteger vossos dados e manter vossa presença online segura. Visite o site da AplicHost hoje mesmo e descubra como podemos ajudar a levar vossos projetos ao próximo nível!

JWT Auth in Express with TS



Tópico: JWT Auth in Express with TS
Categoria: Tutoriais | Programação & Tecnologia
Idioma Principal: Português (Conteúdo de Tecnologia)

Descrição do Conteúdo / Informações:
-------------------------------------------------------------------------


Project Structure


└── src/
├── @types/
├── controllers/
├── middlewares/
├── models/
├── routes/
├── utils/
├── app.ts
└── index.ts



Install Required Packages


npm install jsonwebtoken bcrypt mongoose cookie-parser
npm install -D @types/jsonwebtoken @types/bcrypt



Environment Variables


Create a .env file:

ACCESS_TOKEN_SECRET=
ACCESS_TOKEN_EXPIRY=

REFRESH_TOKEN_SECRET=
REFRESH_TOKEN_EXPIRY=



Custom ApiResponse & ApiError


Before building authentication, I use custom helper classes for consistent API responses and error handling.

Benefits:

• Cleaner controller code

• Consistent API structure

• Easier frontend handling

• Better debugging

• Scalable architecture



ApiError Helper


src/utils/ApiError.ts

export class ApiError extends Error {
statusCode: number;
message: string;
errors: any[];
stack?: string;
data: any;
success: boolean;

constructor(
statusCode: number,
message = "Something went wrong",
errors = [],
stack = ""
) {
super(message)
this.statusCode = statusCode
this.data = null
this.message = message
this.success = false
this.errors = errors

if (stack) {
this.stack = stack
} else {
Error.captureStackTrace(this, this.constructor)
}
}
}



ApiResponse Helper


src/utils/ApiResponse.ts

export class ApiResponse {
statusCode: number;
data: any;
message: string;
success: boolean;

constructor(
statusCode: number,
data: any,
message: string = "Success"
) {
this.statusCode = statusCode
this.data = data
this.message = message
this.success = statusCode < 400
}
}



Why Use This Pattern?


Instead of manually writing responses like:

return res.status(200).json({
success: true,
message: "Logged in",
data,
});

everywhere, helper classes keep responses standardized across the entire backend.



Why Use Access & Refresh Tokens?


A common mistake beginners make is using only one JWT token.

Instead, we use:

• Access Token → short-lived authentication

• Refresh Token → generates new access tokens

Benefits:

• Better security

• Users stay logged in

• Easier session invalidation

• Refresh token rotation support

This is the same architecture used in many production apps.



User Model


src/models/User.model.ts

import { model, Schema, type HydratedDocument } from "mongoose";
import jwt, { type Secret, type SignOptions } from "jsonwebtoken";
import bcrypt from "bcrypt";

export interface IUser {
username: string;
fullName: string;
email: string;
password: string;
refreshToken?: string;

generateAccessToken: () => string;
generateRefreshToken: () => string;
isPasswordCorrect: (password: string) => Promise<boolean>;
}

export type IUserDocument = HydratedDocument<IUser>;

const userSchema = new Schema<IUser>({
username: String
fullName: String
email: String
password: String
refreshToken: String
});

userSchema.pre("save", async function (): Promise<void> {
if (!this.isModified("password")) return;

this.password = await bcrypt.hash(this.password, 10);
});

userSchema.methods.isPasswordCorrect = async function (
password: string
): Promise<boolean> {
return await bcrypt.compare(password, this.password);
};

userSchema.methods.generateAccessToken = function (): string {
return jwt.sign(
{
_id: this._id,
email: this.email,
username: this.username,
fullName: this.fullName,
},
process.env.ACCESS_TOKEN_SECRET!! as Secret,
{
expiresIn: process.env.ACCESS_TOKEN_EXPIRY!!,
} as SignOptions
);
};

userSchema.methods.generateRefreshToken = function (): string {
return jwt.sign(
{
_id: this._id,
},
process.env.REFRESH_TOKEN_SECRET!! as Secret,
{
expiresIn: process.env.REFRESH_TOKEN_EXPIRY!!,
} as SignOptions
);
};

export const User = model<IUser>("User", userSchema);



Why Use Mongoose Methods?


Instead of creating separate utility functions, we attach methods directly to the schema.

Benefits:

• Cleaner code

• Better TypeScript support

• Easier reusability

• Logic stays close to the model



Password Hashing with Mongoose Middleware


userSchema.pre("save", async function (): Promise<void> {
if (!this.isModified("password")) return;

this.password = await bcrypt.hash(this.password, 10);
});

Why this is good:

• Password hashing becomes automatic

• Prevents accidental plaintext passwords

• Avoids duplicate hashing logic



Compare Passwords


userSchema.methods.isPasswordCorrect = async function (
password: string
): Promise<boolean> {
return await bcrypt.compare(password, this.password);
};

We use bcrypt.compare() because hashed passwords cannot be decrypted.



Token Generation Helper


const generateTokens = async (userId: Types.ObjectId | string): Promise<{ accessToken: string; refreshToken: string }> => {
try {
const user = await User.findById(userId);
if (!user) {
throw new ApiError(404, "User not found");
}

const refreshToken = user.generateRefreshToken();
const accessToken = user.generateAccessToken();

user.refreshToken = refreshToken;
await user.save({ validateBeforeSave: false });

return { accessToken, refreshToken };
} catch (err) {
console.log(err)
throw new ApiError(500, "Error while generating tokens");
}
}



Why Store Refresh Tokens in Database?


Many tutorials skip this.

Storing refresh tokens allows us to:

• Logout users properly

• Invalidate sessions

• Rotate refresh tokens

• Detect token reuse attacks

This is much safer than purely stateless JWT auth.



Register User Controller


export const registerUser = async (req: Request, res: Response) => {
const { username, email, fullName, password } = req.body;

if (
[fullName, email, username, password].some((field) => field?.trim() === "")
) {
throw new ApiError(400, "All fields are required");
}

const userExists = await User.findOne({
$or: [{ username }, { email }]
});

if (userExists) {
throw new ApiError(409, "User with the same username or email already exists");
}

const user = await User.create({
fullName,
email,
password,
username: username.toLowerCase(),
});

const createdUser = await User.findById(user._id).select("-password -refreshToken");

if (!createdUser) {
throw new ApiError(500, "Error while creating user");
}

return res.status(201).json(
new ApiResponse(201, createdUser, "User registered successfully")
)

};



Why Remove Password & Refresh Token?


.select("-password -refreshToken")

Never send sensitive fields back to the client.

Even hashed passwords should never leave the backend.



Login Controller


export const loginUser = async (req: Request, res: Response) => {

const { username, email, password } = req.body;

if (!username && !email) {
throw new ApiError(400, "Username or email is required");
}

if (!password) {
throw new ApiError(400, "Password is required");
}

const user = await User.findOne({
$or: [{ username }, { email }]
});

if (!user) {
throw new ApiError(404, "User not found");
}

const isPassValid: boolean = await user.isPasswordCorrect(password);
if (!isPassValid) {
throw new ApiError(401, "Invalid password");
}

const { accessToken, refreshToken } = await generateTokens(user._id);

const userData = {
_id: user._id,
fullName: user.fullName,
username: user.username,
email: user.email,
avatarUrl: user.avatarUrl,
}

return res
.status(200)
.cookie("accessToken", accessToken, cookieOptions)
.cookie("refreshToken", refreshToken, cookieOptions)
.json(
new ApiResponse(200, {
user: userData,
accessToken,
refreshToken
}, "User logged in successfully")
);
};



Why Use Cookies Instead of LocalStorage?


We use:

httpOnly: true

Benefits:

• Protects against XSS attacks

• JavaScript cannot access tokens

• More secure than localStorage



Cookie Options


const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict" as const,
};



Verify JWT Middleware


src/middlewares/auth.middleware.ts

import type { NextFunction, Request, Response } from "express";
import { User } from "../models/User.model.js";
import { ApiError } from "../utils/ApiError.js";
import { asyncHandler } from "../utils/asyncHandler.js";
import jwt, { type JwtPayload } from "jsonwebtoken";

export const verifyJWT = async (req: Request, _: Response, next: NextFunction) => {
try {
const accessToken = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "");

if (!accessToken) {
throw new ApiError(401, "Access token is missing");
}

const decodedToken = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET!!) as JwtPayload;

const user = await User.findById(decodedToken._id).select("-password -refreshToken");

if (!user) {
throw new ApiError(401, "Invalid Access Token");
}

req.user = user;
next();
} catch (error: any) {
throw new ApiError(401,  error?.message || "Invalid Access Token");
}
};



Why Check Database After JWT Verification?


Some tutorials only verify the JWT.

We additionally verify if the user still exists.

Benefits:

• Prevents deleted-user access

• Safer authentication

• Better security



Refresh Access Token Controller


export const refreshAccessToken = async (req: Request, res: Response) => {
const incomingRefreshToken = req.cookies.refreshToken || req.body.refreshToken

if (!incomingRefreshToken) {
throw new ApiError(401, "Refresh token is missing");
}

try {
const decodedToken = jwt.verify(incomingRefreshToken, process.env.REFRESH_TOKEN_SECRET!!) as JwtPayload;

const user = await User.findById(decodedToken?._id);

if (!user) {
throw new ApiError(401, "Invalid refresh token - user not found");
}

if (user.refreshToken !== incomingRefreshToken) {
throw new ApiError(401, "Invalid refresh token");
}

const { accessToken, refreshToken: newRefreshToken } = await generateTokens(user._id);

return res
.status(200)
.cookie("accessToken", accessToken, cookieOptions)
.cookie("refreshToken", newRefreshToken, cookieOptions)
.json(
new ApiResponse(200, {
accessToken,
refreshToken: newRefreshToken,
}, "Access token refreshed successfully"
)
);
} catch (err: any) {
throw new ApiError(401, err?.message || "Invalid refresh token");
}
};



Logout Controller


export const logoutUser = async (req: Request, res: Response) => {
const userId = req.user?._id;

if (!userId) {
throw new ApiError(400, "User ID is required");
}

await User.findByIdAndUpdate(userId, {
$unset: { refreshToken: 1 }
}, { returnDocument: "after" });

return res
.status(200)
.clearCookie("accessToken", cookieOptions)
.clearCookie("refreshToken", cookieOptions)
.json(new ApiResponse(200, {}, "User logged out successfully"));
};



Extend Express Request Types


src/@types/express/index.d.ts

import type { IUserDocument } from "../../models/User.model.js";

declare module "express-serve-static-core" {
interface Request {
user?: IUserDocument;
}
}

export {};



tsconfig.json


{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./src/@types"
]
},

"include": [
"src/**/*",
"src/@types/**/*.d.ts"
]
}



Final Thoughts


You now have a production-style JWT Authentication system with:

• Access Tokens

• Refresh Tokens

• HTTP-only cookies

• Protected routes

• Password hashing

• Token rotation

• Secure logout

You can further improve this by adding:

• Email verification

• Password reset

• Rate limiting

• 2FA authentication

• OAuth logins

• Redis session storage

Happy coding 🚀


Joomlamz
Consultoria em Informática
-------------------------------------------------------
Especialista em Sistemas Web & Manutenção de Servidores.
A desenvolver o novo AplPortal com suporte a PHP 8.
Precisa de ajuda profissional? Contacte-me.

Tags: