Base IdP

Base IdP

Concepts

Tokens & Verification

PASETO v4.public, Ed25519 signatures, key discovery, and exactly what is inside a verified principal.

Base IdP issues PASETO v4.public tokens. Not JWT. The format change is the biggest difference from other identity providers, so this page covers what PASETO is, what a Base IdP token looks like inside, how the SDK verifies it, and what you get back when verification succeeds.

If you have used JWT before, the mental model carries over almost completely. There is a header, a payload of claims, and a signature. The differences are in the details, and the SDK hides most of them.

What a token looks like

A PASETO v4.public token is a single string with four parts separated by periods.

v4.public.eyJpc3MiOiJodHRwczov...<base64url message>...AAAAA.eyJraWQiOiJrXzAxSFJaIn0

The four parts are:

  1. Versionv4.
  2. Purposepublic (asymmetric signing, Ed25519).
  3. Payload — base64url-encoded JSON claims, with the Ed25519 signature appended to the end of the binary message.
  4. Footer — base64url-encoded JSON, contains the key id (kid).

You will never have to read this by hand. The CLI has a token command that decodes a token into structured JSON, and the SDK does it as part of verification.

npx base-idp token v4.public.eyJ...
{
  "header": { "version": "v4", "purpose": "public" },
  "footer": { "kid": "k_01HRZ...", "alg": "v4.public", "typ": "paseto" },
  "claims": {
    "iss": "https://authlayer.squareexp.com",
    "sub": "usr_01HRZ...",
    "aud": "sq_live_yourapp",
    "exp": "2026-06-29T15:22:08.000Z",
    "nbf": "2026-06-29T14:21:38.000Z",
    "iat": "2026-06-29T14:22:08.000Z",
    "jti": "tok_01HRZ...",
    "gid": "gid_01HRZ...",
    "email": "emai@domain.com",
    "name": "First Name",
    "token_use": "access",
    "sid": "ses_01HRZ...",
    "scopes": ["openid", "profile"],
    "role": "operator"
  }
}

Why PASETO

The short version: JWT has an alg field that picks the signing algorithm at runtime, which has historically led to an entire family of bugs.

The most famous one is alg: none — a JWT library that respects the header field will happily accept a token with no signature if the attacker says "this token is unsigned." There are also algorithm-confusion attacks where an RS256 verifier is tricked into using HS256 with the public key as the HMAC secret.

PASETO removes the choice. A v4.public token is Ed25519, full stop. There is no alg field that can be lied about. The format itself is the security contract.

This is the kind of thing you do not need to think about as long as you use the SDK. The SDK only parses tokens that say v4.public. and only verifies them with Ed25519. There is no configuration that can make it do otherwise.

Do not use a JWT library

Never try to parse a Base IdP token with a JWT library. It will fail at best, or silently misbehave at worst. Use the Base IdP SDK for your language. The SDKs are tiny — there is no real cost to including one.

The verification chain

When the SDK verifies a token it runs four checks in order. If any check fails, the token is rejected and the SDK throws or returns an error.

The footer contains the kid (key id), alg, and typ. The SDK parses these without doing any cryptography — it only needs them to look up the right public key. If alg is not v4.public or typ is not paseto, the token is rejected immediately.

Verify the Ed25519 signature

The SDK looks up the public key for the token's kid in its cache. If the key is not in the cache (because keys have rotated), it re-fetches the key set from GET /v1/keys/paseto-v4-public and tries again.

The signature is verified against the message, footer, and an implicit assertion of square-experience:idp:access:v1. If the signature does not match, the token is rejected.

Check the claims

After the signature passes, the SDK validates the claims.

  • token_use must be access (refresh tokens and ID tokens have different values; the access-token verifier only accepts access tokens).
  • iss, sub, aud, and jti must all be non-empty.
  • gid (the global user id) and sid (the session id) must be non-empty.
  • exp must be in the future. There is a small clock-skew tolerance.
  • nbf (not before) must not be more than 30 seconds in the future.
  • The active context must be a valid kind and tenant id.

