Skip to main content
Version 0.2Draft

Key management

Overview

Every ID Token issued by LuxID is signed using an asymmetric key pair. Your application must verify this signature before trusting any claims in the token. This page explains:

  • Which algorithm LuxID uses to sign tokens
  • How to find the correct public key using the JWKS endpoint
  • How to cache JWKS responses efficiently
  • How key rotation works and how to handle it without service disruption
  • How to keep your Client Secret secure

ID Token signing algorithm

LuxID signs ID Tokens using RS256 (RSA Signature with SHA-256), as defined in RFC 7518 (opens in a new tab) §3.3 (JSON Web Algorithms).

RS256 is an asymmetric algorithm:

  • LuxID holds the private key and uses it to sign tokens
  • Your application uses the corresponding public key to verify signatures
  • The public key is published at the JWKS endpoint and can be fetched by anyone
  • The private key never leaves LuxID's infrastructure

This means any OAuth client - public or confidential - can verify the signature without a shared secret. It does not change the token-storage guidance elsewhere in these docs: validate and hold the ID Token server-side, not in browser JavaScript (a browser-resident token is still exposed to XSS).


The JWKS endpoint

The JSON Web Key Set (JWKS) endpoint publishes LuxID's current signing public keys:

https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID

For UAT:

https://login-uat.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID

This endpoint also appears in the discovery document:

https://login.luxid.lu/.well-known/openid-configuration

Look for the jwks_uri field.

Example JWKS response

{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "luxid-signing-key-001",
"alg": "RS256",
"n": "...",
"e": "AQAB"
},
{
"kty": "RSA",
"use": "sig",
"kid": "luxid-signing-key-002",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}

Multiple keys may be published simultaneously to support rotation overlap (see §5).

The kid header

Every ID Token JWT contains a kid (Key ID) header field. This field identifies which key from the JWKS was used to sign the token. Use it to select the correct key before verifying the signature:

import jwt # PyJWT example

# Decode the header without verifying (to extract kid)
header = jwt.get_unverified_header(id_token)
kid = header["kid"]

# Find the matching key in the cached JWKS
signing_key = next(
(key for key in jwks_keys if key["kid"] == kid),
None
)

if signing_key is None:
# Key not found - may have rotated; refresh JWKS and retry once
refresh_jwks_cache()
signing_key = next(
(key for key in jwks_keys if key["kid"] == kid),
None
)
if signing_key is None:
raise ValueError(f"Unknown signing key kid={kid}")

# Verify with the found key
claims = jwt.decode(
id_token,
signing_key,
algorithms=["RS256"],
audience="your-client-id",
issuer="https://login.luxid.lu"
)

Most OIDC libraries handle this lookup automatically when configured with the jwks_uri.


Fetching and caching JWKS

Never hardcode the public key

danger

Do not hardcode the RSA public key in your application. LuxID rotates signing keys periodically. An application that hardcodes the key will fail silently after rotation - tokens will fail signature verification with no clear error until a developer notices and updates the code.

Always fetch from the JWKS endpoint.

Cache the JWKS response

Fetching the JWKS on every token validation would be inefficient and would create an unnecessary dependency on JWKS endpoint availability during token processing. Cache the JWKS response with a reasonable TTL.

Cache TTLTrade-off
< 5 minutesVery fresh, but increases load on JWKS endpoint and adds latency on cache miss
5 - 15 minutesRecommended for most applications
15 - 60 minutesAcceptable if key-not-found refresh (see §4.3) is implemented
> 1 hourRisk of serving stale keys after rotation; only acceptable if rotation overlap is confirmed to be longer

Refresh on key-not-found

When you receive an ID Token whose kid does not match any key in your cached JWKS, do not immediately reject the token. It may have been signed with a newly rotated key that your cache has not yet seen. Implement a "refresh once" pattern:

  1. Token arrives with kid not in cache
  2. Fetch fresh JWKS from endpoint
  3. Update cache
  4. Look up kid again
  5. If still not found, reject the token as using an unknown key
  6. If found, verify normally

This pattern ensures your application handles key rotation without downtime while still protecting against tokens signed with unknown keys.

def get_signing_key(kid: str, force_refresh: bool = False) -> dict:
global jwks_cache, jwks_cache_timestamp

if force_refresh or cache_is_expired():
jwks_cache = fetch_jwks_from_endpoint()
jwks_cache_timestamp = now()

key = find_key_by_kid(jwks_cache, kid)

if key is None and not force_refresh:
# Try once more with a fresh fetch
return get_signing_key(kid, force_refresh=True)

return key # May be None if key is genuinely unknown

Cache in a shared layer for multi-instance Applications

If your application runs as multiple instances (containerised deployment, horizontal scaling), cache the JWKS in a shared layer (Redis, Memcached, or a database) rather than in each instance's memory. This prevents unnecessary concurrent JWKS fetches on every deploy or scale event.


Key rotation

How LuxID rotates keys

LuxID rotates its signing keys periodically as part of routine key management hygiene. Rotation follows an overlap strategy:

  1. A new key pair is generated
  2. The new public key is added to the JWKS response (alongside the current key)
  3. After a period during which all applications have had time to refresh their JWKS caches, tokens begin being signed with the new key
  4. The old key remains in the JWKS response for a further period to allow any tokens signed with it to be validated during their remaining lifetime
  5. The old key is eventually removed from the JWKS response

The overlap means that any application implementing the key-not-found refresh pattern (§4.3) will handle rotation transparently. Applications that hardcode the key, or that cache without a key-not-found refresh, will break.

What Applications must do

  • Implement JWKS caching with TTL (§4.2)
  • Implement key-not-found refresh (§4.3)
  • Never hardcode keys

No application-side configuration change is required for normal key rotation. The rotation is fully transparent if the above patterns are implemented.

Emergency rotation

In the event of a key compromise, LuxID may perform an emergency rotation where the old key is removed immediately without an overlap period. In this scenario:

  • All tokens signed with the compromised key are invalidated immediately
  • Affected users will need to re-authenticate
  • Applications with a key-not-found refresh will quickly adapt
  • Applications that do not refresh on key-not-found will reject all new tokens until their cache expires

Monitor your application's token validation error rates. A sudden spike in signature verification failures is a signal that an emergency rotation may have occurred. Reduce your JWKS TTL or implement cache invalidation hooks for high-availability requirements.


Client authentication - keeping your client secret secure

In addition to LuxID's signing keys (used to verify ID Tokens), your application may have its own credentials for authenticating to LuxID's token endpoint. These are distinct from LuxID's signing keys.

Client secret

Confidential clients authenticate to LuxID's token endpoint using a Client Secret - a high-entropy random value provisioned when you register your application. See Protect your Application §9 for storage and rotation guidance.

Rotation is performed by request to LuxID. See Client credentials for the field reference.


Summary

TopicGuidance
Signing algorithmRS256
JWKS endpoint (production)https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID
JWKS caching TTL5 - 60 minutes, with key-not-found refresh
Hardcoded public keysNever - always fetch from JWKS
Key rotation handlingImplement key-not-found refresh; no manual action required for normal rotation
Client secret rotationBy request to LuxID; see Client Credentials page
Client Secret storageSecrets manager or environment variable injection; never in source code

Further reading

Updated 2026-05-26