Back to blog
July 9, 2026

The Ultimate PASETO Tutorial: Implementing v4 Tokens in Node.js

A complete step-by-step guide to implementing PASETO v4 in Node.js. Learn how to generate keys, sign public tokens, encrypt local tokens, and write robust authentication middleware.

The Ultimate PASETO Tutorial: Implementing v4 Tokens in Node.js

JSON Web Tokens (JWT) have historically been the default standard for stateless sessions, but their design contains a critical architectural flaw: algorithm agility. By trusting the token header to define the verification algorithm, JWTs expose applications to algorithm confusion, downgrade attacks, and configuration footguns.

PASETO (Platform-Agnostic Security Tokens) eliminates these risks by design. It replaces algorithm negotiation with strict, version-locked cryptosuites. In this tutorial, we will write a complete implementation of PASETO Version 4 (v4) in a Node.js environment, covering both symmetric (local) and asymmetric (public) tokens, key generation, and middleware integration.


The PASETO v4 Paradigm: Local vs. Public

Unlike JWT, which can be configured in a hundred different ways, PASETO separates tokens into two explicit purpose-built formats:

  1. v4.local (Symmetric Encryption): The payload is encrypted using a shared secret key (using XChaCha20-Poly1305). The token contents are invisible to the client. Ideal for internal session tokens or service-to-service communication.
  2. v4.public (Asymmetric Signatures): The payload is plaintext (Base64URL-encoded) but digitally signed using a private key (using Ed25519). Anyone can inspect the payload, but it cannot be tampered with. Ideal for public APIs and OAuth/OIDC flows.

Step 1: Installing the Correct Library

In the Node.js ecosystem, the gold standard library for PASETO is the official paseto package, which is built directly on top of Node's native crypto module.

Open your terminal and install it:

npm install paseto

Step 2: Implementing Symmetric Encryption (v4.local)

Symmetric tokens require a secure 32-byte (256-bit) shared secret key. Both the issuing server and the verifying server must possess this key.

Generating a Symmetric Key

Never use weak password strings for cryptographic keys. Use Node’s crypto module to generate a cryptographically secure random key, and export it as a base64 or hex string:

import { randomBytes } from 'crypto';

// Generate a secure 32-byte key
const key = randomBytes(32).toString('base64');
console.log('Secret Key (Store in env):', key);

Encrypting and Decrypting the Token

Here is how you encrypt user session data into a v4.local token and decrypt it upon receipt:

import { V4 } from 'paseto';

// Convert the stored base64 secret key back to a Buffer
const secretKey = Buffer.from(process.env.PASETO_SECRET_KEY, 'base64');

// 1. Define the claims payload
const payload = {
  sub: 'user_128937',
  role: 'editor',
  exp: '2h' // Expires in 2 hours
};

// 2. Encrypt to generate a v4.local token
async function issueLocalToken() {
  const token = await V4.encrypt(payload, secretKey, {
    footer: { kid: 'key-v1' } // Optional unencrypted metadata
  });
  return token;
}

// 3. Decrypt and validate the token
async function verifyLocalToken(token) {
  try {
    const decrypted = await V4.decrypt(token, secretKey);
    console.log('Successfully Decrypted:', decrypted);
    return decrypted;
  } catch (error) {
    console.error('Decryption failed:', error.message);
    throw new Error('Invalid or expired token');
  }
}

Step 3: Implementing Asymmetric Signatures (v4.public)

Asymmetric tokens are useful when your authentication server issues tokens, but multiple downstream microservices need to verify them without having the power to forge them.

For this, PASETO v4 mandates Ed25519, a highly performant and secure Edwards-curve digital signature algorithm.

Generating Ed25519 Keypairs

You can generate a secure Ed25519 keypair natively:

import { generateKeyPairSync } from 'crypto';

const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

console.log('Private Key (Secret):', privateKey);
console.log('Public Key (Distribute):', publicKey);

Signing and Verifying the Token

The authentication server signs the payload with the private key, and the consuming server verifies the signature using the public key:

import { V4 } from 'paseto';

// 1. Sign payload with the Private Key (v4.public)
async function issuePublicToken(payload, privateKeyPem) {
  const token = await V4.sign(payload, privateKeyPem, {
    expiresIn: '1h'
  });
  return token;
}

// 2. Verify payload with the Public Key
async function verifyPublicToken(token, publicKeyPem) {
  try {
    const payload = await V4.verify(token, publicKeyPem);
    console.log('Token Verified. Payload:', payload);
    return payload;
  } catch (error) {
    console.error('Signature verification failed:', error.message);
    throw new Error('Token verification failed');
  }
}

Step 4: Writing Express.js Authentication Middleware

Integrating PASETO v4 verification into your backend router is straightforward. Here is a production-ready middleware function for an Express application:

import express from 'express';
import { V4 } from 'paseto';

const app = express();
const PUBLIC_KEY_PEM = process.env.PASETO_PUBLIC_KEY;

async function authenticatePaseto(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    // Verify the asymmetric public token
    const claims = await V4.verify(token, PUBLIC_KEY_PEM);
    
    // Attach the validated claims to the request object
    req.user = claims;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid or expired security token' });
  }
}

// Protected route example
app.get('/api/dashboard', authenticatePaseto, (req, res) => {
  res.json({ message: `Welcome, user ${req.user.sub}!` });
});

Debugging and Inspecting PASETO Tokens Safely

During development, you will inevitably need to inspect the payload of the tokens you generate to ensure claims (like exp, sub, or key IDs in the footer) are correctly formatted.

Crucial Security Warning: Never paste production tokens containing private claims or user data into third-party, web-based decoders that transit data to their servers. Doing so exposes your tokens to server logs and potential leakage.

FmtDev provides a 100% Client-Side PASETO Decoder. All decoding, parsing, and formatting run entirely within your browser's local sandbox—no token data ever leaves your computer, ensuring absolute privacy.

Interactive Example
Local Execution
v4.public.eyJzdWIiOiJ1c2VyXzEyODkzNyIsInJvbGUiOiJlZGl0b3IiLCJleHAiOiIyMDI2LTA3LTA2VDIwOjE0OjQzWiJ9TzhnbW9kdWxleA

Clicking will load this data into the tool locally.


Conclusion

By eliminating the dangerous algorithm flexibility of the JWT specification, PASETO v4 gives backend developers a secure-by-default environment. Implementing it in Node.js requires minimal boilerplate while providing strong cryptographic guarantees.

For a deeper analysis of the performance overhead and architectural differences between these two stateless token standards, read our detailed guide on PASETO vs JWT Cryptographic Hardening.

Related Tool

Ready to use the Our Secure Tool tool? All execution is 100% local.

Open Our Secure Tool