Code samples and reference repos
Overview
This page is a curated index of community sample repositories and validation utilities that work with LuxID by pointing the issuer at https://login.luxid.lu. None of these repos are maintained by POST Luxembourg; they are canonical upstream examples maintained by the library authors or the broader open-source community.
Each entry includes a short adaptation note - the minimal set of changes needed to point the sample at LuxID rather than the demo issuer it ships with.
For library-specific wiring instructions (install commands, config snippets, things to watch out for), see Platform and framework guides.
Reference repositories by ecosystem
Java / spring security
Canonical sample: spring-projects/spring-security-samples (opens in a new tab) - the servlet/spring-boot/java/oauth2/login sub-project.
Adaptation guide: Open src/main/resources/application.yml. Replace the spring.security.oauth2.client.provider.<name>.issuer-uri value with https://login.luxid.lu. Set client-id and client-secret from environment variables (LUXID_CLIENT_ID, LUXID_CLIENT_SECRET). Remove any provider-specific authorization-uri / token-uri overrides - Spring Security resolves them automatically from the discovery document. The redirect URI template {baseUrl}/login/oauth2/code/{registrationId} needs no change.
.NET / ASP.NET core
Canonical sample: dotnet/aspnetcore (opens in a new tab) - the src/Security/samples/OpenIdConnectSample project inside the monorepo.
Adaptation guide: In Program.cs, set options.Authority = "https://login.luxid.lu". Replace the hardcoded ClientId and ClientSecret with values from environment variables or user secrets (dotnet user-secrets set "LuxID:ClientId" "..."). Verify GetClaimsFromUserInfoEndpoint = true is set so that profile and email claims are populated. The sample ships with SaveTokens = true by default; leave it enabled.
Node.js
Canonical sample: panva/openid-client (opens in a new tab) - the examples/ directory contains an Express-based Authorization Code + PKCE flow.
Adaptation guide: In the example entry point, replace the Issuer.discover(...) URL with https://login.luxid.lu. Set client_id and client_secret from process.env.LUXID_CLIENT_ID and process.env.LUXID_CLIENT_SECRET. Update redirect_uris to match your local or deployed callback URL. No other changes are needed - PKCE and state handling are already present in the example.
Next.js
Canonical sample: nextauthjs/next-auth (opens in a new tab) - the apps/examples/nextjs app in the monorepo.
Adaptation guide: In auth.ts (or [...nextauth]/route.ts for Pages Router), add a custom OIDC provider entry with issuer: 'https://login.luxid.lu' and type: 'oidc'. Set LUXID_CLIENT_ID and LUXID_CLIENT_SECRET in .env.local. Generate AUTH_SECRET with openssl rand -base64 32. Remove any pre-configured provider (GitHub, Google) if you want a clean single-provider setup.
React SPA
Canonical sample: authts/react-oidc-context (opens in a new tab) - a thin React context wrapper around oidc-client-ts with a complete example app.
Adaptation guide: In the AuthProvider configuration, set authority="https://login.luxid.lu", client_id from an environment variable, and redirect_uri to your app's callback route. The sample uses response_type="code" and PKCE by default. Do not add a client_secret - register as a public client with LuxID. If you need token refresh, implement the BFF pattern rather than storing the refresh token in the browser.
Angular
Canonical sample: damienbod/angular-auth-oidc-client (opens in a new tab) - the projects/sample-code-flow app in the monorepo.
Adaptation guide: In app.config.ts, set authority: 'https://login.luxid.lu' and clientId from an environment variable injected at build time. The sample already uses responseType: 'code' and PKCE. Remove any stsServer references to the demo identity server. Set redirectUrl and postLogoutRedirectUri to match your app's routes.
React native
Canonical sample: FormidableLabs/react-native-app-auth (opens in a new tab) - the Example/ directory.
Adaptation guide: In the config object inside the example screen, set issuer: 'https://login.luxid.lu' and clientId from a build-time constant or environment variable. Update redirectUrl to your app's custom URI scheme (e.g. com.yourcompany.yourapp:/auth/callback) and register that URI with LuxID. Ensure usePKCE: true (it is the default). Store the returned refreshToken in the platform keychain, not in component state.
iOS native
Canonical sample: openid/AppAuth-iOS (opens in a new tab) - the Examples/Example-iOS target.
Adaptation guide: In AppAuthExampleViewController.swift, replace the kIssuer constant with https://login.luxid.lu and kClientID with your client ID. Remove any kClientSecret assignment if you are registering as a public client. Update kRedirectURI to match your registered redirect URI. The example handles PKCE and Universal Links out of the box - no structural changes are needed.
Android native
Canonical sample: openid/AppAuth-Android (opens in a new tab) - the app/ module in the repo root.
Adaptation guide: In res/raw/auth_config.json, set "issuer": "https://login.luxid.lu", "client_id" to your client ID, and "redirect_uri" to your registered redirect URI. Update AndroidManifest.xml to use your custom URI scheme. For production, switch to App Links (HTTPS redirect URIs) and configure Digital Asset Links. The sample already uses PKCE by default.
PHP
Canonical sample: jumbojett/OpenID-Connect-PHP (opens in a new tab) - the example/ directory.
Adaptation guide: In the example script, replace the provider URL argument with https://login.luxid.lu and supply LUXID_CLIENT_ID and LUXID_CLIENT_SECRET from environment variables (via getenv()). Ensure setVerifyHost(true) and setVerifyPeer(true) are set. Add setCodeChallengeMethod('S256') to enable PKCE - it is not on by default in older versions of the library.
Python
Canonical sample: lepture/authlib (opens in a new tab) - the examples/flask/ directory.
Adaptation guide: In app.py, change the server_metadata_url in oauth.register(...) to https://login.luxid.lu/.well-known/openid-configuration. Set client_id and client_secret from environment variables. Add 'code_challenge_method': 'S256' to client_kwargs to enable PKCE. Replace the file-based session with a server-side session backend (flask-session + Redis) before deploying to production.
Token validation utilities
The libraries listed above handle token validation automatically. The snippets below are for scenarios where you need to validate a LuxID ID token or access token independently - for example, in an API gateway, a background job, or a test harness.
LuxID signs tokens with RS256. The public keys are available at:
https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID
All validators must check the claims mandated by OIDC Core §3.1.3.7 (opens in a new tab):
issequalshttps://login.luxid.luaudcontains yourclient_idexpis in the futureiatis not unreasonably far in the past (leeway of 60 seconds is typical)algin the JOSE header isRS256(rejectnone)
- Java
- Node.js
- Python
- Go
- .NET
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.*;
import com.nimbusds.jwt.proc.*;
import com.nimbusds.jwt.*;
import java.net.URL;
// Build a processor with JWKS-backed key source
JWKSource<SecurityContext> keySource = new RemoteJWKSet<>(
new URL("https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID")
);
ConfigurableJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>();
processor.setJWSKeySelector(
new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keySource)
);
// Configure mandatory OIDC claims validation
DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier =
new DefaultJWTClaimsVerifier<>(
new JWTClaimsSet.Builder()
.issuer("https://login.luxid.lu")
.audience("YOUR_CLIENT_ID")
.build(),
Set.of("sub", "iat", "exp")
);
processor.setJWTClaimsSetVerifier(claimsVerifier);
// Validate
JWTClaimsSet claims = processor.process(idTokenString, null);
String sub = claims.getSubject();
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID')
);
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: 'https://login.luxid.lu',
audience: process.env.LUXID_CLIENT_ID,
algorithms: ['RS256'],
clockTolerance: 60, // seconds
});
console.log('Subject:', payload.sub);
console.log('Email:', payload.email);
The jose library fetches and caches the JWKS automatically, with key rotation handled transparently.
import jwt
from jwt import PyJWKClient
jwks_url = (
"https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID"
)
jwks_client = PyJWKClient(jwks_url)
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
claims = jwt.decode(
id_token,
signing_key,
algorithms=["RS256"],
audience=os.environ["LUXID_CLIENT_ID"],
issuer="https://login.luxid.lu",
leeway=60,
options={"verify_exp": True, "verify_iat": True},
)
print("Subject:", claims["sub"])
print("Email:", claims.get("email"))
Requires pip install PyJWT[crypto].
package main
import (
"context"
"fmt"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
func validateLuxIDToken(tokenString, clientID string) (jwt.MapClaims, error) {
jwks, err := keyfunc.NewDefaultCtx(
context.Background(),
[]string{"https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID"},
)
if err != nil {
return nil, fmt.Errorf("JWKS init: %w", err)
}
token, err := jwt.ParseWithClaims(
tokenString,
&jwt.MapClaims{},
jwks.Keyfunc,
jwt.WithIssuer("https://login.luxid.lu"),
jwt.WithAudience(clientID),
jwt.WithValidMethods([]string{"RS256"}),
)
if err != nil {
return nil, fmt.Errorf("token validation: %w", err)
}
claims, ok := token.Claims.(*jwt.MapClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return *claims, nil
}
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
// Retrieve signing keys via OIDC discovery (caches automatically)
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
"https://login.luxid.lu/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever()
);
var oidcConfig = await configManager.GetConfigurationAsync();
var validationParams = new TokenValidationParameters
{
ValidIssuer = "https://login.luxid.lu",
ValidAudience = Environment.GetEnvironmentVariable("LUXID_CLIENT_ID"),
IssuerSigningKeys = oidcConfig.SigningKeys,
ValidAlgorithms = new[] { "RS256" },
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(60),
ValidateIssuerSigningKey = true,
};
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(idTokenString, validationParams, out _);
var sub = principal.FindFirst("sub")?.Value;
var email = principal.FindFirst("email")?.Value;
Minimal end-to-end CLI demo
The following shell script performs a complete Authorization Code + PKCE flow against LuxID using only curl, jq, and openssl. It is useful for sanity-testing connectivity to UAT before plugging in a real client library.
Prerequisites:
curl,jq, andopensslmust be on yourPATH. You will need a registered redirect URI ofhttp://localhost:8765/callbackwith LuxID for the test client.
#!/usr/bin/env bash
set -euo pipefail
ISSUER="https://login-uat.luxid.lu"
CLIENT_ID="${LUXID_CLIENT_ID:?Set LUXID_CLIENT_ID}"
CLIENT_SECRET="${LUXID_CLIENT_SECRET:?Set LUXID_CLIENT_SECRET}"
REDIRECT_URI="http://localhost:8765/callback"
SCOPE="openid profile email"
# 1. Generate PKCE code verifier and challenge
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | tr -d '\n' | cut -c1-128)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 \
| tr '+/' '-_' \
| tr -d '=\n')
# 2. Discover authorisation endpoint
AUTH_EP=$(curl -sf "$ISSUER/.well-known/openid-configuration" \
| jq -r '.authorization_endpoint')
TOKEN_EP=$(curl -sf "$ISSUER/.well-known/openid-configuration" \
| jq -r '.token_endpoint')
STATE=$(openssl rand -hex 16)
# 3. Print the authorisation URL - open it in a browser, then paste the callback URL
AUTH_URL="${AUTH_EP}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=$(jq -rn --arg s "$SCOPE" '$s|@uri')&state=${STATE}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256"
echo ""
echo "Open this URL in your browser:"
echo "$AUTH_URL"
echo ""
echo "After login, paste the full callback URL (http://localhost:8765/callback?code=...&state=...):"
read -r CALLBACK_URL
# 4. Extract authorisation code
CODE=$(printf '%s' "$CALLBACK_URL" | grep -oP '(?<=code=)[^&]+')
RETURNED_STATE=$(printf '%s' "$CALLBACK_URL" | grep -oP '(?<=state=)[^&]+')
if [[ "$RETURNED_STATE" != "$STATE" ]]; then
echo "State mismatch - possible CSRF. Aborting." >&2
exit 1
fi
# 5. Exchange code for tokens
TOKEN_RESPONSE=$(curl -sf -X POST "$TOKEN_EP" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=authorization_code" \
-d "code=${CODE}" \
-d "redirect_uri=${REDIRECT_URI}" \
-d "code_verifier=${CODE_VERIFIER}")
echo ""
echo "Token response:"
echo "$TOKEN_RESPONSE" | jq .
# 6. Decode and print the ID token payload (base64 only - no signature verification)
ID_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.id_token')
PAYLOAD=$(echo "$ID_TOKEN" | cut -d. -f2 | tr '_-' '/+' | openssl base64 -d 2>/dev/null || true)
echo ""
echo "ID token claims (decoded, not verified):"
echo "$PAYLOAD" | jq .
Note: The final step decodes the ID token payload for inspection only. It does not verify the signature. In production, always use a library that performs full RS256 signature verification against the JWKS endpoint before trusting any claim. See the token validation utilities above.