PASETO and the Case Against Configurable Tokens
#Security

PASETO and the Case Against Configurable Tokens

Backend Reporter
7 min read

JWT's flexibility is also its biggest liability. PASETO removes the knobs that cause production incidents by fixing the cryptography per protocol version. Here's how it works, where it fits in a distributed system, and what you give up to get it.

Most token security incidents are not cryptographic breaks. They are configuration mistakes. A library that accepts alg: none, a service that validates a signature with the wrong key type, an audience check that nobody wired up. JWT did not invent these problems, but its design surface makes them easy to create and hard to notice until a token forged in five minutes shows up in an auth log.

PASETO (Platform-Agnostic Security Tokens) takes the opposite position. Instead of letting each deployment assemble its own combination of algorithms and validation rules, it ships a small number of fixed, versioned protocols. You pick a version, and the cryptography is decided for you. The project lives at paseto.io, with the reference implementation and the protocol specification on GitHub.

Featured image

The problem: a token format with too many degrees of freedom

JWT is standardized across several RFCs, with RFC 7519 defining the claims and the JOSE family (RFC 7515 through 7518) defining the signing and encryption. The structure is a base64url header, payload, and signature. The header names the algorithm, and that single field is the root of a recurring class of failures.

The algorithm-confusion attack is the canonical example. A service that issues RS256 tokens, signed with an RSA private key, verifies them with the corresponding public key. If the verification code trusts the alg header rather than pinning it, an attacker can switch the header to HS256 and sign the token using the public key as an HMAC secret. The public key is public. The forgery validates. The fix is well known, pin the expected algorithm, but the format invites the mistake by treating the algorithm as negotiable data carried inside the token itself.

The none algorithm is the blunter version of the same flaw. It was specified for cases where integrity is guaranteed by some other layer, but a verifier that does not explicitly reject it will accept unsigned tokens as valid. These are not defects in the math. They are defects that emerge from giving the verifier choices it should never have had.

The approach: protocol versions instead of algorithm fields

PASETO removes the algorithm from the token. There is no field to spoof because there is nothing to choose at runtime. A token begins with its version and its purpose, for example v4.public. or v4.local., and the version completely determines the cryptographic primitives.

This is the design point worth getting right, because the most common write-up of PASETO gets it wrong. Versions and purposes are two separate axes.

Version is the protocol generation. The current versions are v3 and v4. (v1 and v2 still exist in the spec but are deprecated for new systems.) v3 uses NIST-friendly primitives, AES-256-CTR with HMAC-SHA384 for the symmetric case and ECDSA over P-384 for the asymmetric case, which matters if you have FIPS compliance constraints. v4 uses modern non-NIST primitives, XChaCha20 with BLAKE2b for symmetric and Ed25519 for asymmetric. You choose a version once, as a deployment decision, not per token.

Purpose is what the token gives you:

  • local tokens are symmetric. The payload is encrypted and authenticated with a single shared secret key. Anyone without the key sees ciphertext. This is the right tool when the issuer and verifier are the same trust domain, a service issuing session tokens it will later read back, or sensitive data you do not want the bearer to inspect.
  • public tokens are asymmetric. The payload is signed but not encrypted, so it is readable by anyone, and verified with the issuer's public key. This is the JWT-for-API-auth replacement. The signing key stays private to the issuer, and any number of services can verify with the public key without ever holding a secret that could forge tokens.

So v4.local and v4.public are both v4. The earlier framing that called local "version 1" and public "version 2" collapses two independent ideas into one and will lead you to the wrong key-management model. Keep them separate.

Why this matters in a distributed system

The local-versus-public split maps cleanly onto a real architectural distinction, and getting it wrong is where the consistency problems start.

Symmetric tokens need every party that issues or verifies them to hold the same secret. In a single service that round-trips its own sessions, that is one key in one place, simple and fast. Push that pattern across a fleet of services and the shared secret becomes a distribution and rotation problem. Now every verifier is also a potential forger, because the key that checks a token can mint one. Your blast radius on a key compromise is the entire set of services holding it.

Asymmetric tokens change the trust topology. The issuer holds a private signing key; verifiers hold only the public key. A leaked public key forges nothing. This is what makes public tokens the correct default for anything crossing a service boundary, gateway to backend, service to service, issuer to third party. You can distribute the verification key freely, even publish it, and the security property holds. The cost is that signing and verification are slower than HMAC and the tokens are somewhat larger, which is almost always an acceptable trade for the reduced blast radius.

Guardsquare image

The versioning also addresses a coordination problem that JWT handles poorly: migrating cryptography across a running system. Because the version is the first thing in the token and is part of what gets authenticated, a verifier can enforce a strict allowlist. Accept v4.public, reject everything else. When you move from v3 to v4, you run both validators during the transition and flip the issuer over once every verifier understands the new version. There is no window where a downgrade to a weaker primitive validates, because the weaker primitive is a different version string that your allowlist refuses.

What the payload does and does not give you

PASETO defines a small set of registered claims that mirror the JWT ones you actually use: iss, sub, aud, exp, nbf, iat, and jti. Compliant libraries validate the time-based claims and let you assert the issuer and audience. The audience check is the one teams forget, and it is the difference between a token that is valid and a token that is valid here. A signed token issued for analytics.internal should not authenticate against payments.internal, and the only thing stopping it is an explicit aud assertion at the verifier.

PASETO adds one mechanism JWT lacks: a separate, authenticated-but-unencrypted footer, plus an optional implicit assertion that is bound into the authentication tag without being stored in the token. The footer is useful for things like a key identifier that a verifier needs to read before it picks a key. The implicit assertion lets you bind a token to out-of-band context, a tenant ID, a request parameter, so that a token lifted from one context fails to validate in another, without putting that context on the wire.

What you do not get is JWT's broader ecosystem. There is no equivalent of the full JOSE/JWE/JWK stack, fewer off-the-shelf integrations with identity providers, and a smaller pool of libraries and operational knowledge. For a greenfield internal auth system that is rarely a problem. For something that has to interoperate with an existing OIDC deployment, the ecosystem gap is a real cost you weigh against the safety the format buys.

The trade-off, stated plainly

PASETO trades flexibility for a smaller failure surface. You cannot misconfigure the algorithm because you cannot configure it. You cannot accidentally accept none because there is no none. You cannot get tricked into HMAC-verifying with a public key because the version that uses public keys does not use HMAC. The whole design is an argument that in security-critical formats, configurability is a liability, and the safe path should be the only path.

What you give up is the ability to negotiate, to bridge to systems that already speak JWT and JOSE, and to lean on a large existing tooling base. You also inherit a younger ecosystem, though implementations exist across most major languages and the spec has stabilized around v3 and v4.

The honest recommendation: for new internal services, especially where you control both ends and want the secure default to be unavoidable, PASETO removes an entire category of incidents that JWT keeps re-creating. For systems that must interoperate with the existing identity ecosystem, JWT is not going anywhere, and the right move is to pin your algorithms, reject none, validate aud and exp, and treat the flexibility as something to lock down rather than use. Both formats can be deployed safely. PASETO just makes the unsafe deployment harder to reach by accident, and in a large system run by many hands over many years, that property is worth more than the flexibility it removes.

Comments

Loading comments...