Understanding JWT: Structure, Use Cases, Best Practices, and Implementation in Node.js with TypeScript & Mongo DB

Understanding JWT: Structure, Use Cases, Best Practices, and Implementation in Node.js with TypeScript & Mongo DB

JSON Web Tokens (JWT) are a popular way to handle authentication in modern web applications. In this blog post, we'll dive deep into JWTs, discussing their structure, use cases, best practices, and how to issue and verify them with a practical example in Node.js using TypeScript. and Mongo DB as database.

What is JWT?

JWT is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

Structure of a JWT

A JWT consists of three parts separated by dots (.):

  1. Header
  2. Payload
  3. Signature

Header

The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

Example:

{
  "alg": "HS256",
  "typ": "JWT"
}        

Payload

The payload contains the claims. Claims are statements about an entity (typically, the user) and additional metadata. There are three types of claims: registered, public, and private.

Example:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}        

Signature

To create the signature part, you have to take the encoded header, the encoded payload, a secret, and the algorithm specified in the header, and sign that.

For example, if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)        

Use Cases for JWT

  1. Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token.
  2. Information Exchange: JWTs are a good way of securely transmitting information between parties. Since JWTs can be signed, you can be sure the senders are who they say they are. Additionally, the structure of a JWT allows you to verify that the content hasn't been tampered with.

Best Practices

  1. Keep it Secret, Keep it Safe: The signing key should be treated like any other credential and kept secret.
  2. Set Appropriate Expiration: Tokens should have an expiration time to limit their lifespan and reduce potential misuse.
  3. Use HTTPS: Always use HTTPS to ensure your tokens are not intercepted during transmission.
  4. Validate Tokens: Always validate the token signature and the claims to ensure the token is legitimate and not tampered with.
  5. Don't Store Sensitive Data in the Payload: JWTs are not encrypted by default. Don't store sensitive information in the JWT payload.

Issuing and Verifying JWTs in Node.js with TypeScript

Setup

First, let's set up a Node.js project with TypeScript.

  1. Initialise the project:

mkdir jwt-nodejs-sample
cd jwt-example
npm init -y        

2. Install dependencies:

npm install express jsonwebtoken bcrypt mongoose
npm install --save-dev typescript @types/node @types/express @types/jsonwebtoken @types/bcrypt ts-node-dev @types/mongoose        

3. Initialise TypeScript:

npx tsc --init        

4. Configure TypeScript (tsconfig.json):

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}        

Create the following directory structure:

src/
├── controllers/
│   └── authController.ts
├── middlewares/
│   └── authMiddleware.ts
├── models/
│   └── User.ts
├── routes/
│   └── authRoutes.ts
├── types/
│   └── express/index.d.ts
└── index.ts        

Let's create User Model

src/models/User.ts:

import { Schema, model, Document, Model, Types } from 'mongoose';
import bcrypt from 'bcrypt';

interface IUser extends Document {
  _id: Types.ObjectId;
  username: string;
  email: string;
  password: string;
  comparePassword(candidatePassword: string): Promise<boolean>;
}

const userSchema = new Schema<IUser>({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});

userSchema.pre<IUser>('save', async function (next) {
  if (!this.isModified('password')) {
    return next();
  }
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

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

const User: Model<IUser> = model<IUser>('User', userSchema);

export { User, IUser };        

Let's create Authentication Middleware

src/middlewares/authMiddleware.ts:

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { User, IUser } from '../models/user';
import { Types } from 'mongoose';

export interface AuthRequest extends Request {
  user?: IUser;
}

export const authMiddleware = async (req: AuthRequest, res: Response, next: NextFunction) => {
  const token = req.cookies.jwt;

  if (!token) {
    return res.status(401).json({ message: 'No token, authorization denied' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { id: Types.ObjectId };
    const user = await User.findById(decoded.id).select('-password');
    if (!user) {
      return res.status(401).json({ message: 'Token is not valid' });
    }
    (req as AuthRequest).user = user;
    next();
  } catch (err) {
    res.status(401).json({ message: 'Token is not valid' });
  }
};        

Let's create Authentication Controller

src/controllers/authController.ts:

import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { User, IUser } from '../models/user';
import { Types } from 'mongoose';
import { AuthRequest } from '../midlewares/authMiddleware';

const generateToken = (id: Types.ObjectId) => {
  return jwt.sign({ id }, process.env.JWT_SECRET!, { expiresIn: '1h' });
};

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

  try {
    const user: IUser = new User({ username, email, password });
    await user.save();

    const token = generateToken(user._id);
    res.cookie('jwt', token, { httpOnly: true, maxAge: 3600000 });

    res.status(201).json({ message: 'User registered successfully', user: { username, email } });
  } catch (error) {
    const err = error as Error;
    res.status(400).json({ message: err.message });
  }
};

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

  try {
    const user = await User.findOne({ email });

    if (!user || !(await user.comparePassword(password))) {
      return res.status(400).json({ message: 'Invalid credentials' });
    }

    const token = generateToken(user._id);
    res.cookie('jwt', token, { httpOnly: true, maxAge: 3600000 });

    res.status(200).json({ message: 'User logged in successfully', user: { username: user.username, email: user.email } });
  } catch (error) {
    const err = error as Error;
    res.status(400).json({ message: err.message });
  }
};

export const logout = async (req: Request, res: Response) => {
  res.cookie('jwt', '', { httpOnly: true, expires: new Date(0) });
  res.status(200).json({ message: 'User logged out successfully' });
};

export const getProfile = async (req: Request, res: Response) => {
    try {
      const user = (req as AuthRequest).user;
      if (!user) {
        return res.status(404).json({ message: 'User not found' });
      }
      res.status(200).json({ user: { username: user.username, email: user.email } });
    } catch (error) {
      const err = error as Error;
      res.status(500).json({ message: err.message });
    }
  };        

Let's create Authentication Routes

src/routes/authRoutes.ts:

import { Router } from 'express';
import { register, login, logout, getProfile } from '../controllers/authController';
import { authMiddleware } from '../midlewares/authMiddleware';

const router = Router();

router.post('/register', register);
router.post('/login', login);
router.get('/logout', authMiddleware, logout);
router.get('/profile', authMiddleware, getProfile);

export default router;        

Let's create Main Application File

src/index.ts:

import express from 'express';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
import authRoutes from './routes/authRoutes';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(express.json());
app.use(cookieParser());

app.use('/api/auth', authRoutes);

mongoose.connect(process.env.MONGO_URI!)
  .then(() => {
    app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
  })
  .catch(err => console.error(err));        

JWT is a powerful tool for handling authentication and authorization in web applications. Understanding its structure, use cases, and best practices can help you implement secure and efficient authentication mechanisms. With the practical example provided, you should be able to set up JWT-based authentication in a Node.js application using TypeScript. Remember to always validate tokens, use HTTPS, and keep your signing keys secure.


Connect with me on LinkedIn. Thanks for reading

Parathan Thiyagalingam

Software Engineer | Full Stack Developer | AWS Community Builder | Educator & Content Creator | Academic | Tech Enthusiast

8 个月

AVA? - An Orange Education Label May I know what made you feel Funny on this? ??

回复

要查看或添加评论,请登录

Parathan Thiyagalingam的更多文章