Tokens and claims
Overview
Every LuxID authentication flow produces up to three tokens. Each token has a different purpose, a different lifetime, and different handling requirements. Understanding the distinctions is essential for building a secure integration - using the wrong token for the wrong purpose, or skipping validation steps, opens security vulnerabilities that attackers actively exploit.
This page covers:
- the structure and purpose of each token type,
- how to validate them correctly,
- where to store them securely,
- the complete catalogue of claims LuxID may include in an ID Token,
- LuxID-specific extension claims.
For background on JWT structure and the JWKS endpoint, see Identity fundamentals.
The ID Token
Purpose
The ID Token is an identity assertion. It answers the question: "Did this specific person really authenticate with LuxID just now, and if so, what do I know about them?"
The ID Token is defined in OpenID Connect Core 1.0 (opens in a new tab) and encoded as a JSON Web Token (RFC 7519 (opens in a new tab)), signed with RS256 (RFC 7518 (opens in a new tab)) using a key published at the JWKS endpoint.
The ID Token is intended for your application server - it is the credential you validate to establish who the user is. It is not a credential to present to other services; that is the Access Token's role.
Lifetime
ID Tokens issued by LuxID expire approximately 1 hour after issuance (exp - iat ≈ 3600 seconds). An ID Token is a snapshot of an authentication event - once that event is sufficiently old, re-authenticate rather than extending the token.
Format
A LuxID ID Token is a standard three-part JWT:
<base64url(header)>.<base64url(payload)>.<base64url(signature)>
Decoded header:
{
"alg": "RS256",
"typ": "JWT",
"kid": "OIDC-LUXID-signing-key-1"
}
Decoded payload (full example):
{
"iss": "https://login.luxid.lu",
"sub": "a3f8b2c1-7e4d-4a1b-9c0f-5d2e8b3a6f1c",
"aud": "your-client-id",
"exp": 1748048400,
"iat": 1748044800,
"auth_time": 1748044750,
"nonce": "n-0S6_WzA2Mj",
"acr": "urn:luxid:acr:level:substantial",
"amr": ["pwd", "otp"],
"azp": "your-client-id",
"given_name": "Marie",
"family_name": "Dupont",
"name": "Marie Dupont",
"email": "marie.dupont@example.lu",
"email_verified": true,
"phone_number": "+352621123456",
"phone_number_verified": true,
"birthdate": "1990-06-15",
"updated_at": 1745000000
}
The claims present in the payload depend on which scopes and Claim Templates the user has consented to. The required OIDC claims (iss, sub, aud, exp, iat) are always present.
Validation rules
Run all of the following checks before trusting any claim in an ID Token. These checks are specified in OIDC Core 1.0, §3.1.3.7.
-
Signature: Fetch the public key from the JWKS endpoint at
https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID. Select the key bykid. Verify the RS256 signature. Reject immediately if the signature is invalid - do not inspect the payload of a token with a bad signature. -
iss(Issuer): Must be exactlyhttps://login.luxid.lu. A mismatch means the token was not issued by LuxID. -
aud(Audience): Must contain your application'sclient_id. Ifaudis an array, yourclient_idmust be present. Ifaudcontains values other than yourclient_id, verify thatazpequals yourclient_id. Reject tokens issued to other clients - this prevents token substitution attacks. -
exp(Expiry): The current time must be beforeexp. Allow a small clock skew (about a minute or less) to accommodate minor time-sync differences between your server and LuxID's. -
nonce: If you included anoncein the authorisation request (which you should always do for flows involving a browser redirect), the token must carry the samenoncevalue. Compare it against the value you stored in the user's session cookie before the redirect. This prevents replay attacks.
Additionally:
iat(Issued At): Verify this is a recent timestamp. Tokens with aniatfar in the past may indicate a replay attempt. A practical threshold is: reject ifiatis more than 5 minutes in the past relative to the time you first receive the token (not relative to the expiry).acr: If your application required a specificauth_levelviaacr_values, verify that the receivedacrmeets or exceeds your requirement. See Authentication levels for the level ordering.
Compliant OIDC libraries (e.g. openid-client for Node.js, Authlib for Python, Spring Security OAuth2 for Java) perform checks 1-5 automatically. Always configure your library with the issuer URL and your client_id; do not bypass or disable these checks.
JWKS caching
Do not fetch the JWKS on every token validation. Cache the JWKS response according to the Cache-Control headers. If you receive an ID Token with a kid you do not recognise (key rotation has occurred), fetch the JWKS fresh once. If the new JWKS still does not contain the kid, reject the token.
Where to store the ID Token
The ID Token is a server-side credential. After validation, extract the claims you need into your application's session state (e.g. a secure server-side session or a signed application-level JWT). Do not store the raw ID Token in a cookie accessible to JavaScript (HttpOnly: false), in localStorage, or in sessionStorage.
See Session management for complete storage guidance.
What to do on expiry
ID Tokens expire after ~1 hour. You have two options:
- Refresh silently: if you hold a valid Refresh Token, use it to obtain new tokens without re-prompting the user (see The Refresh Token).
- Re-authenticate: initiate a new authorisation flow. If the user's LuxID session is still active, LuxID will issue new tokens without asking the user to re-enter credentials.
The Access Token
Purpose
The Access Token is an authorisation credential. It answers the question: "Is the bearer of this token permitted to call this API endpoint right now?"
In a standard LuxID OIDC flow, the Access Token is presented to the UserInfo endpoint (https://login.luxid.lu/mga/sps/oauth/oauth20/userinfo) to retrieve claims that were not included in the ID Token. If your application calls other LuxID APIs or Resource Servers that require bearer token authorisation, the Access Token is the credential to present.
Do not present the Access Token to your own backend as proof of identity - that is the ID Token's role. Your backend should accept an Access Token only if it is itself a Resource Server registered with LuxID, and it must validate the token accordingly.
Lifetime
Access Tokens issued by LuxID are short-lived. This short lifetime limits the window of exposure if a token is intercepted. If your application needs to invalidate access immediately rather than waiting for the token to expire, discuss the available options with LuxID.
Format
LuxID issues Access Tokens as JWTs signed with RS256. Partner APIs acting as Resource Servers may validate them locally against the JWKS endpoint (see the validation steps below), or use the introspection endpoint when authoritative revocation status is needed. If you only use the Access Token to call the UserInfo endpoint, you do not need to parse or validate it yourself - LuxID validates it server-side.
Validation (resource server scenario)
If your backend acts as a Resource Server that accepts LuxID Access Tokens from client applications:
- Validate the signature,
iss,aud, andexpusing the same JWKS endpoint as for ID Tokens. - Check that the
scopeclaim contains the scopes required for the requested operation. - Reject tokens that are expired, have an invalid signature, or carry insufficient scope.
Use the Token Introspection endpoint (RFC 7662 (opens in a new tab)) as an alternative to local validation - introspection is authoritative (LuxID checks its own revocation state) at the cost of one extra network call per request.
Where to store the Access Token
Store the Access Token in your application's server-side session or in memory (not persisted to disk). Never expose it to the browser. If you are building a single-page application (SPA) that needs to call an API, use a backend-for-frontend (BFF) proxy: the SPA authenticates via the BFF, and the BFF holds the Access Token server-side, proxying API calls with the token attached.
What to do on expiry
Exchange the Refresh Token for a new Access Token. See The Refresh Token.
The Refresh Token
Purpose
The Refresh Token is a long-lived credential that allows your application to obtain new Access Tokens and ID Tokens without requiring the user to re-authenticate. It enables silent renewal of sessions within the token lifetime window.
Lifetime
LuxID Refresh Tokens are long-lived. The exact lifetime is not returned in the token response, so do not hardcode it - drive renewal from the exp of the tokens you receive and treat a failed refresh as the end of the session. If you need the precise value, ask LuxID. After the refresh token expires, the user must re-authenticate.
Format
Refresh Tokens are opaque strings - they carry no structured information readable by your application. Do not attempt to parse or decode them.
Obtaining a Refresh Token
Refresh Tokens are issued only when you include offline_access in the scope parameter of the authorisation request:
scope=openid%20profile%20email%20offline_access
Without offline_access, LuxID will not issue a Refresh Token. offline_access is the one scope value that does have a functional effect in LuxID (unlike profile/email/phone, which do not gate claim release - see Requesting claims). You must include it in the scope parameter of the authorisation request for a Refresh Token to be issued; your Application must also be permitted to use it. .
Using the Refresh Token
Send a POST request to the token endpoint:
POST https://login.luxid.lu/mga/sps/oauth/oauth20/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
A successful response returns a new Access Token, a new ID Token, and optionally a new Refresh Token. Some LuxID Partner configurations use rotating refresh tokens: each use of a Refresh Token invalidates the old one and returns a new one. If your configuration uses rotation, store the new Refresh Token immediately and discard the old one.
Refresh Token validity conditions
A Refresh Token becomes invalid before its expiry if:
- The user revokes consent for your Application from https://account.luxid.lu/ (opens in a new tab).
- An administrator revokes the user's session with LuxID.
- The Refresh Token has been used (if token rotation is enabled and the rotated token is replayed).
A password change does not currently invalidate existing Refresh Tokens: session and token invalidation on password change is planned but not yet implemented. Do not rely on a password reset to terminate a user's active sessions or refresh tokens. This page will be updated when that behaviour ships.
Your application must handle invalid_grant errors on refresh attempts gracefully: detect the error, clear the stored tokens, and redirect the user to re-authenticate.
Where to store the Refresh Token
The Refresh Token is the highest-value credential in the token set - it grants long-term access. Store it exclusively in your server-side session store or a secure encrypted database. Never expose it to a browser, never store it in localStorage or sessionStorage, and never log it.
Token summary
| ID Token | Access Token | Refresh Token | |
|---|---|---|---|
| Purpose | Identity assertion | API authorisation | Silent renewal |
| Format | JWT (RS256) | JWT (RS256) | Opaque string |
| Lifetime | ~1 hour | Short-lived | Long-lived |
| Who validates | Your app server | Resource Server or UserInfo endpoint | LuxID token endpoint |
| Scope required | openid | openid + others | offline_access |
| Storage | Server-side session | Server-side session / memory | Server-side store (encrypted) |
| On expiry | Refresh or re-auth | Refresh | Re-authenticate |
The claim catalogue
Claims are key-value pairs in the ID Token payload (and returnable from the UserInfo endpoint). LuxID supports the standard OIDC claims defined in OIDC Core 1.0, §5.1 (opens in a new tab) plus a set of LuxID extension claims prefixed with luxid_.
Claims are only present in a token if:
- The corresponding scope was requested (e.g.
profile,email,phone). - The user has consented to the Claim Template declaring that attribute.
- LuxID holds the value for that user (e.g.
phone_numberis absent if the user has not registered a phone number).
Claims quick index
| Group | Claims |
|---|---|
| Required (always present) | sub, iss, aud, exp, iat |
| Conditional OIDC | auth_time, nonce, acr, amr, azp |
Profile (scope profile) | given_name, family_name, name, birthdate, gender, updated_at |
Email (scope email) | email, email_verified |
Phone (scope phone) | phone_number, phone_number_verified, address |
| Age (roadmap) | age_over_16 - planned, not currently offered |
| LuxID extensions | luxid_verified, luxid_verification_level, luxid_risk_level, luxid_federation_realm, luxid_organization |
| Partner-specific | luxid_<partner-prefix>_<name> |
Required OIDC claims (always present)
sub
Type: string
The subject identifier - LuxID's pseudonymous stable identifier for the user within a Sphere. The sub value is unique per user per Sphere; the same user will have a different sub in a different Sphere. It is not a national identity number and carries no personally identifiable meaning on its own.
Never use sub from one application to look up a user in another application that belongs to a different Sphere - the values will not match and any correlation attempt is intentional design, not a bug.
The sub is stable as long as the user maintains at least one active Subscription in the Sphere. If the user revokes all Subscriptions and later re-subscribes, a new sub is allocated. See Roles in the Ecosystem - The Subscription for implications.
iss
Type: string (URI)
The issuer identifier. Always https://login.luxid.lu. Validate this matches exactly.
aud
Type: string or array of strings
The audience. Contains your application's client_id. If multiple values are present, azp identifies the authorised party.
exp
Type: integer (Unix timestamp)
Token expiry time. Reject tokens where the current time is past exp (allowing a small clock skew, about a minute or less).
iat
Type: integer (Unix timestamp)
Token issuance time. Use to detect stale tokens.
Conditional OIDC claims
auth_time
Type: integer (Unix timestamp)
The time the end-user most recently authenticated with LuxID. Present when max_age was included in the authorisation request, or when the Application's configuration requires it. Use this to enforce step-up freshness windows.
nonce
Type: string
The nonce value from the authorisation request. Present if and only if a nonce was included in the request. Validate this matches the value stored in the user's session before the redirect.
acr
Type: string (URI)
The auth_level tier reached during authentication, expressed as one of three URNs: urn:luxid:acr:level:low, urn:luxid:acr:level:substantial, or urn:luxid:acr:level:high. The request-side acr_values parameter uses the same URN form (the accepted request values are agreed per Partner during onboarding). See Authentication levels for the full mapping to eIDAS LoA tiers and validation guidance.
amr
Type: array of strings
Authentication Methods References - the methods used during the authentication event. Array entries are method identifiers:
| Value | Method |
|---|---|
pwd | Password |
otp | One-Time Code via SMS or voice call |
totp | Time-based One-Time Code (authenticator app) |
fido | Hardware-bound key (passkey, device-bound WebAuthn) |
luxtrust | LuxTrust smartcard or mobile app |
These five values are the complete amr catalogue. Example: a user who authenticated with password + SMS OTP will have "amr": ["pwd", "otp"].
azp
Type: string
The authorised party - the client_id of the application that requested the token. Present when aud contains multiple values or when the client is different from the sole audience entry.
Profile claims (scope: profile)
given_name
Type: string
The user's given name (first name). Example: "Marie".
family_name
Type: string
The user's family name (surname). Example: "Dupont".
name
Type: string
The user's full display name, typically given_name + " " + family_name. Example: "Marie Dupont".
birthdate
Type: string (ISO 8601 date)
The user's date of birth in YYYY-MM-DD format. Example: "1990-06-15".
This claim requires identity verification (LuxID Verified) to be populated. Requesting it without the user having completed verification will result in the claim being absent. If precise age is not required, age_over_16 will be the preferred minimal claim once LuxID offers it (planned, not currently offered - see below).
gender
Type: string
The user's gender as set in their LuxID Account profile. Self-declared by the user, not verified. Apply claim minimisation: request it only if your application genuinely uses it.
updated_at
Type: integer (Unix timestamp)
The time the user's profile was last updated. Use to detect stale profile data in your own cache.
Email claims (scope: email)
email
Type: string
The user's email address. This is the user's LuxID identifier - the address they registered with and use to sign in. Example: "marie.dupont@example.lu".
email_verified
Type: boolean
Whether LuxID has verified that the user controls this email address. LuxID verifies email at registration via a confirmation link. This value will be true for all standard LuxID Accounts. A value of false or an absent claim should be treated as unverified.
Phone claims (scope: phone)
phone_number
Type: string (E.164 format)
The user's phone number. Example: "+352621123456". Present only if the user has registered a phone number with LuxID. Phone number is optional in LuxID - it is used for OTP-based 2FA and is not a required field.
phone_number_verified
Type: boolean
Whether LuxID has verified that the user controls this phone number. LuxID verifies phone numbers via SMS OTP confirmation when the number is enrolled.
address
Type: object (OpenID Connect Core address claim (opens in a new tab))
The user's postal address as a JSON object with street_address, locality, postal_code and country (and optionally region). Present only if your Application's Claim Template includes address and the user has provided and consented to share it. Note: address is not advertised in the discovery document's claims_supported, but LuxID can release it via a Claim Template.
Age claim (scope: profile or dedicated Claim Template)
age_over_16
The age_over_16 claim is roadmap only: it is planned but LuxID does not offer or populate it today (the same applies to age_over_18). The guidance below describes the intended behaviour for when it becomes available.
Type: boolean
A privacy-preserving derived claim that asserts whether the user is aged 16 or over, without revealing the user's actual date of birth. This is the recommended claim for age-gated content where you need age verification but not the precise birthdate.
true means the user is 16 or older. false means the user is under 16. This claim requires identity verification to be populated.
Once LuxID offers age_over_16, prefer it over birthdate when your application's legal requirement is only to confirm a minimum age threshold. Requesting birthdate when age_over_16 is sufficient is inconsistent with the data minimisation principle under GDPR (Regulation (EU) 2016/679, CELEX:32016R0679 (opens in a new tab), Art. 5(1)(c)).
LuxID extension claims
These claims are specific to LuxID and are not part of the OIDC Core standard. They are available as Claim Templates with LuxID. Extension claims are prefixed with luxid_.
luxid_verified
Type: boolean
Whether the user has completed LuxID Verified identity verification. true means the user's name and date of birth have been verified against official documentation. Used as a prerequisite check before relying on birthdate or luxid_verification_level.
luxid_verification_level
Type: string
The LoA value declared by the upstream identity proofing source (LuxTrust) and relayed by LuxID. The current value is "substantial" for all identities verified via LuxID Verified - LuxTrust performs the proofing at Substantial and LuxID relays that LoA. A value of "high" is not issued via LuxID Verified; auth_level 9 (LuxTrust as MFA method) is the sole path to the urn:luxid:acr:level:high tier.
luxid_risk_level
Type: string
A qualitative risk indicator produced by LuxID's authentication risk engine for the specific authentication event - for example "low", "medium", or "high". Use this in application logic to apply different policies at different risk levels.
Present only subject to Partner configuration. A companion luxid_risk_score claim may also be present where enabled; treat it as an opaque LuxID-internal value rather than a published scale. See 07-features/ for risk scoring documentation.
luxid_federation_realm
Type: string
The federation realm (the routed email domain) the user authenticated through via a LuxID Pro corporate IdP - for example "post.lu". Present only for federated users; absent for direct LuxID Account holders. Use it to distinguish federated users from direct LuxID Account holders and to drive realm-based access control.
luxid_organization
Type: string
The name or identifier of the organisation via which the user federated (LuxID Pro). Present only when luxid_federation_realm is present. Example: "ExampleCorp S.A.".
Partner-specific technical claims
Beyond the platform luxid_* claims above, a partnership can have its own bespoke claims provisioned by LuxID - for example a partner-side membership or registration number that your application wants returned on every sign-in. These follow the naming convention luxid_<partner-prefix>_<name> (for example luxid_examplecorp_membership_id), so the exact claim name is not the bare attribute name you might expect (membership_id); confirm the precise name with LuxID.
Partner-specific claims are technical claims: invisible to the user, requiring no consent, carrying partner-supplied data. Unlike the profile attributes the user controls, their value is set by you, per user, through the Partner API claims capability. They have a two-step lifecycle:
- Declared in your Application's Claim Template by LuxID (so the claim is allowed to be released at all).
- Set per subscriber by your back-end via
PUT .../subscribers/{subscriberExtId}/claims/{claimName}.
A claim that is declared but never set for a given user simply does not appear in that user's token - see Via Claim Templates for the troubleshooting checklist.
Requesting claims
In vanilla OIDC, a client can influence what claims it receives at runtime by setting the scope parameter (e.g. requesting phone to get phone_number). LuxID does not work this way. Each Application has a Claim Template pre-declared and pinned in LuxID's configuration, reviewed and approved by LuxID before the Application is activated. The scope parameter in your authorisation request is effectively ignored for claim release: LuxID releases the Claim Template's claims (subject to user consent), not the claims your scope parameter asks for. Claim entitlement is therefore a configuration-time decision between Partner and LuxID, not a runtime decision in the client.
This is a deliberate privacy guardrail. It stops a Partner from quietly broadening the data it collects by adding scopes to its requests, and it lets LuxID act as a check against over-collection before a single real user authenticates. The trade-off is that adding a new claim is a Claim Template change (a Partner request to LuxID, reviewed against the Ethical Code of Conduct and the agreed Permitted Use), not a code change in the client.
What to put in the scope parameter
You still need to send a valid scope parameter to satisfy the OIDC spec - LuxID will reject the authorisation request if openid is missing. In practice send openid profile email (plus offline_access if you want a Refresh Token):
| Scope | What it means in LuxID |
|---|---|
openid | Required. Signals an OIDC (not pure OAuth) request. LuxID always includes the OIDC structural claims (sub, iss, aud, exp, iat, auth_time, nonce, acr, amr, azp) when openid is present. |
profile | No effect on claim release. Recommended for spec compatibility. |
email | No effect on claim release. Recommended for spec compatibility. |
phone | No effect on claim release. Recommended for spec compatibility. |
offline_access | Has effect: enables Refresh Token issuance. |
What you actually receive in the ID Token and UserInfo response is the Claim Template assigned to your Application, not the union of these scopes.
Via the UserInfo endpoint
Claims may be returned in the ID Token directly or deferred to the UserInfo endpoint, depending on your Partner configuration. To retrieve deferred claims, send a GET request with the Access Token as a bearer credential:
GET https://login.luxid.lu/mga/sps/oauth/oauth20/userinfo
Authorization: Bearer ACCESS_TOKEN
The response is a JSON object containing the claims the user has consented to share. The sub claim in the UserInfo response must match the sub in the ID Token - verify this before using any UserInfo claims.
Via Claim Templates
LuxID extension claims (luxid_*) and some standard claims require a Claim Template declaration with LuxID before they will be released. Your Application must have the relevant Claim Template configured before those claims will be returned.
If a claim is missing from the token, work through this checklist before contacting us:
- Standard or platform claim (
email,given_name,luxid_verified, ...): confirm it is in your Claim Template and, where applicable, that the user consented. If not, request a Claim Template update. - Partner-specific technical claim (
luxid_<partner-prefix>_<name>): confirm both steps - (a) the claim is declared in your Claim Template, and (b) a value has actually been set for this user via the Partner API (PUT .../subscribers/{subscriberExtId}/claims/{claimName}). A declared-but-never-set technical claim is simply omitted for users who have no value.
Contact us if a required claim is still not returned after these checks.
Claims not to misuse
| Claim | Common misuse | Correct approach |
|---|---|---|
sub | Using it as a cross-Sphere identifier | sub is Sphere-scoped; do not share it across Sphere boundaries |
sub | Treating it as permanent | Store it as a foreign key; handle new sub on Subscription revocation + re-grant |
email | Using it as the primary key instead of sub | Users can change their email; sub is the stable programmatic identifier |
birthdate | Requesting it when only age verification is needed | Use age_over_16 (planned, not currently offered) to minimise data exposure |
acr | Not checking it after receiving the token | Always verify the received acr meets your minimum requirement |
| Access Token | Presenting it to your own backend as proof of identity | Use the ID Token for identity; the Access Token for API authorisation |
Summary
| Token | Purpose | Lifetime | Format | Key validation |
|---|---|---|---|---|
| ID Token | Identity assertion | ~1 hour | JWT (RS256) | Signature, iss, aud, exp, nonce, acr |
| Access Token | API authorisation | Short-lived | JWT (RS256) | Signature, iss, aud, exp, scope |
| Refresh Token | Silent renewal | Long-lived | Opaque | invalid_grant on use failure |
Related pages
- Identity fundamentals - JWT structure, JWKS endpoint, sessions vs tokens.
- Authentication levels -
auth_levelscale,acrandamrclaim semantics, step-up authentication. - Privacy and consent - which claims you may request and your obligations as a Partner.
- Session management - where and how to store tokens securely.
- Protect your Application - token validation in the context of your overall security model.