Skip to main content
Version 0.4Draft

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:

FieldValue
issuerhttps://login.luxid.lu
authorization_endpointhttps://login.luxid.lu/mga/sps/oauth/oauth20/authorize
token_endpointhttps://login.luxid.lu/mga/sps/oauth/oauth20/token
userinfo_endpointhttps://login.luxid.lu/mga/sps/oauth/oauth20/userinfo
jwks_urihttps://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID
introspection_endpointhttps://login.luxid.lu/mga/sps/oauth/oauth20/introspect
revocation_endpointhttps://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

ParameterDescription
response_typeMust be code for the Authorization Code flow. Only code and none are supported. Implicit flow (id_token) is not supported
client_idYour application's client identifier, issued by POST Luxembourg
redirect_uriThe URI LuxID redirects to after authentication. Must exactly match a registered value
scopeSpace-separated list of scopes. Must include openid

Security parameters (mandatory in practice)

ParameterDescription
stateA cryptographically random opaque value. LuxID echoes it back in the redirect. Validate it to prevent CSRF
nonceA cryptographically random opaque value. LuxID includes it in the ID token. Validate it to prevent token replay
code_challengeBASE64URL(SHA256(code_verifier)) - required for all clients
code_challenge_methodMust be S256. The plain method is not supported

Optional parameters

ParameterDescription
response_modefragment or form_post. Default is query for response_type=code. See section 10
acr_valuesRequested 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.
promptControls login UI behaviour. See section 9
login_hintPre-fills the email field. Pass the user's known email address
max_ageMaximum acceptable age of the authentication in seconds. Forces re-authentication if auth_time is older
langPreferred 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_hintA 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

ScopeClaims includedNotes
openidsub, iss, aud, exp, iat, auth_time, acr, amr, nonceAlways required
profilename, given_name, family_name, updated_at, birthdateStandard profile claims
emailemail, email_verifiedThe LuxID identifier (user's email address)
phonephone_number, phone_number_verifiedOptional 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.

LuxID divergence

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

ParameterDescription
grant_typeauthorization_code
codeThe authorisation code received in the callback
redirect_uriMust exactly match the value used in the authorisation request
client_idYour application's client identifier
code_verifierThe original random string from which code_challenge was derived
client_secretConfidential clients only

Client authentication methods

MethodHow to use
client_secret_postInclude client_id and client_secret as body parameters
client_secret_basicSend 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:

TokenLifetime
Access tokenShort-lived (read expires_in / the exp claim)
ID token~1 hour
Refresh tokenLong-lived (not returned in the response - ask LuxID)

ID Token validation

caution

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.

  • iat should be recent (reject tokens issued more than a few minutes ago as a secondary replay guard).
  • acr should meet the minimum auth_level your application requires. Verify the returned acr URN is at least the level you requested (for example, urn:luxid:acr:level:substantial or higher). See Authentication levels for the acr URN values and their ordering.
  • auth_time must satisfy any max_age constraint you specified.

ID Token claims reference

ClaimTypeDescription
substringStable, opaque user identifier. Use as primary key
issstringIssuer: https://login.luxid.lu
audstring or arrayAudience: your client_id
expintegerExpiry (Unix timestamp)
iatintegerIssued at (Unix timestamp)
auth_timeintegerTime of the authentication event (Unix timestamp)
noncestringEchoed nonce from the authorisation request
acrstringLuxID'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.
amrarrayAuthentication methods used, e.g. ["pwd", "otp"], ["pwd", "totp"], ["fido"]
namestringFull display name (requires profile scope)
given_namestringFirst name (requires profile scope)
family_namestringLast name (requires profile scope)
birthdatestringDate of birth, YYYY-MM-DD format (requires profile scope)
updated_atintegerLast profile update (Unix timestamp; requires profile scope)
emailstringEmail address - the LuxID identifier (requires email scope)
email_verifiedbooleanWhether the email address has been verified (requires email scope)
phone_numberstringPhone number in E.164 format (requires phone scope; absent if not set)
phone_number_verifiedbooleanWhether the phone number is verified (requires phone scope)
age_over_16booleanDefined 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_uri with a new code, no UI shown.
  • No active session or consent required: redirects with error=login_required or error=consent_required.

id_token_hint is strongly recommended with prompt=none to identify which user's session to check.

prompt parameter values

ValueBehaviour
noneNo UI. Error if authentication or consent is needed
loginForce re-authentication even if the user has an active session
consentForce re-display of the consent screen
select_accountNot 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

info

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:

  1. Invalidate the server-side session record.
  2. Expire and clear the session cookie.
  3. 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.

ModeDeliveryUse case
queryQuery string parametersDefault for response_type=code. Server-side apps
fragmentURL fragment (#code=...&state=...)SPAs processing the redirect client-side
form_postHTTP POST with form bodyPrevents 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.

Not enabled for Partners today

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:

ErrorMeaning
authorization_pendingUser has not yet completed authentication - keep polling
slow_downReduce polling frequency - increase interval by 5 seconds
expired_tokenDevice code expired - restart the flow
access_deniedUser 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.


Updated 2026-07-02