Check the optional blocklist

If the SDK is configured with a blocklist (used for logout and admin revocation), it checks the jti against it. If the token is revoked, it is rejected even if the signature is valid.

What a verified principal contains

When verification succeeds, you get a typed principal. The exact field names vary by SDK (subject vs sub, globalId vs gid) but the values are the same.

FieldDescription
sub / subjectThe subject claim — the user id within the issuer.
gid / globalIdThe global user id — stable across products.
emailThe user's email, if granted.
name / displayNameThe user's display name, if granted.
scopesThe scopes granted on this token.
roleThe user's role within the active context.
sid / sessionIdThe session id, useful for logout.
aalAuthentication assurance level (1 = password, 2 = MFA).
amrAuth methods used to authenticate this session.
ent / entitlementRefsReferences to active entitlements.
aud / audienceThe audience the token was minted for.
iss / issuerThe issuer that minted the token.
exp / expiresAtThe expiration time.

In code:

TypeScript
const principal = await client.verifyAccessToken(token);
console.log(principal.subject, principal.email, principal.scopes);
Go
p, _ := client.VerifyAccessToken(ctx, token, baseidp.VerifyOptions{})
fmt.Println(p.Subject, p.Email, p.Scopes)
Rust
let p = client.verify_access_token(token, VerifyOptions::default()).await?;
println!("{} {} {:?}", p.subject, p.email, p.scopes);

Key discovery and rotation

Base IdP rotates its signing keys regularly. The SDK handles this for you, but it is useful to know how.

The keys endpoint

GET /v1/keys/paseto-v4-public HTTP/1.1
Host: authlayer.squareexp.com
{
  "keys": [
    {
      "kid": "k_01HRZ...",
      "alg": "v4.public",
      "crv": "Ed25519",
      "x": "base64url-encoded-public-key"
    },
    {
      "kid": "k_01HRY...",
      "alg": "v4.public",
      "crv": "Ed25519",
      "x": "base64url-encoded-public-key"
    }
  ]
}

The set always contains the current key and the previous key (so tokens minted just before rotation still verify). When a token references a kid the SDK has never seen, the SDK re-fetches the set before failing.

What you should never do

Never hardcode a public key into your application. Keys rotate. A hardcoded key works until the day it does not, and then every request fails until someone redeploys.

don't do this
const PUBLIC_KEY: &[u8] = &[/* hardcoded bytes */]; // wrong

The SDK has a key cache with a TTL. Trust it.

The /v1/me shortcut

If you do not want to verify locally, you can ask Base IdP directly.

GET /v1/me HTTP/1.1
Host: authlayer.squareexp.com
Authorization: Bearer v4.public.eyJ...
{
  "sub": "usr_01HRZ...",
  "gid": "gid_01HRZ...",
  "email": "emai@domain.com",
  "name": "First Name",
  "scopes": ["openid", "profile"],
  "role": "operator"
}

The token is validated server-side — signature, audience, expiry — and the principal is returned. This is the easiest possible integration. It is also the slowest, because it adds a round trip to Base IdP on every authenticated request that uses it.

Use /v1/me for:

  • Serverless functions where caching public keys is awkward.
  • One-off scripts and CLI tools.
  • The initial login-exchange step (where a single extra round trip is fine).

Use offline verification for:

  • High-traffic APIs.
  • Anything where Base IdP being slow should not slow your service.

Token lifetimes

Defaults are sensible, not negotiable.

TokenLifetimeRotates?
Access1 hourNo, expires
Refresh30 daysYes, on every use
IdP session30 daysYes, on every refresh

If you find yourself wanting to bump the access token's lifetime, you almost certainly want a refresh-token-based flow instead. The SDK already handles refresh; you do not need to extend the access lifetime to avoid logins.

Where to go next

On this page