InTab

Blog

PASETO tokens: the complete guide

By Uxair Ijaz · 2026-10-24 · 10 min read

PASETO (Platform-Agnostic SEcurity TOkens) is a format for the same job as a JWT: carrying a small set of claims, such as "this is user 42 and the token expires at 15:00", from one system to another in a way that cannot be tampered with. The difference is in what PASETO refuses to let you choose. A JWT lets the token, the library and the developer pick from dozens of algorithms, and many real attacks came from those choices. PASETO fixes the cryptography for each version, so there is nothing to pick and nothing to get wrong.

This guide covers how a PASETO is built, the difference between local and public tokens, what goes in the payload, the footer and implicit assertions, how to manage keys, and how to move from JWTs. Every example can be reproduced in the PASETO tool, which runs entirely in your browser.

What a PASETO looks like

v4.public.eyJzdWIiOiJ1c2VyLTQyIiwiZXhwIjoiMjAyNi0xMC0yNFQxNTowMDowMCswMDowMCJ9vmLsVi…Ag.eyJraWQiOiJrZXktMjAyNiJ9

A token has three or four parts separated by dots:

Part Example Meaning
Version v4 Which fixed set of algorithms is used
Purpose public local (encrypted) or public (signed)
Payload eyJzdWIi… Base64URL bytes: the message plus the nonce, tag or signature
Footer eyJraWQi… Optional, Base64URL; readable by anyone but protected from changes

The version and purpose together decide every algorithm. There is no alg field to forge, no none algorithm, and no way to persuade a server that an RSA public key is really an HMAC secret. Those three tricks are behind some of the best-known JWT vulnerabilities.

Versions: use v4

Version local (symmetric) public (asymmetric) Status
v1 AES-256-CTR + HMAC-SHA384 RSA-PSS Deprecated
v2 XChaCha20-Poly1305 Ed25519 Deprecated, replaced by v4
v3 AES-256-CTR + HMAC-SHA384 ECDSA P-384 Current, for NIST-only environments
v4 XChaCha20 + BLAKE2b-MAC Ed25519 Current, recommended

Pick v4 unless a compliance rule forces NIST algorithms, in which case pick v3. The two are designed so that switching between them is a configuration change, not a redesign. Never accept both "just in case": a server should accept exactly one version and purpose per key.

local vs public: the most important decision

PASETO has two purposes, and choosing between them is the main design decision you make.

v4.local: encrypted, one shared key

A local token is encrypted and authenticated with a single 32-byte secret key. Nobody without the key can read the payload, and nobody without the key can create or change a token.

Use it when the same party creates and checks the token, for example:

  • a session token your own API issues and later reads back
  • a password-reset or email-confirmation link
  • state you hand to the browser and expect back unchanged

Because one key does everything, any service that can verify a local token can also mint new ones. That is fine inside one application and a problem across organisations.

v4.public: signed, readable by anyone

A public token is signed with an Ed25519 secret key and verified with the matching public key. The payload is not encrypted: anyone holding the token can Base64URL-decode it and read every claim.

Use it when one party issues and many verify, for example:

  • an identity service issuing access tokens that several APIs check
  • tokens a partner verifies with a public key you publish
  • any case where verifiers must not be able to create tokens themselves
v4.local v4.public
Payload readable without the key No Yes
Who can verify Holders of the secret key Anyone with the public key
Who can create tokens Holders of the secret key Only the private key holder
Key size 32-byte symmetric key 32-byte seed, 32-byte public key
Overhead 32-byte nonce + 32-byte tag 64-byte signature

A quick rule: if you would have used HS256 with a JWT and you also want the payload hidden, use v4.local. If you would have used RS256 or ES256, use v4.public.

How v4.local works

You do not need this to use PASETO, but it explains why the format is hard to misuse.

  1. Generate a fresh 32-byte random nonce n for every token.
  2. Derive two keys from your key k and that nonce with BLAKE2b: an encryption key (plus a 24-byte cipher nonce) and a separate authentication key.
  3. Encrypt the message with XChaCha20.
  4. Compute a BLAKE2b-MAC over the header, nonce, ciphertext, footer and implicit assertion, joined with PAE.
  5. The token is v4.local. + Base64URL(nonce ‖ ciphertext ‖ tag), plus .footer if there is one.

