JWT vs PASETO v2 vs TECTO: Choosing the Right Token Protocol in 2026

Published: (February 21, 2026 at 11:13 PM EST)
9 min read
Source: Dev.to

Source: Dev.to

Tokens are everywhere in modern auth flows. But not all tokens are created equal.
In this post we compare three approaches side‑by‑side — classic JWT (HS256), the more modern PASETO v2, and the brand‑new TECTO — across security, ergonomics, and real‑code examples.


Quick Comparison Table

PropertyJWT (HS256)PASETO v2TECTO
Payload visible?✅ Yes (base64)✅ Yes (signed, not encrypted)❌ Fully encrypted
CipherNone (HMAC)Ed25519 (sign) / XChaCha20 (encrypt)XChaCha20‑Poly1305
NonceN/A24‑byte per token24‑byte CSPRNG per token
Key sizeVariableVariableExactly 256‑bit (enforced)
Tamper detectionHMAC signatureEd25519 / Poly1305 tagPoly1305 auth tag
Error specificityReveals reasonReveals reasonGeneric “Invalid token”
Algo‑confusion attacks⚠️ Yes (the alg: none problem)✅ No✅ No
Key rotation built‑in❌ DIY❌ DIY✅ Native (kid in token)

1️⃣ JWT – The Old Faithful

jsonwebtoken is the most widely used token library in Node.js. It’s battle‑tested, has a massive ecosystem, and is dead‑simple to start with.

npm install jsonwebtoken
import jwt from "jsonwebtoken";

const SECRET = "my-secret-key"; // ← This is the problem

// Sign
const token = jwt.sign(
  { userId: 42, role: "admin" },
  SECRET,
  { expiresIn: "1h", issuer: "my-app" }
);

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMCwiaXNzIjoibXktYXBwIn0.SIGNATURE

// Verify
const payload = jwt.verify(token, SECRET) as { userId: number; role: string };
console.log(payload.userId); // 42

Payload is readable

// Decode the middle segment of any JWT
const [, payload] = token.split(".");
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
console.log(decoded);
// {"userId":42,"role":"admin","iat":1700000000,"exp":1700003600,"iss":"my-app"}

Anyone who intercepts the token can read the payload – no key needed. This is by design: JWTs are signed, not encrypted. Many developers don’t realize this at first.

Algorithm‑confusion problem

JWT allows the algorithm to be specified in the header. This led to the infamous alg: none attack where attackers could forge tokens by setting the algorithm to none. Even with modern libraries that block none, HMAC vs RSA confusion attacks are still a real concern if you accept tokens from multiple issuers.

When JWT makes sense

  • Public, non‑sensitive payloads (user IDs, roles)
  • Integrating with third‑party services that require JWT (OAuth, OIDC)
  • Your team already has JWT infrastructure

2️⃣ PASETO – Platform‑Agnostic Security Tokens

PASETO was designed to fix JWT’s foot‑guns. It removes algorithm agility entirely – you pick a version and you get a fixed, well‑chosen algorithm. No alg: none, no confusion attacks.

npm install paseto
import { V2 } from "paseto";

const key = await V2.generateKey("local");

