Verifying a Token

How to verify a LiSTNR JWT in your service.


Overview

LiSTNR tokens are signed RS256 JWTs. Verification is stateless — fetch the public keys once, cache them, and verify locally. There is no need to call the token provider on every request.

Client  ──(LiSTNR JWT)──▶  Your service
                                │
                      (1) fetch JWKS once, cache it
                                │
                      (2) verify signature + claims locally
                                │
                      (3) trust sub and any permitted claims

JWKS endpoint

The token provider publishes its public keys at:

EnvironmentJWKS URL
Devhttps://australia-southeast1-pcone-xl-fb-dev.cloudfunctions.net/listnr-token-provider/v1/listnr-token-provider/jwks
Productionhttps://australia-southeast1-pcone-xl-fb-prod.cloudfunctions.net/listnr-token-provider/v1/listnr-token-provider/jwks

No authentication is required. The response includes Cache-Control: public, max-age=300 — respect this TTL.


Token structure

A LiSTNR JWT is a standard three-part RS256 JSON Web Token: header.payload.signature.

Header

{
  "alg": "RS256",
  "kid": "<key-id>",
  "typ": "JWT"
}
FieldNotes
algAlways RS256. Reject any token with a different algorithm, including none or any symmetric algorithm.
kidKey ID — use this to select the matching public key from the JWKS. Changes on key rotation.

Payload

{
  "iss": "<issuer-url>",
  "sub": "<subject-identifier>",
  "aud": "<your-audience>",
  "iat": 1748649600,
  "exp": 1748650500,
  "jti": "<uuid>",
  "scope": "<granted-scopes>"
}

Standard claims — always present

ClaimTypeDescription
issstringIssuer. Verify this matches the expected value for your environment (see the environments table in Getting Started).
substringSubject — the stable identifier for the user or entity. Use this as the canonical key in your system.
audstringAudience — the value assigned to your integration during onboarding. Verify this matches.
iatnumberIssued-at time (Unix seconds).
expnumberExpiry time (Unix seconds). Tokens are valid for 15 minutes.
jtistringUnique token ID (UUID v4). Can be used for replay-attack detection if you maintain a short-lived denylist.

Optional claims — present when the client was granted relevant scopes

ClaimTypeDescription
scopestringSpace-separated list of granted scopes.
emailstringThe user's verified email address.
display_namestringThe user's full display name.
first_namestringThe user's first name.

Which optional claims your integration receives depends on the scopes and claim allowlist configured during onboarding.


Verification steps

Perform all of the following checks. Return 401 Unauthorized if any check fails.

#CheckExpectedFailure action
1Algorithmalg header is RS256Reject — algorithm confusion attack guard
2SignatureValid RS256 signature using the public key whose kid matches the token headerReject — token tampered or forged
3Expiryexp is in the futureReject — token expired
4Issueriss matches the expected value for your environmentReject — token from wrong issuer
5Audienceaud matches the value assigned to your integrationReject — token not intended for your service
6Subjectsub is present and non-emptyReject — no identity
7Scope (if applicable)scope contains the required valueReturn 403 Forbidden — token not authorised for this operation

After passing all checks, use sub as the user's identifier and trust any claims present in the payload.


JWKS caching

Fetch the JWKS once and cache it according to the Cache-Control response header (5 minutes by default).

Cache invalidation on unknown kid: If a token arrives with a kid not in your cached JWKS, re-fetch the JWKS before rejecting the token. This handles key rotation without downtime.

During a key rotation there will be two keys in the keys array — the current active key and the previous one. Always select the key by kid, not by array position.


Code examples

Node.js — jose

import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('<jwks-url-for-your-environment>')
)

async function verifyToken(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: '<issuer-url-for-your-environment>',
    audience: '<your-audience>',
    algorithms: ['RS256'],
  })

  return payload
}

createRemoteJWKSet handles caching, kid-based key selection, and cache invalidation on unknown kid automatically.

Python — PyJWT

import jwt
from jwt import PyJWKClient

jwks_client = PyJWKClient('<jwks-url-for-your-environment>')

def verify_token(token: str) -> dict:
    signing_key = jwks_client.get_signing_key_from_jwt(token)
    payload = jwt.decode(
        token,
        signing_key,
        algorithms=['RS256'],
        audience='<your-audience>',
        issuer='<issuer-url-for-your-environment>',
    )
    return payload

Java — nimbus-jose-jwt

import com.nimbusds.jose.jwk.source.*;
import com.nimbusds.jose.proc.*;
import com.nimbusds.jwt.proc.*;
import java.net.URL;

JWKSource<SecurityContext> keySource = JWKSourceBuilder
    .create(new URL("<jwks-url-for-your-environment>"))
    .retrying(true)
    .build();

ConfigurableJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>();
processor.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keySource));

JWTClaimsSet claims = processor.process(token, null);

// Verify claims
assert claims.getIssuer().equals("<issuer-url-for-your-environment>");
assert claims.getAudience().contains("<your-audience>");

Common errors

SymptomLikely causeResolution
kid not found in JWKSKey rotated since last JWKS fetchRe-fetch JWKS and retry once
exp in the pastToken expired (15-minute lifetime)Request a new token
Signature invalidToken tampered, or wrong key usedVerify you are selecting the key matching kid
iss mismatchToken from wrong environmentEnsure client and verifier use the same environment
aud mismatchToken not issued for your integrationConfirm your aud value with the LiSTNR platform team

Security notes

  • Never accept alg: none or symmetric algorithms. Explicitly pin RS256 in your JWT library.
  • Always validate aud. A token issued for a different integration must be rejected even if the signature is valid.
  • Always fetch the public key from the JWKS endpoint. Do not trust a public key embedded in or alongside the token.
  • Clock skew: Allow up to 30 seconds of tolerance on exp if your clocks may be slightly out of sync.