Aller au contenu principal
Version 0.6Brouillon

Configure LuxID

Overview

This page walks through the complete path from zero to a working test login. There are five stages:

  1. Become a LuxID Partner (business agreement)
  2. Register your Application (identity, redirect URIs, Claim Templates, access rules)
  3. Communicate your UAT passlist entries (email domains, phone numbers)
  4. Configure your environment (UAT then production)
  5. Run the first test login (Authorization Code + PKCE)

Stage 1 - become a LuxID Partner

LuxID is a closed ecosystem. You must sign a LuxID Agreement before receiving credentials.

Contacts:

  • Business enquiries (joining LuxID, commercial discussions): LuxID
  • Operational support (onboarding follow-up, technical questions, incidents): LuxID

During onboarding, POST Luxembourg will:

  • Confirm your Organisation's eligibility
  • Provide the LuxID Agreement for signature
  • Create your Organisation record and assign a Partner
  • Provision your UAT credentials

The LuxID Agreement defines the permitted use cases, data handling obligations, and acceptable claim scope. Read it carefully - it constrains what claims you may request.

The Agreement also includes an Ethical Code of Conduct that LuxID Partners commit to respecting. The Code sets the behavioural standards expected of every Partner regarding user privacy, fair use of the LuxID surface, and overall ecosystem stewardship.

There is no self-serve sign-up page. By design, LuxID keeps Partner onboarding human-driven and values proximity to its Partners - the first step is a conversation, not a form. Reach out via the contacts above and LuxID will guide you from there.

How the conversation unfolds

After initial contact, LuxID follows up to understand your needs. A short exchange between technical teams typically clarifies which standard (OIDC, OAuth 2.0, or SAML) fits best, what claims you'll need, and what assurance level your service requires. From that exchange, LuxID creates your Application in the UAT environment based on the registration information you provide.

Stage 2 - register your Application

You provide the Application registration information to LuxID; LuxID performs the registration on your behalf and returns your credentials. The dedicated Register an Application page is the canonical list of every field, with formats, constraints, and examples - use it as the template for your submission.

