Aller au contenu principal
Version 0.5Brouillon

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

Validate every ID Token before trusting any claim

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.

  1. Signature: Fetch the public key from the JWKS endpoint at https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID. Select the key by kid. Verify the RS256 signature. Reject immediately if the signature is invalid - do not inspect the payload of a token with a bad signature.

  2. iss (Issuer): Must be exactly https://login.luxid.lu. A mismatch means the token was not issued by LuxID.

  3. aud (Audience): Must contain your application's client_id. If aud is an array, your client_id must be present. If aud contains values other than your client_id, verify that azp equals your client_id. Reject tokens issued to other clients - this prevents token substitution attacks.

  4. exp (Expiry): The current time must be before exp. Allow a small clock skew (about a minute or less) to accommodate minor time-sync differences between your server and LuxID's.

  5. nonce: If you included a nonce in the authorisation request (which you should always do for flows involving a browser redirect), the token must carry the same nonce value. 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 an iat far in the past may indicate a replay attempt. A practical threshold is: reject if iat is 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 specific auth_level via acr_values, verify that the received acr meets 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:

  1. Validate the signature, iss, aud, and exp using the same JWKS endpoint as for ID Tokens.
  2. Check that the scope claim contains the scopes required for the requested operation.
  3. 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 TokenAccess TokenRefresh Token
PurposeIdentity assertionAPI authorisationSilent renewal
FormatJWT (RS256)JWT (RS256)Opaque string
Lifetime~1 hourShort-livedLong-lived
Who validatesYour app serverResource Server or UserInfo endpointLuxID token endpoint
Scope requiredopenidopenid + othersoffline_access
StorageServer-side sessionServer-side session / memoryServer-side store (encrypted)
On expiryRefresh or re-authRefreshRe-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:

  1. The corresponding scope was requested (e.g. profile, email, phone).
  2. The user has consented to the Claim Template declaring that attribute.
  3. LuxID holds the value for that user (e.g. phone_number is absent if the user has not registered a phone number).

Claims quick index

GroupClaims
Required (always present)sub, iss, aud, exp, iat
Conditional OIDCauth_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 extensionsluxid_verified, luxid_verification_level, luxid_risk_level, luxid_federation_realm, luxid_organization
Partner-specificluxid_<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:

ValueMethod
pwdPassword
otpOne-Time Code via SMS or voice call
totpTime-based One-Time Code (authenticator app)
fidoHardware-bound key (passkey, device-bound WebAuthn)
luxtrustLuxTrust 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

Roadmap - planned, not currently offered

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:

  1. Declared in your Application's Claim Template by LuxID (so the claim is allowed to be released at all).
  2. 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

LuxID divergence - read this carefully

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):

ScopeWhat it means in LuxID
openidRequired. 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.
profileNo effect on claim release. Recommended for spec compatibility.
emailNo effect on claim release. Recommended for spec compatibility.
phoneNo effect on claim release. Recommended for spec compatibility.
offline_accessHas 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:

  1. 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.
  2. 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

ClaimCommon misuseCorrect approach
subUsing it as a cross-Sphere identifiersub is Sphere-scoped; do not share it across Sphere boundaries
subTreating it as permanentStore it as a foreign key; handle new sub on Subscription revocation + re-grant
emailUsing it as the primary key instead of subUsers can change their email; sub is the stable programmatic identifier
birthdateRequesting it when only age verification is neededUse age_over_16 (planned, not currently offered) to minimise data exposure
acrNot checking it after receiving the tokenAlways verify the received acr meets your minimum requirement
Access TokenPresenting it to your own backend as proof of identityUse the ID Token for identity; the Access Token for API authorisation

Summary

TokenPurposeLifetimeFormatKey validation
ID TokenIdentity assertion~1 hourJWT (RS256)Signature, iss, aud, exp, nonce, acr
Access TokenAPI authorisationShort-livedJWT (RS256)Signature, iss, aud, exp, scope
Refresh TokenSilent renewalLong-livedOpaqueinvalid_grant on use failure

Mise à jour le 2026-07-02