Two details matter. Deriving fresh keys per token means a repeated nonce, already astronomically unlikely at 32 bytes, still does not expose your main key. And the MAC covers the header and footer, so changing v4.local to anything else, or editing the footer, breaks the token.

Pre-Authentication Encoding (PAE)

Signing header + payload as one string is ambiguous: "ab" + "c" and "a" + "bc" produce the same bytes. PAE removes that ambiguity by writing the number of pieces, then each piece's length, then the piece:

PAE(h, m, f) = LE64(3) ‖ LE64(len h) ‖ h ‖ LE64(len m) ‖ m ‖ LE64(len f) ‖ f

LE64 is a 64-bit little-endian length. Because every boundary is spelled out, no two different sets of pieces can ever produce the same signed bytes.

How v4.public works

  1. Build PAE("v4.public.", message, footer, implicit).
  2. Sign it with Ed25519.
  3. The token is v4.public. + Base64URL(message ‖ 64-byte signature), plus .footer.

Ed25519 is deterministic (the same input always gives the same signature), fast, and has no parameters to choose. Verification checks the signature against the public key and only then returns the message.

The payload: claims

The payload is a JSON object. PASETO reserves these claim names:

Claim Meaning Example
iss Issuer "https://auth.example.com"
sub Subject, usually a user id "user-42"
aud Audience: who the token is for "https://api.example.com"
exp Expires at "2026-10-24T15:00:00+00:00"
nbf Not valid before "2026-10-24T14:00:00+00:00"
iat Issued at "2026-10-24T14:00:00+00:00"
jti Token id, for revocation lists and replay checks "8f14e45f-ceea-467a-a866-051e6b1b3f2c"

The big difference from JWT: times are ISO 8601 strings with a time zone, not Unix seconds. "2026-10-24T15:00:00+00:00" is unambiguous to a human reading logs and leaves no room for a seconds-versus-milliseconds mix-up. In the PASETO tool, iat now and exp 1 h fill these in for you.

{
  "iss": "https://auth.example.com",
  "sub": "user-42",
  "aud": "https://api.example.com",
  "iat": "2026-10-24T14:00:00+00:00",
  "exp": "2026-10-24T15:00:00+00:00",
  "jti": "8f14e45f-ceea-467a-a866-051e6b1b3f2c",
  "role": "editor"
}

Add your own claims freely, but keep tokens small: they travel in every request header.

Checking the signature is not checking the claims. A library that verifies a token proves it was issued by your key and not changed. Rejecting expired tokens, the wrong audience or the wrong issuer is a separate step, and you must do it on every request.

The footer

The footer is an optional fourth part. It is authenticated (it cannot be changed without breaking the token) but never encrypted, even in local tokens.

Its main use is the key id, so a server knows which key to verify with before it has verified anything:

{"kid": "k4.lid.Y5x9-cq8hN9kxPU8Wg6_RpwYqCHGSQZbaTOyFWuKAyCn"}

Rules for the footer:

  • No secrets, no personal data. Anyone can read it.
  • Read it, but do not trust it until the token has been verified. Use the kid only to look up a key from your own list of allowed keys, never to fetch a key from a URL the footer supplies.
  • Keep it short; it is sent with every token.

Implicit assertions

v3 and v4 add a feature JWT has no equivalent for: the implicit assertion. It is extra data that is included in the MAC or signature but not stored in the token. The verifier must supply exactly the same value, or verification fails.

That lets you bind a token to its context without revealing the context:

  • a tenant or account id, so a token issued for one tenant fails for another
  • a hash of the user's device or session id
  • the database row the token is allowed to act on

If an attacker copies a token into another context, it simply does not verify. In the PASETO tool, type a value in Implicit assertion when creating a token, then try verifying with and without it.

Keys

