Token introspection
What token introspection does
Token introspection lets a resource server determine whether a token is currently active and retrieve its metadata, without validating a JWT signature locally.
The LuxID introspection endpoint implements RFC 7662 - OAuth 2.0 Token Introspection (opens in a new tab).
Endpoint:
https://login.luxid.lu/mga/sps/oauth/oauth20/introspect
Method: POST
Although the discovery document advertises an introspection_endpoint, token introspection is not supported for Partners today. Testing against UAT returned an HTML error page (HTTP 400) instead of an RFC 7662 JSON response. Do not build against it.
LuxID issues JWT access and ID tokens, so validate tokens locally instead: verify the RS256 signature against the JWKS endpoint (opens in a new tab) and check the exp, iss, and aud claims. See Token validation issues. The remainder of this page is retained as reference; if you have a genuine need for server-side introspection, contact LuxID.
Client authentication is required - an unauthenticated introspection request is rejected with 401 Unauthorized. See Client authentication below.
When to use introspection
Introspection is useful when:
- Your resource server receives an opaque access token (a token that is not a self-contained JWT and cannot be validated locally).
- You need to verify a refresh token before attempting a silent renewal - confirming it has not been revoked before making a call to the Token endpoint.
- You run a polyglot environment where implementing full JWT validation in every language/service is not practical.
- You need the current metadata of a token (remaining TTL, scope, subject) in addition to its validity.
Introspection is a network hop - it adds latency and creates a dependency on the LuxID server for every token check. If your access tokens are JWTs, prefer local validation using the JWKS endpoint for the common path; reserve introspection for cases where local validation is insufficient (for example, checking whether a token has been explicitly revoked).
See Advanced security options for guidance on combining local JWT validation with selective introspection.
Client authentication
All introspection requests must be authenticated by the calling application. LuxID supports the following client authentication methods:
| Method | Description |
|---|---|
client_secret_basic | HTTP Basic Auth: Authorization: Basic base64(client_id:client_secret) |
client_secret_post | Include client_id and client_secret in the POST body |
Use the authentication method configured for your application in the LuxID Console.
Request
POST /mga/sps/oauth/oauth20/introspect HTTP/1.1
Host: login.luxid.lu
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=<access_or_refresh_token>&token_type_hint=access_token
Request parameters
| Parameter | Required | Description |
|---|---|---|
token | Yes | The token to introspect (access token or refresh token) |
token_type_hint | No | access_token or refresh_token - helps the server look up the token faster; ignored if incorrect |
cURL examples
Using HTTP basic auth
curl -s \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "your_client_id:your_client_secret" \
-d "token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...&token_type_hint=access_token" \
https://login.luxid.lu/mga/sps/oauth/oauth20/introspect
Using client_secret_post
curl -s \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d "token_type_hint=access_token" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret" \
https://login.luxid.lu/mga/sps/oauth/oauth20/introspect
Introspecting a Refresh Token
curl -s \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "your_client_id:your_client_secret" \
-d "token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...&token_type_hint=refresh_token" \
https://login.luxid.lu/mga/sps/oauth/oauth20/introspect
Response - active token
{
"active": true,
"scope": "openid profile email",
"client_id": "your_client_id",
"username": "jean.dupont@example.lu",
"token_type": "Bearer",
"exp": 1716378000,
"iat": 1716374400,
"nbf": 1716374400,
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"aud": "your_client_id",
"iss": "https://login.luxid.lu",
"jti": "7f3e9b1d-2a4c-4d5e-8f6a-1b2c3d4e5f6a"
}
Response fields
| Field | Type | Description |
|---|---|---|
active | boolean | true if the token is valid and has not expired or been revoked |
scope | string | Space-separated list of scopes associated with the token |
client_id | string | The application that was issued the token |
username | string | Human-readable identifier for the user (email address) |
token_type | string | Bearer for access tokens |
exp | integer | Unix timestamp at which the token expires |
iat | integer | Unix timestamp at which the token was issued |
nbf | integer | Unix timestamp before which the token is not valid |
sub | string | Subject - the user's stable LuxID identifier |
aud | string | Audience - the intended recipient of the token |
iss | string | Issuer - https://login.luxid.lu |
jti | string | JWT ID - unique identifier for this token |
Response - inactive token
{
"active": false
}
An active: false response means the token is expired, revoked, or was never valid. No other fields are returned for inactive tokens - per RFC 7662 §2.2, implementations MUST NOT return additional metadata for inactive tokens to avoid leaking information.
Do not distinguish between "expired" and "revoked" in your application logic - treat both as "token not usable" and respond accordingly.
Error responses
401 unauthorized - client authentication failed
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
Resolution: verify your client_id and client_secret are correct and match what is configured in the LuxID Console.
400 bad request - missing token parameter
{
"error": "invalid_request",
"error_description": "The request is missing the required parameter: token"
}
Resolution: include the token parameter in the POST body.
Performance and caching
Introspection adds a round-trip to the LuxID server on every call. For high-throughput resource servers, this can become a bottleneck. Mitigate with short-lived caching:
- Cache key: the token string itself (or a hash of it).
- Cache TTL: the lesser of your chosen cache duration and the token's remaining lifetime (
exp - now). A 30-second TTL is a reasonable starting point for access tokens; adjust based on your acceptable staleness. - Invalidation: do not serve cached
active: trueresponses after the token'sexptimestamp. Always checkexpbefore returning a cached result. - Negative caching: cache
active: falseresponses for a short period (for example, 10 seconds) to avoid hammering the endpoint with invalid tokens.
The introspection response is specific to the token and to the calling client.
Caching example
- Python
- Java
- PHP
- Node.js
- C#
def introspect_with_cache(token, cache, luxid_client):
cache_key = "introspect:" + sha256(token)
cached = cache.get(cache_key)
if cached is not None:
# Check that the cached result has not expired
if cached["active"] and cached["exp"] > time.now():
return cached
elif not cached["active"]:
return cached # Negative cache hit
result = luxid_client.introspect(token)
ttl = min(30, max(0, result.get("exp", 0) - time.now()))
cache.set(cache_key, result, ttl=ttl)
return result
Map<String, Object> introspectWithCache(String token, Cache cache, LuxIdClient luxid) {
String cacheKey = "introspect:" + sha256(token);
Map<String, Object> cached = cache.get(cacheKey);
if (cached != null) {
boolean active = (Boolean) cached.get("active");
long now = Instant.now().getEpochSecond();
if (active && (Long) cached.get("exp") > now) {
return cached; // valid cache hit
} else if (!active) {
return cached; // negative cache hit
}
}
Map<String, Object> result = luxid.introspect(token);
long exp = ((Number) result.getOrDefault("exp", 0L)).longValue();
long ttl = Math.min(30, Math.max(0, exp - Instant.now().getEpochSecond()));
cache.set(cacheKey, result, ttl);
return result;
}
function introspectWithCache(string $token, Cache $cache, LuxIdClient $luxid): array {
$cacheKey = 'introspect:' . hash('sha256', $token);
$cached = $cache->get($cacheKey);
if ($cached !== null) {
if ($cached['active'] && $cached['exp'] > time()) {
return $cached; // valid cache hit
}
if (!$cached['active']) {
return $cached; // negative cache hit
}
}
$result = $luxid->introspect($token);
$ttl = min(30, max(0, ($result['exp'] ?? 0) - time()));
$cache->set($cacheKey, $result, $ttl);
return $result;
}
async function introspectWithCache(token, cache, luxid) {
const cacheKey = 'introspect:' + sha256(token);
const cached = await cache.get(cacheKey);
if (cached) {
const now = Math.floor(Date.now() / 1000);
if (cached.active && cached.exp > now) {
return cached; // valid cache hit
}
if (!cached.active) {
return cached; // negative cache hit
}
}
const result = await luxid.introspect(token);
const now = Math.floor(Date.now() / 1000);
const ttl = Math.min(30, Math.max(0, (result.exp ?? 0) - now));
await cache.set(cacheKey, result, ttl);
return result;
}
async Task<IntrospectionResult> IntrospectWithCacheAsync(
string token, ICache cache, LuxIdClient luxid)
{
var cacheKey = "introspect:" + Sha256(token);
var cached = await cache.GetAsync(cacheKey);
if (cached is not null)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (cached.Active && cached.Exp > now) return cached; // valid cache hit
if (!cached.Active) return cached; // negative cache hit
}
var result = await luxid.IntrospectAsync(token);
var ttl = Math.Min(30, Math.Max(0, result.Exp - DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
await cache.SetAsync(cacheKey, result, TimeSpan.FromSeconds(ttl));
return result;
}
Resource server integration pattern
A typical resource server validates an incoming Bearer token as follows:
Introspection vs local JWT validation
| Local JWT validation | Introspection | |
|---|---|---|
| Network call | No - validates locally using cached JWKS | Yes - calls LuxID server |
| Revocation awareness | No - cannot detect explicit revocation | Yes - reflects current state |
| Latency | Low | Higher (network round-trip) |
| Best for | Common path in high-throughput APIs | Revocation checks, opaque tokens |
Validate the JWT signature locally (cache the JWKS with a reasonable TTL). Use introspection only when you need to confirm the token has not been explicitly revoked, or when the token is opaque.
Related pages
- Tokens and claims - token structure and claims reference
- OAuth 2.0 for APIs - resource server integration
- Revocation - explicitly invalidating tokens
- Advanced security options - PAR, JAR, FAPI alignment
- Client credentials - managing application credentials
- OAuth and OIDC error codes - error reference