OpenID Connect
Overview
LuxID implements OpenID Connect Core 1.0 on top of OAuth 2.0 (RFC 6749). This page is the complete protocol reference. If you are starting from scratch, begin with Add Login for a step-by-step walkthrough, then return here for parameter detail.
Supported grant types: authorization_code, refresh_token.
Supported response types: code, none. Implicit flow (response_type=id_token) is not supported - it is deprecated in RFC 9700 (opens in a new tab) and not advertised in LuxID's discovery document. Use Authorization Code + PKCE.
ID token signing: RS256 (asymmetric; public keys at JWKS endpoint).
PKCE: Required for all clients (public and confidential). Only S256 is accepted.
Discovery
LuxID publishes a standard OpenID Connect Discovery document at:
https://login.luxid.lu/.well-known/openid-configuration
This document contains all endpoint URLs, supported scopes, claims, response types, and signing algorithms. Libraries that accept an issuer or discovery_url will fetch this automatically.
curl -s https://login.luxid.lu/.well-known/openid-configuration | python3 -m json.tool
Key fields in the discovery document:
| Field | Value |
|---|---|
issuer | https://login.luxid.lu |
authorization_endpoint | https://login.luxid.lu/mga/sps/oauth/oauth20/authorize |
token_endpoint | https://login.luxid.lu/mga/sps/oauth/oauth20/token |
userinfo_endpoint | https://login.luxid.lu/mga/sps/oauth/oauth20/userinfo |
jwks_uri | https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID |
introspection_endpoint | https://login.luxid.lu/mga/sps/oauth/oauth20/introspect |
revocation_endpoint | https://login.luxid.lu/mga/sps/oauth/oauth20/revoke |
Always derive endpoint URLs from the discovery document rather than hardcoding them. This ensures your integration adapts if URLs change.
UAT environment
Replace login.luxid.lu with login-uat.luxid.lu throughout. The UAT discovery document is at:
https://login-uat.luxid.lu/.well-known/openid-configuration
Authorisation request parameters
Send the user's browser to the authorisation endpoint with a GET redirect. All parameters are query string values.
Required parameters
| Parameter | Description |
|---|---|
response_type | Must be code for the Authorization Code flow. Only code and none are supported. Implicit flow (id_token) is not supported |
client_id | Your application's client identifier, issued by POST Luxembourg |
redirect_uri | The URI LuxID redirects to after authentication. Must exactly match a registered value |
scope | Space-separated list of scopes. Must include openid |
Security parameters (mandatory in practice)
| Parameter | Description |
|---|---|
state | A cryptographically random opaque value. LuxID echoes it back in the redirect. Validate it to prevent CSRF |
nonce | A cryptographically random opaque value. LuxID includes it in the ID token. Validate it to prevent token replay |
code_challenge | BASE64URL(SHA256(code_verifier)) - required for all clients |
code_challenge_method | Must be S256. The plain method is not supported |
Optional parameters
| Parameter | Description |
|---|---|
response_mode | fragment or form_post. Default is query for response_type=code. See section 10 |
acr_values | Requested minimum auth_level. Canonical form: urn:luxid:acr:level:low, urn:luxid:acr:level:substantial, or urn:luxid:acr:level:high - symmetric with the response acr claim. The accepted request values are agreed per Partner during onboarding. See Authentication levels. |
prompt | Controls login UI behaviour. See section 9 |
login_hint | Pre-fills the email field. Pass the user's known email address |
max_age | Maximum acceptable age of the authentication in seconds. Forces re-authentication if auth_time is older |
lang | Preferred login-page language (LuxID-specific query parameter, not OIDC ui_locales). Accepted values: fr, de, en, and lu for Luxembourgish (note: the parameter value is lu, even though the BCP47 language code is lb) |
id_token_hint | A previously issued ID token. Used with prompt=none to identify the current user |
For a single consolidated list of every authorize-endpoint parameter (required and optional, with accepted values and LuxID specifics), see Authorization request parameters.
Example authorisation request
GET https://login.luxid.lu/mga/sps/oauth/oauth20/authorize
?response_type=code
&client_id=my-app-client-id
&redirect_uri=https%3A%2F%2Fapp.example.lu%2Fcallback
&scope=openid%20profile%20email%20offline_access
&state=b8f3a2e1c7d049ab
&nonce=f1e2d3c4b5a60789
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&acr_values=urn:luxid:acr:level:substantial
&lang=fr
The accepted acr_values request values are agreed per Partner during onboarding.
Scopes
| Scope | Claims included | Notes |
|---|---|---|
openid | sub, iss, aud, exp, iat, auth_time, acr, amr, nonce | Always required |
profile | name, given_name, family_name, updated_at, birthdate | Standard profile claims |
email | email, email_verified | The LuxID identifier (user's email address) |
phone | phone_number, phone_number_verified | Optional field; only present if the user has set a phone number |
offline_access | (enables refresh_token in the token response) | Request only if your application genuinely needs offline access |
Only request scopes your application needs. The user sees a consent screen listing the data your application will receive.
The age_over_16 claim is defined in the OIDC schema but is not currently offered by LuxID.
Standard OIDC allows a client to request any scope or claim the server advertises at runtime. LuxID does not work that way: the released set of claims is pinned in your Application's Claim Template on the LuxID side, and the scope parameter you send does not gate release. You still need to send a valid scope (at minimum openid) to satisfy the OIDC spec, but what you actually receive is the Claim Template's set, subject to user consent. New claims are added by a Claim Template update via a request to LuxID. See Tokens and Claims - Requesting claims for the full model.
Token request
Exchange the authorisation code for tokens at the token endpoint using POST with Content-Type: application/x-www-form-urlencoded.
Request parameters
| Parameter | Description |
|---|---|
grant_type | authorization_code |
code | The authorisation code received in the callback |
redirect_uri | Must exactly match the value used in the authorisation request |
client_id | Your application's client identifier |
code_verifier | The original random string from which code_challenge was derived |
client_secret | Confidential clients only |
Client authentication methods
| Method | How to use |
|---|---|
client_secret_post | Include client_id and client_secret as body parameters |
client_secret_basic | Send Authorization: Basic BASE64(client_id:client_secret) header |
Public clients (SPAs, native mobile apps) omit the client secret and use PKCE only.
Token response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 600,
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "c3VwZXJzZWNyZXRyZWZyZXNo...",
"scope": "openid profile email offline_access"
}
refresh_token is only present when offline_access was in the requested scope and the client is authorised for refresh tokens.
Token lifetimes:
| Token | Lifetime |
|---|---|
| Access token | Short-lived (read expires_in / the exp claim) |
| ID token | ~1 hour |
| Refresh token | Long-lived (not returned in the response - ask LuxID) |
ID Token validation
You must validate the ID token on every token exchange. Perform all five mandatory checks defined in OIDC Core 1.0 §3.1.3.7.
Check 1 - signature
Fetch the JWKS from https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID. Find the key matching the kid header claim. Verify the RS256 signature using that key.
JWKS caching: Cache the key set. Only re-fetch when you encounter an unknown kid. Do not re-fetch on every request. See Key management.
Check 2 - issuer
iss must be exactly https://login.luxid.lu. Use string equality, not URL comparison.
Check 3 - audience
aud must contain your client_id. If aud is an array with more than one value, azp must be present and must equal your client_id.
Check 4 - expiry
exp must be in the future. Allow only a small clock skew (about a minute or less).
Check 5 - nonce
nonce in the token must equal the nonce you sent in the authorisation request. Consume and discard the stored nonce after first use to prevent replay.
Additional recommended checks
iatshould be recent (reject tokens issued more than a few minutes ago as a secondary replay guard).acrshould meet the minimumauth_levelyour application requires. Verify the returnedacrURN is at least the level you requested (for example,urn:luxid:acr:level:substantialor higher). See Authentication levels for theacrURN values and their ordering.auth_timemust satisfy anymax_ageconstraint you specified.
ID Token claims reference
| Claim | Type | Description |
|---|---|---|
sub | string | Stable, opaque user identifier. Use as primary key |
iss | string | Issuer: https://login.luxid.lu |
aud | string or array | Audience: your client_id |
exp | integer | Expiry (Unix timestamp) |
iat | integer | Issued at (Unix timestamp) |
auth_time | integer | Time of the authentication event (Unix timestamp) |
nonce | string | Echoed nonce from the authorisation request |
acr | string | LuxID's auth_level-based classification: urn:luxid:acr:level:low (auth_level 2), urn:luxid:acr:level:substantial (auth_level 3/4/8), or urn:luxid:acr:level:high (auth_level 9). Not a formal eIDAS LoA assertion. |
amr | array | Authentication methods used, e.g. ["pwd", "otp"], ["pwd", "totp"], ["fido"] |
name | string | Full display name (requires profile scope) |
given_name | string | First name (requires profile scope) |
family_name | string | Last name (requires profile scope) |
birthdate | string | Date of birth, YYYY-MM-DD format (requires profile scope) |
updated_at | integer | Last profile update (Unix timestamp; requires profile scope) |
email | string | Email address - the LuxID identifier (requires email scope) |
email_verified | boolean | Whether the email address has been verified (requires email scope) |
phone_number | string | Phone number in E.164 format (requires phone scope; absent if not set) |
phone_number_verified | boolean | Whether the phone number is verified (requires phone scope) |
age_over_16 | boolean | Defined in the schema but not currently offered |
For the full token anatomy, see Tokens and claims.
UserInfo endpoint
The UserInfo endpoint returns claims about the authenticated user as a JSON object. Send the access token as a Bearer token.
curl -s https://login.luxid.lu/mga/sps/oauth/oauth20/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"
The claims returned are those in your Application's Claim Template on the LuxID side (the scope parameter you sent does not gate release - see Tokens and claims for the full model). The sub claim is always present and must match the sub in the ID token - verify this to prevent token substitution attacks.
For full documentation, see UserInfo endpoint.
Refresh tokens
Use a refresh token to obtain a new access token without requiring the user to log in again.
Requirement: offline_access must be included in the original scope.
curl -X POST https://login.luxid.lu/mga/sps/oauth/oauth20/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=YOUR_REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Refresh token rotation: LuxID issues a new refresh token on each use. Replace the stored refresh token immediately. Using a rotated-away refresh token will result in an error.
Refresh token lifetime: long-lived; the exact lifetime is not returned in the token response - ask LuxID if you need it. Re-authentication is required after expiry.
Scope reduction: include a scope parameter to request a subset of the original scopes.
Silent re-authentication
Use prompt=none to check whether the user has an active LuxID session without displaying any UI. Useful for SPAs that want to silently renew tokens in a hidden iframe.
GET https://login.luxid.lu/mga/sps/oauth/oauth20/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.lu/silent-callback
&scope=openid
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&prompt=none
&id_token_hint=PREVIOUS_ID_TOKEN
Outcomes:
- Active LuxID session present: redirects to
redirect_uriwith a newcode, no UI shown. - No active session or consent required: redirects with
error=login_requiredorerror=consent_required.
id_token_hint is strongly recommended with prompt=none to identify which user's session to check.
prompt parameter values
| Value | Behaviour |
|---|---|
none | No UI. Error if authentication or consent is needed |
login | Force re-authentication even if the user has an active session |
consent | Force re-display of the consent screen |
select_account | Not applicable. LuxID is a single-account IdP: a browser holds at most one LuxID session, so there is no account chooser to display |
Logout
LuxID does not support OIDC RP-initiated logout
LuxID does not expose the OIDC end_session_endpoint and does not implement OpenID Connect RP-Initiated Logout 1.0. Omitting RP-initiated logout specifically is an architectural decision: LuxID is an SSO authority used by many Partners simultaneously, and a single application should not be able to terminate the LuxID session that other unrelated applications still rely on. The application is responsible for ending its own session. Front-channel and back-channel logout are separate specifications and remain under review - see below.
User-driven LuxID sign-out
If the user explicitly wants to end their LuxID session as well as their application session, your application can redirect the browser to:
https://login.luxid.lu/auth/logout?client_id=<your-client-id>
LuxID renders a logout confirmation screen that displays your application name (derived from the client_id). The user then chooses whether to also end the LuxID session. Your application does not make that decision on the user's behalf.
This is the right pattern for shared-device deployments and high-assurance scenarios.
Local logout only
To clear only the application session without terminating the LuxID session:
- Invalidate the server-side session record.
- Expire and clear the session cookie.
- Do not redirect to the LuxID logout endpoint.
The user remains authenticated at LuxID and can return without re-entering credentials (subject to any max_age constraint).
Front-channel and back-channel logout
Support for front-channel and back-channel logout is under review. Contact POST Luxembourg to confirm availability before implementing.
Session management
The OIDC Session Management 1.0 specification provides a browser-based mechanism for SPAs to detect session changes. Confirm availability with POST Luxembourg before implementing.
Response modes
The response_mode parameter controls how LuxID delivers the authorisation response to your redirect_uri.
| Mode | Delivery | Use case |
|---|---|---|
query | Query string parameters | Default for response_type=code. Server-side apps |
fragment | URL fragment (#code=...&state=...) | SPAs processing the redirect client-side |
form_post | HTTP POST with form body | Prevents the code appearing in server logs |
form_post example
When response_mode=form_post, LuxID returns an HTTP 200 response with an auto-submitting form:
<form method="POST" action="https://app.example.lu/callback">
<input type="hidden" name="code" value="AUTHORISATION_CODE" />
<input type="hidden" name="state" value="STATE_VALUE" />
</form>
<script>document.forms[0].submit();</script>
Your callback endpoint must accept POST requests and read parameters from the request body.
Device flow (RFC 8628)
The OAuth 2.0 Device Authorization Grant (RFC 8628 (opens in a new tab)) targets devices with limited input capability - smart TVs, CLI tools, embedded systems.
The discovery document does expose a device_authorize_endpoint (note: a non-standard field name, not the RFC 8628 device_authorization_endpoint), and the endpoint is reachable - but urn:ietf:params:oauth:grant-type:device_code is not listed in grant_types_supported, so the device flow is not enabled for Partner use today. The flow below is kept as reference; if your integration needs it (smart TV, CLI, etc.), contact LuxID to have it enabled. Re-check grant_types_supported in the discovery document before building against it.
# Step 1: Request a device code
curl -X POST https://login.luxid.lu/mga/sps/oauth/oauth20/device_authorize \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "scope=openid profile email"
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://link.luxid.lu",
"verification_uri_complete": "https://link.luxid.lu/?code=WDJB-MJHT",
"expires_in": 900,
"interval": 5
}
Display user_code and verification_uri to the user. Poll the token endpoint until the user completes authentication or the code expires.
# Step 2: Poll for token (repeat at interval seconds)
curl -X POST https://login.luxid.lu/mga/sps/oauth/oauth20/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS" \
-d "client_id=YOUR_CLIENT_ID"
Poll responses:
| Error | Meaning |
|---|---|
authorization_pending | User has not yet completed authentication - keep polling |
slow_down | Reduce polling frequency - increase interval by 5 seconds |
expired_token | Device code expired - restart the flow |
access_denied | User denied the request |
| (no error) | Success - token response is in the body |
The verification_uri is the short, brandable activation URL https://link.luxid.lu (opens in a new tab); the verification_uri_complete pre-fills the code as https://link.luxid.lu/?code=WDJB-MJHT (opens in a new tab) so a QR code can take the user straight to the consent screen without manual code entry.
Related references
- Add Login - step-by-step quickstart
- OAuth 2.0 for APIs - bearer tokens, introspection, revocation
- Tokens and claims - full claim anatomy
- Authentication levels -
auth_levelscale,acr_valuesmechanism, and informal eIDAS alignment - Session management - local session lifecycle and logout
- Key management - JWKS caching and key rotation
- Advanced security options - PAR and JAR (not currently implemented)
- UserInfo endpoint - UserInfo detail
- OAuth and OIDC error codes - error reference