Generating them

  • v4.local needs exactly 32 random bytes from a cryptographically secure generator. Do not use a password, and do not hash a password into a key: use a proper key-derivation function if a password is all you have.
  • v4.public needs an Ed25519 key pair. The secret key is a 32-byte seed (often stored as 64 bytes: seed plus public key). The public key is 32 bytes and can be shared freely.

The tool's Generate buttons use your browser's secure random source.

One key, one purpose

A key belongs to one version and one purpose. Never use the same bytes as a v4.local key and as a v4.public seed, and never use a PASETO key for anything else. The specification calls this algorithm lucidity, and good libraries enforce it by wrapping keys in typed objects.

Storing and sharing them

The companion standard PASERK gives keys a self-describing text form, such as k4.local.…, k4.public.… and k4.secret.…, plus short key ids (k4.lid.…, k4.pid.…) for the footer. Use it if your library supports it; it stops a key from being used with the wrong version or purpose.

Keep secret keys in a secrets manager or environment configuration, never in source code. Public keys can be published, for example at a well-known URL your verifiers fetch and cache.

Rotating them

  1. Create a new key and give it a new id.
  2. Start issuing tokens with the new key and its id in the footer.
  3. Keep accepting the old key until every token it signed has expired.
  4. Remove the old key.

Short expiry times make rotation fast and painless.

PASETO vs JWT

JWT PASETO v4
Algorithm choice In the header, many options Fixed by version and purpose
alg: none and algorithm confusion Possible if the library allows it Impossible by design
Encrypted tokens JWE, a separate and complex standard v4.local, built in
Time claims Unix seconds ISO 8601 strings
Binding to context Not available Implicit assertions
Ecosystem Everywhere: OAuth, OpenID Connect, every identity provider Libraries for most languages, far fewer integrations

PASETO is the safer format; JWT is the more widely supported one. If you need OpenID Connect or a third-party identity provider, you will receive JWTs and should validate them carefully (see What is a JWT?). If you control both ends, PASETO removes a whole class of mistakes.

Moving from JWT to PASETO

  1. Map the algorithm. HS256 becomes v4.local (and the payload becomes private as a bonus). RS256, PS256, ES256 and EdDSA become v4.public.
  2. Convert time claims from Unix seconds to ISO 8601 strings. The timestamp converter helps check values by hand.
  3. Move kid from the JWT header to the PASETO footer.
  4. Generate new keys. Do not reuse an HMAC secret or an RSA key.
  5. Run both formats side by side for one token lifetime: accept JWTs and PASETOs, issue only PASETOs, then drop JWT support.

Mistakes to avoid

  • Putting secrets in a v4.public payload. It is signed, not encrypted. Anyone can read it.
  • Using v4.local when other parties must verify. They would need the secret key, which also lets them mint tokens.
  • Skipping claim checks after verification: exp, nbf, aud and iss are your job.
  • Trusting the footer before verifying, or letting it choose a key URL.
  • Accepting several versions or purposes for one endpoint "for compatibility".
  • Long-lived tokens. Like a JWT, a PASETO cannot be revoked before it expires unless you keep a list of revoked jti values. Prefer short access tokens and refresh them.
  • Weak keys. A local key must be 32 random bytes, not a password.

Try it

Open the PASETO tool and click Load sample, or:

  1. Choose Create, then local or public.
  2. Write the claims, click iat now and exp 1 h, and optionally add a footer and an implicit assertion.
  3. Click Generate for a key, then copy the token.
  4. Switch to Verify, paste the token and the key (and the same implicit assertion), and check the result and the expiry badges.

Keys and tokens never leave your browser. For comparison, open the same claims in the JWT encoder / decoder and see how much more of the token is readable.

Tools in this article

  • PASETO Tokens: Create and verify PASETO v4 tokens: v4.local (encrypted) and v4.public (Ed25519 signed).
  • JWT Encoder / Decoder: Decode, verify and sign JSON Web Tokens: HS, RS, PS, ES and EdDSA, with key generation.
  • Timestamp Converter: Convert Unix timestamps to dates and back.