At a high level, expect to provide:

  • Application identity - a display name and logo (square .png/.jpeg, min 64x64 px, works in light and dark mode), shown on the LuxID consent screen.
  • Protocol - OIDC, OAuth 2.0, or SAML.
  • Public URL - the landing page of your service.
  • Redirect URI(s) - one or several callback URLs. LuxID enforces exact-match on these, so register every variant you actually use (e.g. https://app.example.lu/auth/callback, myapp://auth/callback, http://localhost:3000/auth/callback for UAT).
  • Contacts - an administrative contact and a technical contact.
  • Claim Templates - the claims your Application will request (e.g. given_name, email, phone_number), each marked essential or voluntary, with optional per-claim hint labels. LuxID reviews the requested set against data minimisation; you can only receive claims that were declared at registration. See the available claims list below.
  • Access rules - any Auth requirements (minimum auth_level), Guard rules, or User rules you want LuxID to evaluate on sign-in. Can also be added later by request.

A few notes specific to this stage:

  • Sphere assignment is not a Partner choice. LuxID assigns your Application to a Sphere as an outcome of the technical exchange in Stage 1, based on which other Applications you should (or should not) share a pseudonymous sub with. If you want consistent sub values across several of your Applications - or across Applications from multiple Organisations - tell LuxID early so the Sphere is set up correctly. See Plan and design for the design rationale.
  • Redirect URIs are exact-match. HTTPS is required in Production; http://localhost is allowed in UAT only; no wildcards. An Application can have several redirect URIs registered at once, and LuxID accepts any request whose redirect_uri matches one of them. This is what makes a zero-downtime migration possible: register the new callback alongside the old one, cut traffic over at your own pace, then ask LuxID to remove the old URI once the new one is validated. See Redirect URIs and domains for the full rules.

What you will receive

Once your Application has been registered by LuxID, you will receive:

  • Client ID - a stable public identifier for your Application
  • Client Secret - a confidential shared secret (treat as a password; rotate periodically)

LuxID Partner authentication is based on Client ID and Client Secret. A self-service LuxID Console is on the roadmap; in the interim, Application configuration changes happen by request to LuxID (see LuxID Console for the future surface and the current request-based procedure).

Available claims

LuxID supports the following OIDC standard claims. You may only receive claims that were declared at registration and that the user consented to share.

ClaimScope requiredDescription
subopenidPseudonymous user identifier (Sphere-scoped)
nameprofileFull name
given_nameprofileGiven name
family_nameprofileFamily name
birthdateprofileDate of birth (YYYY-MM-DD)
genderprofileGender, self-declared in the user's profile (not verified)
updated_atprofileLast profile update timestamp
emailemailEmail address
email_verifiedemailWhether email has been verified
phone_numberphonePhone number
phone_number_verifiedphoneWhether phone number has been verified
addressaddressPostal address (object); available via Claim Template, not advertised in discovery
age_over_16profileBoolean age gate. Defined in the OIDC schema but not currently offered by LuxID

Apply the principle of claim minimisation - request only what your application genuinely needs. Requesting unnecessary claims increases consent friction and creates data liability.

Partner-specific (technical) claims

Beyond the standard claims above, your partnership can have bespoke technical claims provisioned by LuxID - partner-supplied data such as a membership or registration number that LuxID carries back to you on every sign-in. They follow the luxid_<partner-prefix>_<name> naming convention, are declared in your Claim Template, and their value is set per user by your back-end via the Partner API claims capability. A declared-but-never-set technical claim does not appear in the token. See Tokens and claims - partner-specific technical claims.

Stage 3 - communicate your UAT passlist entries

The UAT environment uses a passlist (whitelist) on the user-facing communication channels to keep test traffic isolated from real users. Before you can complete an end-to-end sign-in flow in UAT, you need to register at least:

  • The email domain(s) the test user accounts will be on - LuxID's UAT only sends activation links, password resets, and other transactional emails to addresses whose domain is on the passlist.
  • The phone number(s) the test users will use, if your Application requires SMS-based OTP - LuxID's UAT only sends SMS and voice calls to numbers on the passlist.

Provide these to LuxID during onboarding (or by email to LuxID).

Domain rule. The email domain you register must be a domain you own and control (typically your own corporate domain, or a dedicated test sub-domain such as qa.example.lu). Free email providers - Gmail, Outlook.com, Yahoo, ProtonMail, iCloud, and similar - are not accepted on the UAT passlist. This is an intentional safety guardrail to prevent test signups from spilling into real consumer mailboxes and to keep UAT traffic clearly distinguishable from production.

Phone rule. Phone numbers are registered as full E.164 strings (e.g. +352621234567). Wildcards are not supported. Register only numbers you control - LuxID will send real OTP messages to them.

Symptoms of missing passlist entries: activation emails never arrive, OTP SMS never arrives, password reset links are not delivered. If you see any of these during UAT testing, check first whether the recipient domain or number is on the passlist.

Production has no passlist - LuxID sends to any valid recipient. The passlist exists only in UAT.

Stage 4 - choose your environment

LuxID provides two environments. Use UAT for all development and testing. Never test against production.

EnvironmentBase URLDiscovery document
UAThttps://login-uat.luxid.lu (opens in a new tab)https://login-uat.luxid.lu/.well-known/openid-configuration (opens in a new tab)
Productionhttps://login.luxid.lu (opens in a new tab)https://login.luxid.lu/.well-known/openid-configuration (opens in a new tab)

All endpoint paths are identical between environments. Replace the host to switch. SAML metadata:

EnvironmentSAML IdP metadata
UATDownload UAT SAML metadata
ProductionDownload production SAML metadata

OIDC endpoints (production)

EndpointURL
Issuerhttps://login.luxid.lu (opens in a new tab)
Authorizationhttps://login.luxid.lu/mga/sps/oauth/oauth20/authorize (opens in a new tab)
Tokenhttps://login.luxid.lu/mga/sps/oauth/oauth20/token (opens in a new tab)
UserInfohttps://login.luxid.lu/mga/sps/oauth/oauth20/userinfo (opens in a new tab)
JWKShttps://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID (opens in a new tab)
Introspectionhttps://login.luxid.lu/mga/sps/oauth/oauth20/introspect (opens in a new tab)
Revocationhttps://login.luxid.lu/mga/sps/oauth/oauth20/revoke (opens in a new tab)
Device authorisehttps://login.luxid.lu/mga/sps/oauth/oauth20/device_authorize (opens in a new tab)

Fetch the discovery document to confirm endpoints and supported capabilities programmatically. Most OIDC libraries do this automatically.

Stage 5 - first test login

The recommended flow for all new integrations is Authorization Code + PKCE (RFC 7636 (opens in a new tab)). It is secure for both public clients (SPAs, mobile apps) and confidential clients (server-side apps).

Flow overview

Step-by-step

Step 1 - Generate PKCE values

Generate a cryptographically random code_verifier (43-128 characters, URL-safe). Derive code_challenge as the Base64-URL-encoded SHA-256 hash of the verifier.

code_verifier=$(openssl rand -base64 64 | tr -d '=+/' | cut -c1-64)
code_challenge=$(printf '%s' "$code_verifier" \
| openssl dgst -sha256 -binary \
| openssl base64 -A \
| tr '+/' '-_' \
| tr -d '=')

echo "code_verifier=$code_verifier"
echo "code_challenge=$code_challenge"

Step 2 - Generate state and nonce

state=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-32)
nonce=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-32)

Store state, nonce, and code_verifier server-side (or in a short-lived session cookie) before redirecting.

Step 3 - Build the authorisation URL

Assemble the URL from the values produced in Steps 1-2 (code_challenge, state, nonce) plus your Client ID and redirect URI. The script prints the URL - redirect the user's browser to it to start the login.

# Reuses code_challenge, state, nonce from the previous steps
authorize_endpoint="https://login-uat.luxid.lu/mga/sps/oauth/oauth20/authorize"
client_id="$LUXID_CLIENT_ID"
redirect_uri="https://app.example.lu/auth/callback"
scope="openid profile email"