// ---- Encrypt (v2.local) ----
const token = await V2.encrypt(
  { userId: 42, role: "admin" },
  key,
  { expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

// Decrypt
const payload = await V2.decrypt(token, key);
console.log(payload.userId); // 42

// ---- Sign (v2.public) ----
const { privateKey, publicKey } = await V2.generateKey("public");

// Sign (payload is visible, like JWT)
const signed = await V2.sign({ userId: 42 }, privateKey, { expiresIn: "1h" });

// Verify
const verified = await V2.verify(signed, publicKey);

Highlights

  • No algorithm confusion – the version (v2) pins the algorithm.
  • v2.local encrypts the payload (XChaCha20‑Poly1305).
  • Clean, modern async API.
  • No native key‑rotation storykid is not part of the token format; you must manage key versioning yourself.
  • Error messages can still reveal the failure mode.
  • No entropy validation on keys – you can pass a weak key and the library will silently accept it.

3️⃣ TECTO – Transport Encrypted Compact Token Object

TECTO takes a different philosophy: every token is fully encrypted, always. There is no “signed‑but‑readable” mode.

bun add tecto
import {
  generateSecureKey,
  MemoryKeyStore,
  TectoCoder,
  InvalidSignatureError,
  TokenExpiredError,
  assertEntropy,
} from "tecto";

// 1️⃣ Generate a cryptographically secure 256‑bit key
const key = generateSecureKey();

// 2️⃣ Set up the key store
const store = new MemoryKeyStore();
store.addKey("my-key-2026", key);

// 3️⃣ Create a coder
const coder = new TectoCoder(store);

// 4️⃣ Encrypt
const token = coder.encrypt(
  { userId: 42, role: "admin" },
  { expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// tecto.v1.my-key-2026.base64url_nonce.base64url_ciphertext

// 5️⃣ Decrypt
const payload = coder.decrypt(token);
console.log(payload.userId); // 42

Token format

tecto.v1...
  • The kid (key ID) is embedded in the token itself, enabling native key rotation – no extra metadata or headers needed.
// Rotate keys
store.addKey("key-2026-01", oldKey);          // old key still decrypts old tokens
store.rotate("key-2026-06", newKey);          // new tokens use the new key
store.removeKey("key-2026-01");              // drop old key when it’s no longer needed

Strong key‑entropy checks

// These all throw KeyError
assertEntropy(new Uint8Array(32));                 // all zeros
assertEntropy(new Uint8Array(32).fill(0xaa));      // repeating byte
assertEntropy(new Uint8Array(16));                 // wrong length

// This is safe
const goodKey = generateSecureKey(); // always high‑entropy, 256‑bit
assertEntropy(goodKey); // ✅ passes

Uniform error handling

try {
  coder.decrypt(tamperedToken);
} catch (err) {
  if (err instanceof InvalidSignatureError) {
    // err.message === "Invalid token"
    // You don’t know *why* it failed – that’s intentional.
    // Attackers can’t probe the system by watching error messages.
  }
  if (err instanceof TokenExpiredError) {
    // Token is simply expired – safe to tell the client.
  }
}

When TECTO shines

  • You need full confidentiality for every claim.
  • Native key‑rotation without extra plumbing.
  • Uniform, non‑informative error messages to thwart probing attacks.

TL;DR

FeatureJWT (HS256)PASETO v2TECTO
Payload readable?✅ (base64)✅ (signed)❌ (encrypted)
Fixed algorithm?❌ (alg header)✅ (version pins)✅ (built‑in)
Native key rotation?✅ (kid in token)
Strong key‑size enforcement?✅ (256‑bit)
Uniform error messages?
Best for…Public, non‑sensitive data; existing JWT ecosystemsModern apps that want signed tokens + optional encryptionAnything that must stay confidential and benefit from built‑in rotation

Pick the token format that matches your security and operational requirements – and don’t assume “signed = safe. Encryption matters.


Token Formats Compared

Below is a quick‑look comparison of JWT, PASETO v2, and TECTO.
All three can carry the same payload – e.g. { userId: 42, role: "admin" } – but they differ dramatically in how the data is protected.

Concrete example

// Payload we want to transport
{ userId: 42, role: "admin" }

JWT (signed only)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ   ← base64 of { userId: 42, role: "admin" }
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Anyone can run atob() on the middle segment and read the payload – no secret key is required.

PASETO v2.local (encrypted)

v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFULL_CIPHERTEXT

Encrypted with XChaCha20‑Poly1305. Without the key the ciphertext is opaque.

TECTO (encrypted + built‑in key‑id)

tecto.v1.my-key-2026.NONCE_BASE64URL.CIPHERTEXT_BASE64URL

Encrypted with XChaCha20‑Poly1305.
The kid (my‑key‑2026) is visible only as a label; the payload remains unreadable without the key.


Feature Matrix

FeatureJWTPASETO v2TECTO
Key ID in token❌ Not standard❌ Not standard✅ Built‑in kid
Old token decryptable after rotationDIYDIYstore.rotate() handles it
Revoke old keyDIYDIYstore.removeKey() zeroes memory
Entropy validation❌ No❌ NoassertEntropy() enforced
Tamper‑evidence✅ HMAC❌ Payload readable by anyone✅ Authenticated encryption (XChaCha20‑Poly1305)
Algorithm agility❌ Algorithm confusion attack surface✅ Version pins algorithm✅ Version pins algorithm
Native key rotation❌ No❌ No✅ With kid
Generic errors (no oracle attacks)
Timing‑safe comparisons
Memory zeroing on key removal
Payload size limits (DoS prevention)
Type‑checked registered claims

When to Use Which Token

  • Use JWT if you need:

    • Compatibility with OAuth / OIDC / existing infrastructure
    • Payloads that contain no sensitive data (just IDs, roles, etc.)
    • Integration with third‑party services that only understand JWT
  • Use PASETO v2.local if you want:

    • A well‑audited, standardized encrypted token
    • Interoperability across many languages / platforms
    • No built‑in native key rotation (you’ll handle rotation yourself)
  • Use TECTO if you want:

    • Encryption‑by‑default with zero configuration mistakes possible
    • Native key rotation without extra infrastructure
    • A greenfield TypeScript/Bun project
    • Defense‑in‑depth: entropy validation, generic errors, timing safety, memory zeroing, payload size limits, etc.

Quick‑Start with TECTO

bun add tecto
import { generateSecureKey, MemoryKeyStore, TectoCoder } from "tecto";

// 1️⃣ Create a key store and add a key
const store = new MemoryKeyStore();
store.addKey("v1", generateSecureKey());

// 2️⃣ Build a coder that knows about the store
const coder = new TectoCoder(store);

// 🔐 Encrypt a payload (expires in 1 hour)
const token = coder.encrypt({ userId: 42 }, { expiresIn: "1h" });

// 🔓 Decrypt it back (type‑checked)
const { userId } = coder.decrypt(token);

Persistent key storage

TECTO ships with adapters for SQLite, PostgreSQL, and MariaDB – all implement the same KeyStoreAdapter interface, so swapping the backend is trivial.


Bottom Line

  • JWT will stay the de‑facto standard for federated auth and OAuth flows – and that’s fine for public, non‑sensitive claims.
  • PASETO v2.local gives you a solid, standardized encrypted token, but you must manage rotation and entropy yourself.
  • TECTO goes a step further: batteries‑included key rotation, mandatory entropy checks, generic error handling, timing‑safe comparisons, and automatic memory zeroing.

The best token protocol is the one you can’t misconfigure.
TECTO makes a strong case for that.

0 views
Back to Blog

Related posts

Read more »