In today’s web development landscape, securing APIs and managing user sessions are fundamental. JSON Web Tokens (JWT) have been the go-to choice for many developers for stateless authentication, but a newer, more secure alternative has emerged: Platform-Agnostic Security Tokens (PASETO).
In this comprehensive blog post, we'll dive deep into both JWT and PASETO, compare their features, and implement complete authentication systems using TypeScript with Express.js. We’ll also include a refresh token mechanism for better security and user experience.
What is JWT?
JWT (JSON Web Token) is a widely used format for securely transmitting information between parties. It consists of three parts:
Header: Contains metadata about the token and the algorithm used for signing.
Payload: Contains the claims or data you want to share securely.
Signature: Verifies that the token hasn’t been altered.
A sample JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cWhat is PASETO?
PASETO (Platform-Agnostic Security Tokens) is a modern alternative to JWT that addresses some of its security flaws. Unlike JWT, PASETO focuses on simplicity and strong cryptographic defaults, preventing insecure implementations.
A sample PASETO token looks like this:
v2.local.dXNlcl9pZDoxMjM0NTY3ODkwKey Differences Between JWT and PASETO
Instead of a table, let's break down the differences between JWT (JSON Web Token) and PASETO (Platform-Agnostic Security Tokens) into several categories. This way, we can highlight the contrasts in a more detailed, narrative format.
1. Security by Design
- JWT: Offers flexibility in terms of signing algorithms, but this can be a double-edged sword. JWT supports algorithms like HS256, RS256, and even the infamous none algorithm, which, if improperly configured, can make the token vulnerable to attacks (e.g., algorithm confusion attacks). Developers need to explicitly choose strong algorithms to avoid risks.
- PASETO: Prioritizes security by using only strong cryptographic algorithms. For instance, version 2 (V2) of PASETO uses XChaCha20-Poly1305 for encryption and Ed25519 for digital signatures. This limited choice reduces the chance of misconfiguration, making PASETO a safer choice by default.
2. Ease of Use
- JWT: Offers a wide variety of configuration options, which can be both a strength and a weakness. The flexibility allows JWT to be adapted for different use cases, but it can also lead to complexity and potential pitfalls, especially for developers who are not well-versed in cryptography.
- PASETO: Aims for simplicity and ease of use. The token format is straightforward, and the cryptographic choices are limited but strong. This makes it easier for developers to implement PASETO correctly without needing in-depth knowledge of cryptography.
3. Token Structure
- JWT: The token consists of three parts: Header, Payload, and Signature, separated by dots (.). The payload is base64-encoded JSON data, which can be easily decoded and read (even though it is signed, it is not encrypted by default). This visibility can expose sensitive data if not handled properly.
- Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG5kb2UifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c- PASETO: The token format has two versions (V1 and V2), with V2 being the most commonly used. PASETO tokens are either local (encrypted) or public (signed), and the content is always encrypted by default if using the local token. This ensures confidentiality without needing extra configuration.
- Example PASETO (V2 local):
v2.local.dXNlcl9pZDoxMjM0NTY3ODkw4. Compatibility and Adoption
- JWT: Has widespread adoption and is supported by many libraries across different languages and frameworks. It has become a de facto standard for token-based authentication in web applications and APIs.
- PASETO: While gaining popularity, PASETO is still relatively new compared to JWT. It is less widely supported but is being adopted by developers who prioritize security and prefer strong cryptographic defaults.
5. Use Cases
- JWT: Suitable for scenarios where flexibility and compatibility are key requirements. It’s a good choice for applications that need to work with existing standards and infrastructure (e.g., OAuth 2.0 and OpenID Connect).
- PASETO: Ideal for scenarios where security is the top priority and the complexity of cryptographic choices should be minimized. It’s particularly well-suited for new projects or systems where security best practices are paramount.
6. Performance
- JWT: Can have performance issues when using asymmetric encryption (e.g., RS256) due to the computational cost of RSA. However, symmetric algorithms like HS256 are faster but might compromise security if not used correctly.
- PASETO: Uses modern cryptographic primitives (e.g., XChaCha20-Poly1305), which are designed for high performance and efficiency. This can result in faster token encryption and decryption compared to some JWT algorithms.
Setting Up a TypeScript Project
To get started, set up your TypeScript-based Express.js project:
mkdir token-auth-example
cd token-auth-example
npm init -y
npm install express jsonwebtoken paseto @types/express @types/jsonwebtoken typescript ts-node
npx tsc --initUpdate your tsconfig.json:
"target": "ES6",
"moduleResolution": "node",
"esModuleInterop": true,Create a src folder and an index.ts file. We’ll implement both JWT and PASETO authentication, including a refresh token mechanism.
JWT Authentication with Refresh Tokens
src/jwtAuth.ts
Imports and Configuration:
import express, { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
const JWT_SECRET = 'your_jwt_secret';
const JWT_REFRESH_SECRET = 'your_jwt_refresh_secret';
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
interface JwtPayload {
username: string;
}
const refreshTokens: string[] = []; // Store refresh tokens (use a database in production)
Token Generation Functions:
const generateAccessToken = (payload: JwtPayload) => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
};
const generateRefreshToken = (payload: JwtPayload) => {
const refreshToken = jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
refreshTokens.push(refreshToken);
return refreshToken;
};
Login and Refresh Routes:
export const jwtLogin = (req: Request, res: Response) => {
const { username } = req.body;
if (!username) return res.status(400).send('Username is required');
const payload = { username };
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
res.json({ accessToken, refreshToken });
};
export const jwtRefreshToken = (req: Request, res: Response) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.status(403).send('Refresh token invalid or not found');
}
try {
const payload = jwt.verify(refreshToken, JWT_REFRESH_SECRET) as JwtPayload;
const newAccessToken = generateAccessToken({ username: payload.username });
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(403).send('Invalid refresh token');
}
};
JWT Middleware:
export const jwtAuth = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1];
if (!token) return res.status(401).send('Token missing');
try {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
req.body.user = decoded;
next();
} catch (error) {
res.status(403).send('Invalid or expired access token');
}
};
PASETO Authentication with Refresh Tokens
src/pasetoAuth.ts
Imports and Configuration:
import express, { Request, Response, NextFunction } from 'express';
import { V2 } from 'paseto';
const PASETO_SECRET = 'your_32_byte_secret_key';
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
interface PasetoPayload {
username: string;
}
const refreshTokens: string[] = [];
Token Generation Functions:
const generateAccessToken = async (payload: PasetoPayload) => {
return await V2.encrypt(payload, PASETO_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
};
const generateRefreshToken = async (payload: PasetoPayload) => {
const refreshToken = await V2.encrypt(payload, PASETO_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
refreshTokens.push(refreshToken);
return refreshToken;
};
Login and Refresh Routes:
export const pasetoLogin = async (req: Request, res: Response) => {
const { username } = req.body;
if (!username) return res.status(400).send('Username is required');
const payload = { username };
const accessToken = await generateAccessToken(payload);
const refreshToken = await generateRefreshToken(payload);
res.json({ accessToken, refreshToken });
};
export const pasetoRefreshToken = async (req: Request, res: Response) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.status(403).send('Refresh token invalid or not found');
}
try {
const payload = await V2.decrypt<PasetoPayload>(refreshToken, PASETO_SECRET);
const newAccessToken = await generateAccessToken({ username: payload.username });
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(403).send('Invalid refresh token');
}
};
PASETO Middleware:
export const pasetoAuth = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1];
if (!token) return res.status(401).send('Token missing');
try {
const payload = await V2.decrypt<PasetoPayload>(token, PASETO_SECRET);
req.body.user = payload;
next();
} catch (error) {
res.status(403).send('Invalid or expired access token');
}
};
Integrating Both in the Main Express Application
src/index.ts
import express from 'express';
import { jwtLogin, jwtRefreshToken, jwtAuth } from './jwtAuth';
import { pasetoLogin, pasetoRefreshToken, pasetoAuth } from './pasetoAuth';
const app = express();
app.use(express.json());
app.post('/jwt-login', jwtLogin);
app.post('/jwt-refresh', jwtRefreshToken);
app.get('/jwt-protected', jwtAuth, (req, res) => {
res.send(`Hello ${req.body.user.username}, you accessed a protected JWT route!`);
});
app.post('/paseto-login', pasetoLogin);
app.post('/paseto-refresh', pasetoRefreshToken);
app.get('/paseto-protected', pasetoAuth, (req, res) => {
res.send(`Hello ${req.body.user.username}, you accessed a protected PASETO route!`);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Conclusion
In this blog post, we explored both JWT and PASETO token-based authentication in a TypeScript-based Express application. We implemented:
- Authentication and verification middleware.
- Refresh token mechanisms for secure and seamless session management.
With this knowledge, you can choose the token strategy that best suits your application's needs and ensure a secure user experience.