# Minimal RFC 3986 encoder (state/nonce/code_challenge are already URL-safe)
urlencode() {
local s="$1" o="" i c
for (( i=0; i<${#s}; i++ )); do
c="${s:i:1}"
case "$c" in
[a-zA-Z0-9.~_-]) o+="$c" ;;
*) printf -v c '%%%02X' "'$c"; o+="$c" ;;
esac
done
printf '%s' "$o"
}

authorization_url="${authorize_endpoint}?response_type=code"
authorization_url+="&client_id=$(urlencode "$client_id")"
authorization_url+="&redirect_uri=$(urlencode "$redirect_uri")"
authorization_url+="&scope=$(urlencode "$scope")"
authorization_url+="&state=${state}"
authorization_url+="&nonce=${nonce}"
authorization_url+="&code_challenge=${code_challenge}"
authorization_url+="&code_challenge_method=S256"

echo "$authorization_url"

Step 4 - Handle the callback

LuxID redirects to your redirect_uri with the authorisation code and your state:

Redirect callback
https://app.example.lu/auth/callback?code=AUTH_CODE&state=STATE

Parse that callback URL, then verify state matches the value you stored in Step 2 before using the code - this is the CSRF protection check. The script extracts the code into the variable Step 5 expects.

# The full callback URL your redirect_uri received (from your web framework's request)
callback_url="https://app.example.lu/auth/callback?code=AUTH_CODE&state=STATE"

query="${callback_url#*\?}"
get_param() { printf '%s' "$query" | tr '&' '\n' | grep "^$1=" | cut -d= -f2- ; }

returned_state=$(get_param state)

# CSRF check against the state stored in Step 2
if [ "$returned_state" = "$state" ]; then
AUTH_CODE=$(get_param code)
echo "AUTH_CODE=$AUTH_CODE"
else
echo "State mismatch - abort (possible CSRF)" >&2
fi

Step 5 - Exchange the code for tokens

POST the authorisation code, along with the code_verifier from Step 1 and your Client ID + Client Secret, to the token endpoint.

curl -sS -X POST "https://login-uat.luxid.lu/mga/sps/oauth/oauth20/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.lu/auth/callback" \
--data-urlencode "client_id=$LUXID_CLIENT_ID" \
--data-urlencode "client_secret=$LUXID_CLIENT_SECRET" \
--data-urlencode "code_verifier=$code_verifier"

The response is a JSON object:

Token response
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 600,
"id_token": "...",
"refresh_token": "...",
"scope": "openid profile email"
}

Step 6 - Validate the ID token

Before trusting any claims, validate the ID token:

  1. Fetch the JWKS from the environment-appropriate URL and cache it:
  2. Verify the JWT signature using the matching key (kid header claim).
  3. Verify iss equals https://login-uat.luxid.lu (UAT) or https://login.luxid.lu (production).
  4. Verify aud contains your client_id.
  5. Verify exp is in the future.
  6. Verify nonce matches the value you stored in Step 2.

Use a well-maintained OIDC library for your language rather than implementing JWT validation by hand. Most libraries perform all six checks automatically when configured with the issuer and client ID.

Step 7 - Establish your application session

Once the ID token is validated, create your own application session (e.g. a signed cookie or server-side session record) keyed on the sub claim. Store the refresh token securely for silent renewal.

Do not pass the access token to your frontend unless your frontend needs to call a resource server. The ID token is for your server only - do not expose it to the browser.

Token lifetimes

TokenLifetime
Access tokenShort-lived
ID token~1 hour
Refresh tokenLong-lived

You can read the exact access token lifetime from the expires_in field in the token response and from the exp claim in the access and ID tokens. The refresh token lifetime is not returned in the response - ask LuxID if you need the exact value. Do not hardcode lifetimes; drive renewal from the exp claim.

LuxID rotates refresh tokens on use, with immediate revocation of the previous pair. When you exchange a refresh token at the token endpoint, LuxID issues a new access token AND a new refresh token, and the previous access token and the previous refresh token are revoked immediately - not lazily on first use of the new pair (as some other IdPs do).

Two practical consequences:

  • Always discard the old refresh token and persist the new one as soon as you receive the token response. A successful exchange is the point of no return.
  • Serialise concurrent refresh attempts. If two threads, processes, or app instances try to refresh from the same refresh token in parallel, one will succeed and invalidate the token while the other is mid-flight. The losing call gets invalid_grant and the user is forced back to interactive sign-in. Guard the refresh path with a per-user mutex or a single-flight pattern.

Troubleshooting your first login

If the authorisation redirect fails, check:

  • The redirect_uri exactly matches a registered URI (scheme, host, port, path, no trailing slash difference)
  • The client_id is for the correct environment (UAT vs production)
  • The scope values are space-separated and URL-encoded

If the token exchange fails with invalid_grant:

  • The authorisation code has already been used (codes are single-use)
  • The code_verifier does not match the code_challenge from Step 3
  • The code has expired (codes are short-lived - exchange it promptly)

For a full error reference see Common errors.

Next steps

Mise à jour le 2026-07-03