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:
| Environment | JWKS URL |
|---|---|
| Dev | https://australia-southeast1-pcone-xl-fb-dev.cloudfunctions.net/listnr-token-provider/v1/listnr-token-provider/jwks |
| Production | https://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"
}
| Field | Notes |
|---|---|
alg | Always RS256. Reject any token with a different algorithm, including none or any symmetric algorithm. |
kid | Key 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
| Claim | Type | Description |
|---|---|---|
iss | string | Issuer. Verify this matches the expected value for your environment (see the environments table in Getting Started). |
sub | string | Subject — the stable identifier for the user or entity. Use this as the canonical key in your system. |
aud | string | Audience — the value assigned to your integration during onboarding. Verify this matches. |
iat | number | Issued-at time (Unix seconds). |
exp | number | Expiry time (Unix seconds). Tokens are valid for 15 minutes. |
jti | string | Unique 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
| Claim | Type | Description |
|---|---|---|
scope | string | Space-separated list of granted scopes. |
email | string | The user's verified email address. |
display_name | string | The user's full display name. |
first_name | string | The 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.
| # | Check | Expected | Failure action |
|---|---|---|---|
| 1 | Algorithm | alg header is RS256 | Reject — algorithm confusion attack guard |
| 2 | Signature | Valid RS256 signature using the public key whose kid matches the token header | Reject — token tampered or forged |
| 3 | Expiry | exp is in the future | Reject — token expired |
| 4 | Issuer | iss matches the expected value for your environment | Reject — token from wrong issuer |
| 5 | Audience | aud matches the value assigned to your integration | Reject — token not intended for your service |
| 6 | Subject | sub is present and non-empty | Reject — no identity |
| 7 | Scope (if applicable) | scope contains the required value | Return 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
| Symptom | Likely cause | Resolution |
|---|---|---|
kid not found in JWKS | Key rotated since last JWKS fetch | Re-fetch JWKS and retry once |
exp in the past | Token expired (15-minute lifetime) | Request a new token |
| Signature invalid | Token tampered, or wrong key used | Verify you are selecting the key matching kid |
iss mismatch | Token from wrong environment | Ensure client and verifier use the same environment |
aud mismatch | Token not issued for your integration | Confirm your aud value with the LiSTNR platform team |
Security notes
- Never accept
alg: noneor symmetric algorithms. Explicitly pinRS256in 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
expif your clocks may be slightly out of sync.