Token debugger
Overview
A LuxID web tool for decoding ID Tokens and Access Tokens, validating their signature against the LuxID JWKS, and surfacing expiry, audience, issuer,
acr, andamrvalues.
When integrating with LuxID, you will frequently need to inspect tokens to understand what claims are present, whether the signature is valid, and whether the token is still within its validity window. The LuxID Token Debugger is a dedicated web tool for this purpose.
The Token Debugger is available at: https://tools.luxid.lu/token-debugger
It is intended for use with UAT tokens and development tokens only. See the Security warning section before pasting any token.
What you can do with the token debugger
Paste a token or full token response
The tool accepts:
- A raw ID Token (a JWT string beginning with
eyJ...). - A raw Access Token (also a JWT in the LuxID implementation).
- A full token response JSON - paste the entire body returned by the token endpoint, and the tool will extract and analyse both the
id_tokenandaccess_tokenfields automatically.
Decode header and payload
The tool decodes and formats both the JWT header and payload sections as pretty-printed JSON:
Header example:
{
"alg": "RS256",
"typ": "JWT",
"kid": "luxid-signing-key-2024-01"
}
Payload example:
{
"iss": "https://login.luxid.lu",
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"aud": "your-client-id",
"exp": 1748000000,
"iat": 1747996400,
"auth_time": 1747996395,
"nonce": "abc123",
"acr": "urn:luxid:acr:level:substantial",
"amr": ["otp"],
"email": "testuser@example.com",
"email_verified": true,
"given_name": "Jean",
"family_name": "Dupont"
}
Validate the signature
The tool fetches the current JWKS from the LuxID JWKS endpoint for the relevant environment (production or UAT, detected from the iss claim) and verifies the token signature against the matching key by kid.
The result is displayed as:
- Signature valid - the token was issued and signed by LuxID.
- Signature invalid - the token has been tampered with, was signed by a different key, or is malformed.
- Key not found - the
kidin the token header does not match any key currently in the JWKS (this can happen if the token was issued before a key rotation and the old key has since been retired).
Expiry and timing checks
The tool displays the following in colour-coded form:
| Field | Green | Red |
|---|---|---|
exp (expiry) | Token is still valid | Token has expired |
iat (issued at) | Within expected window | Issued in the future (clock skew issue) |
nbf (not before, if present) | Current time is past nbf | Token not yet valid |
auth_time | Present and within session window | Missing or unreasonably old |
Audience and issuer checks
iss: compared against the expected LuxID issuer for the detected environment. A mismatch is flagged in red.aud: displayed and highlighted. You must manually verify this matches your Client ID - the tool cannot know your application's registered Client ID.
If the aud contains an unexpected value (for example, aud is an array containing a third-party client ID you do not recognise), this should be treated as a serious security concern - see Protect your Application.
acr and amr human descriptions
LuxID tokens carry machine-readable values for the Authentication Context Class Reference (acr) and Authentication Methods References (amr). The tool maps these to human-readable descriptions:
acr value | Description |
|---|---|
urn:luxid:acr:level:low | Low assurance - password only (auth_level 2) |
urn:luxid:acr:level:substantial | Substantial assurance - password + second factor or passkey (auth_level 3, 4, or 8) |
urn:luxid:acr:level:high | High assurance - LuxTrust (auth_level 9) |
amr value | Description |
|---|---|
pwd | Password authentication |
otp | One-Time Code via SMS or voice call |
totp | Authenticator app (TOTP) |
fido | Passkey (WebAuthn/FIDO2) |
luxtrust | LuxTrust mobile app or smartcard |
For a full description of assurance levels, see Multi-factor authentication.
Screenshot
The Token Debugger showing a decoded ID Token with signature validation, expiry status, and acr/amr descriptions.
Security warning
Never paste production tokens from real users into any online tool.
Production tokens contain personal data (email, given_name, family_name, sub, and potentially phone_number or other verified attributes). Pasting them into a web-based tool - including the LuxID Token Debugger - exposes that data to the tool's infrastructure, which may conflict with your data processing obligations under GDPR.
Follow this rule:
- Use the LuxID Token Debugger only with UAT tokens generated from test user accounts.
- For any analysis of production tokens, use offline tools running entirely within your own infrastructure (see Offline alternatives below).
- If you need to share a token for debugging with the LuxID support team, use a UAT token or redact the payload before sharing.
Offline alternatives
If you prefer to work offline, or you need to inspect production tokens without sending them to an external service, the following approaches are recommended.
Using jwt.io (UAT and development tokens only)
jwt.io (opens in a new tab) is a widely used JWT inspector. Paste a token to decode the header and payload. To verify the signature:
- Open the LuxID JWKS endpoint for UAT:
https://login-uat.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID
- Find the key whose
kidmatches thekidin your token header. - Copy the
x5corn/evalues and paste them as the public key in jwt.io.
The signature verification will confirm whether the token is authentic.
Same rule applies: only use jwt.io with UAT or development tokens, not production tokens from real users.
Inspecting a token programmatically
Validate the signature, issuer, audience and expiry in one call, entirely within your own infrastructure. Only run this against UAT or development tokens, not production tokens from real users.
- Python
- Java
- PHP
- Node.js
- C#
import jwt
import requests
def get_jwks(env='production'):
base = 'https://login.luxid.lu' if env == 'production' else 'https://login-uat.luxid.lu'
resp = requests.get(f"{base}/mga/sps/oauth/oauth20/jwks/OIDC-LUXID")
resp.raise_for_status()
return jwt.PyJWKClient(f"{base}/mga/sps/oauth/oauth20/jwks/OIDC-LUXID")
def inspect_token(id_token, client_id, env='production'):
issuer = f"https://login{'-uat' if env == 'uat' else ''}.luxid.lu"
jwks_client = get_jwks(env)
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
payload = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"],
audience=client_id,
issuer=issuer,
)
return payload
JWKSource<SecurityContext> keys = JWKSourceBuilder
.create(new URL("https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID")).build();
ConfigurableJWTProcessor<SecurityContext> proc = new DefaultJWTProcessor<>();
proc.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keys));
proc.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>(
new JWTClaimsSet.Builder()
.issuer("https://login.luxid.lu")
.audience(clientId)
.build(),
Set.of("sub", "iat", "exp")));
JWTClaimsSet payload = proc.process(idToken, null);
System.out.println("Payload: " + payload.toJSONObject());
System.out.println("ACR: " + payload.getStringClaim("acr"));
System.out.println("AMR: " + payload.getStringListClaim("amr"));
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
$jwks = json_decode(file_get_contents(
'https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID'), true);
JWT::$leeway = 60;
$payload = JWT::decode($idToken, JWK::parseKeySet($jwks)); // verifies the RS256 signature
// firebase/php-jwt does not check iss / aud - do it explicitly
if ($payload->iss !== 'https://login.luxid.lu') throw new RuntimeException('Bad issuer');
if ($payload->aud !== $clientId) throw new RuntimeException('Bad audience');
print_r($payload);
echo 'ACR: ' . $payload->acr . PHP_EOL;
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID')
);
async function inspectToken(idToken) {
const { payload, protectedHeader } = await jwtVerify(idToken, JWKS, {
issuer: 'https://login.luxid.lu',
audience: process.env.LUXID_CLIENT_ID,
});
console.log('Header:', protectedHeader);
console.log('Payload:', payload);
console.log('Expires:', new Date(payload.exp * 1000).toISOString());
console.log('ACR:', payload.acr);
console.log('AMR:', payload.amr);
}
var config = await new ConfigurationManager<OpenIdConnectConfiguration>(
"https://login.luxid.lu/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever()).GetConfigurationAsync();
var result = new JsonWebTokenHandler().ValidateToken(idToken,
new TokenValidationParameters
{
ValidIssuer = "https://login.luxid.lu",
ValidAudience = clientId,
ValidAlgorithms = new[] { "RS256" },
IssuerSigningKeys = config.SigningKeys,
});
if (!result.IsValid) throw result.Exception;
var jwt = (JsonWebToken)result.SecurityToken;
Console.WriteLine($"ACR: {jwt.GetClaim("acr").Value}");
Using cURL and manual inspection
For quick payload inspection without signature verification:
# Extract and decode the payload section of a JWT
ID_TOKEN="eyJ..."
# Split on '.' and base64-decode the second segment
echo "$ID_TOKEN" | cut -d '.' -f2 | base64 -d 2>/dev/null | jq .
This shows the payload but does not verify the signature. Use it only for quick development checks, not for any security-sensitive decision.
Common issues
"Key not found" after key rotation
LuxID rotates signing keys periodically. If your application caches the JWKS aggressively, a token signed with the new key will fail validation until your cache is refreshed.
Best practice: cache the JWKS with a short TTL (5-15 minutes) and re-fetch when a kid is not found in the cached keyset. Most JOSE libraries handle this automatically.
See Token validation issues for a full troubleshooting guide.
Token appears valid but claims are missing
If expected claims (for example, email or phone_number) are absent from the payload, the most likely causes are:
- The corresponding scope was not requested in the authorisation request.
- The claim is not configured in the Claim Template for your application.
- The user has not granted consent for that claim.
Use the Attribute preview tool to diagnose the exact cause before changing code.
aud does not match your client ID
If the aud value in the token is not your Client ID, you must reject the token. This can happen if:
- You are accidentally using the wrong token (for example, a token from a different application's flow).
- A token is being replayed from a different application.
Never accept a token whose aud does not match your registered Client ID.