JWT Access Token Authentication
Contents
- Overview
- How JWT access token authentication works
- Chain configuration
- Required header
- JWT format
- Sample JWT
- Signature verification and JWKS
- Claim validation
- Scope enforcement
- User resolution
- Stateless sessions
- Error handling
Overview
JWT access token authentication allows end-users (shoppers/customers) to be authenticated against the Spaaza API using a signed JSON Web Token (JWT) issued by a trusted external identity provider. This is an alternative to the standard session-based user authentication and is designed for integrations where the client already holds a JWT access token and does not need to call the Spaaza login endpoint.
This page is intended for developers integrating an external identity provider with the Spaaza API who need to authenticate end-users using JWT access tokens.
For a general overview of the different authentication methods supported by the Spaaza API (including standard user, admin, and privileged authentication), see the Authentication section of the API documentation.
How JWT access token authentication works
The authentication flow is as follows:
- The client obtains a JWT access token from a trusted identity provider (e.g. an OAuth 2.0 authorization server or custom identity service).
- The client sends the JWT in the
X-Spaaza-Access-Token-JWTheader along with theX-Spaaza-MyPrice-App-Hostnameheader to identify the Spaaza app context. - The Spaaza API validates the JWT:
- Verifies the signature using the public keys published at the chain's configured JWKS (JSON Web Key Set) URL.
- Checks the token's header fields (
alg,kid,typ). - Validates the claims (
iss,exp,customer_guid, and optionallyaud).
- If validation succeeds, the API resolves the
customer_guidclaim to the corresponding Spaaza user and processes the request in the context of that user.
No server-side session is created or stored. Each request is independently verified.
Chain configuration
Before JWT access token authentication can be used, the chain must be configured with the identity provider's trust settings. These fields are returned by the get-chain endpoint:
| Field | Description |
|---|---|
jwt_jwks_url | (string, required) The HTTPS URL of the identity provider's JWKS (JSON Web Key Set) endpoint. This endpoint publishes the public keys used to verify JWT signatures. Must use the https scheme. |
jwt_issuer | (string, required) The expected value of the iss (issuer) claim in the JWT. This must match the identity provider's issuer identifier exactly. |
jwt_audience | (string, optional) The expected value of the aud (audience) claim in the JWT. When configured, the API verifies that the token's audience includes this value. When not configured, audience validation is skipped. |
jwt_jwks_url and jwt_issuer must both be set or both be cleared. jwt_audience is independent and can be omitted for identity providers that issue multi-service tokens without a dedicated audience for the Spaaza API.
Required header
To authenticate an end-user with a JWT access token, the following HTTP headers must be sent with each API request:
| Header | Description |
|---|---|
X-Spaaza-Access-Token-JWT | (string, required) The signed JWT access token issued by the trusted identity provider. |
X-Spaaza-MyPrice-App-Hostname | (string, required) The hostname of the Spaaza app that the request is associated with. This determines the chain context. |
When the X-Spaaza-Access-Token-JWT header is present, the API uses JWT access token authentication. The standard session-based headers (X-Spaaza-Session-User-Id, X-Spaaza-Session-Key) are not required and are ignored when a JWT access token is provided.
JWT format
The JWT must conform to the RFC 9068 profile for OAuth 2.0 access tokens. It consists of three Base64url-encoded segments separated by dots: <header>.<payload>.<signature>.
Header
The JWT header must contain the following fields:
| Field | Description | Required |
|---|---|---|
alg | The signing algorithm. Must be RS256. | Yes |
kid | The key ID identifying which public key in the JWKS was used to sign the token. | Yes |
typ | The token type. Must be at+jwt (access token JWT) when present. | No |
Example header (after Base64url decoding):
{
"alg": "RS256",
"kid": "key-2026-04",
"typ": "at+jwt"
}
Payload
The JWT payload must contain the following claims:
| Claim | Description | Required |
|---|---|---|
iss | The issuer identifier. Must exactly match the jwt_issuer value configured on the chain. | Yes |
exp | The expiration time of the token as a Unix timestamp (seconds since epoch). Requests with expired tokens are rejected. A clock skew tolerance of up to 60 seconds is applied. | Yes |
customer_guid | A string that uniquely identifies the end-user in the identity provider's system. This value is mapped to the Spaaza user via their authentication point identifier for the relevant app. | Yes |
aud | The intended audience for the token. Can be a single string or an array of strings. Validated only when jwt_audience is configured on the chain. | When jwt_audience is configured |
scope | An array of scope strings or a space-delimited scope string indicating the permissions granted to the token. Used for endpoint-level scope enforcement. | No |
nbf | The "not before" time as a Unix timestamp. If present, the token is rejected before this time (with clock skew tolerance). | No |
iat | The "issued at" time as a Unix timestamp. | No |
Sample JWT
The following is an example of a decoded JWT access token suitable for use with the Spaaza API.
Header:
{
"alg": "RS256",
"kid": "key-2026-04",
"typ": "at+jwt"
}
Payload:
{
"iss": "https://identity.example.com",
"aud": "example-rewards-api",
"exp": 1776865960,
"iat": 1776862360,
"customer_guid": "cust-00412",
"scope": ["customer_data", "customer_profile.read"]
}
In this example:
- The token was issued by
https://identity.example.comand is intended forexample-rewards-api. - The end-user is identified by
customer_guidvaluecust-00412, which corresponds to their authentication point identifier in Spaaza. - The token grants
customer_dataandcustomer_profile.readscopes.
Signature verification and JWKS
The Spaaza API verifies the JWT signature using the public key identified by the kid header field. The public key is retrieved from the JWKS (JSON Web Key Set) document published at the chain's configured jwt_jwks_url.
The JWKS document is cached to avoid fetching it on every request. The cache TTL respects the Cache-Control: max-age directive from the JWKS endpoint response when available, and defaults to 3600 seconds (one hour) otherwise.
Key rotation
When the identity provider rotates signing keys, the new key's kid will not initially be present in the cached JWKS. The API handles this automatically:
- If the JWT's
kidis not found in the cached JWKS, the API fetches a fresh copy of the JWKS from the configured URL. - If the refreshed JWKS contains the
kid, signature verification proceeds normally. - If the
kidis still not found after a refresh, the request is rejected.
This approach ensures seamless key rotation without requiring manual cache invalidation. During a key rollover period, the identity provider should publish both the old and the new keys in the JWKS document.
Claim validation
After signature verification, the following claim checks are performed:
- Issuer (
iss): must exactly match the chain'sjwt_issuerconfiguration. - Audience (
aud): ifjwt_audienceis configured on the chain, the token'saudclaim (whether a single string or an array) must include the configured value. Ifjwt_audienceis not configured, audience validation is skipped. - Expiration (
exp): the token must not be expired. A clock skew tolerance of up to 60 seconds is applied. - Customer GUID (
customer_guid): must be a non-empty string that maps to a known Spaaza user via their authentication point identifier for the relevant app.
If any of these checks fail, the request is rejected with an authentication error.
Scope enforcement
JWT access tokens may include a scope claim that indicates the permissions granted to the token. Spaaza uses these scopes for endpoint-level authorization.
When a JWT-authenticated request reaches an endpoint, the API checks that the token includes at least one of the required scopes. If the endpoint does not specify explicit scope requirements, the following baseline scopes are checked:
customer_datacustomer_profile.readcustomer_profile.write
Some endpoints may enforce specific scopes. For example, endpoints that modify the user profile require the customer_profile.write scope.
If the token does not include any of the required scopes, the request is rejected with an access denied error.
User resolution
The customer_guid claim in the JWT is used to identify the end-user. This value is matched against the authentication point identifier associated with the user's account for the relevant Spaaza app (as determined by the X-Spaaza-MyPrice-App-Hostname header).
If no Spaaza user is found for the given customer_guid and app combination, the request is rejected.
Stateless sessions
JWT access token authentication creates an ephemeral, request-scoped session. Unlike standard user authentication:
- No session is stored server-side.
- Each request is independently validated against the JWT.
- The session cannot be revoked via the logout endpoint. Token expiry is the primary mechanism for session termination.
- The client is responsible for obtaining fresh tokens from the identity provider as needed.
Error handling
When JWT access token authentication fails, the API returns a generic error message. Detailed failure reasons (such as specific claim mismatches or signature errors) are logged server-side but are not exposed in the API response. This prevents leaking information about the chain's trust configuration to callers.
Common reasons for authentication failure include:
- The JWT signature cannot be verified against the JWKS.
- The
kidin the JWT header does not match any key in the JWKS. - The
issclaim does not match the chain's configured issuer. - The
audclaim does not match the chain's configured audience (when audience validation is enabled). - The token has expired.
- The
customer_guiddoes not correspond to a known Spaaza user for the app. - The token is missing required scopes for the requested endpoint.