Skip to main content

Introspection for AI Requests

Token introspection allows your MCP servers and AI agents to validate Guardhouse access tokens in real-time without requiring JWT signature validation. This is essential for real-time security and access control.

What is Token Introspection?

Token introspection is an OAuth 2.0 extension (RFC 7662) that provides a standardized endpoint for validating tokens and retrieving metadata. Instead of validating JWT signatures locally, you make a request to Guardhouse to ask "Is this token still valid?"

When to Use Introspection

Use token introspection when:

  • Real-time Security - Check if tokens are revoked immediately
  • Access Control - Enforce dynamic permissions changes
  • Multi-tenant Isolation - Validate tokens across tenants
  • Debugging - Inspect token metadata and scopes
  • Compliance - Meet security audit requirements

When NOT to Use Introspection

Use JWT signature validation instead when:

  • High-performance API - No network overhead for each request
  • Offline Validation - Works without network connectivity
  • Simpler Clients - Don't need introspection client credentials
  • Public Key Caching - JWKS caching works locally

Introspection vs JWT Signature Validation

AspectJWT SignatureIntrospection
SpeedFast (local crypto)Slower (network call)
Network DependencyNoneRequired
Client ComplexitySimpleModerate
Real-time RevocationNoYes
Resource UsageMinimalHigher
DebuggingHarderEasier
Multi-tenantSimpleComplex
Best Use CasePublic APIsMCP/AI Agents

How It Works

Introspection Flow

┌──────────────────────────────────────────┐
│ Your Application │
│ (Client, Service, Agent) │
│ │
└────────────┬───────────────────────┘


┌─────────────────────────────────────┐
│ Guardhouse Introspection │
│ (Validation Service) │
└────────────┬───────────────────────┘


┌─────────────────────────────────────┐
│ Response (Active/Inactive) │
└──────────────────────────────────────┘



Your Application checks response

Introspection Endpoint

Request

Endpoint: POST /oauth/introspect

Authentication: HTTP Basic with Client ID and Client Secret

Content-Type: application/x-www-form-urlencoded

Request Body:

token={token_to_validate}
&token_type_hint=access_token

Headers:

Authorization: Basic {base64(client_id:client_secret)}
Content-Type: application/x-www-form-urlencoded

Success Response (Active Token)

{
"active": true,
"client_id": "YOUR_CLIENT_ID",
"sub": "user_123456789",
"aud": [
"https://your_tenant.guardhouse.cloud/api/v2",
"mcp:read:documents",
"mcp:write:documents",
"mcp:execute:tools"
],
"scope": "openid profile mcp:read:documents mcp:write:documents mcp:execute:tools",
"iss": "https://your_tenant.guardhouse.cloud",
"exp": 1641000000,
"iat": 1640996400,
"nbf": 1640996400,
"jti": "unique-token-id"
}

Inactive Token Response

{
"active": false,
"client_id": "YOUR_CLIENT_ID"
}

Response Fields

FieldTypeDescription
activebooleanWhether the token is currently valid
client_idstringClient ID that issued the token
substringSubject (user/agent ID)
audstring[]Array of valid audiences for this token
scopestringSpace-separated list of granted scopes
issstringToken issuer (Guardhouse domain)
expintegerToken expiration time (Unix timestamp)
iatintegerToken issued at time (Unix timestamp)
nbfintegerToken not valid before time (Unix timestamp)
jtistringUnique token identifier (for revocation)

Implementation Examples

Node.js Example

const https = require('https');

async function introspectToken(token, clientId, clientSecret) {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

const response = await https.request(
'https://your_tenant.guardhouse.cloud/oauth/introspect',
{
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token,
token_type_hint: 'access_token'
})
}
);

return await response.json();
}

// Usage
const isActive = await introspectToken(
process.env.GUARDHOUSE_CLIENT_ID,
process.env.GUARDHOUSE_CLIENT_SECRET,
process.env.GUARDHOUSE_TOKEN
);

if (isActive.active) {
console.log('Token is active for user:', isActive.sub);
console.log('MCP scopes:', isActive.scope.split(' ').filter(s => s.startsWith('mcp:')));
} else {
console.log('Token is inactive or expired');
}

Python Example

import requests
import base64

def introspect_token(token, client_id, client_secret):
"""Introspect a Guardhouse access token"""

# Prepare Basic auth credentials
credentials = base64.b64encode(f"{client_id}:{client_secret}").decode('ascii')

# Make introspection request
response = requests.post(
'https://your_tenant.guardhouse.cloud/oauth/introspect',
auth=(client_id, client_secret),
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/x-www-form-urlencoded'
},
data={
'token': token,
'token_type_hint': 'access_token'
}
)

result = response.json()

# Check if token is active
if result.get('active', False):
print(f"Token is inactive or expired")
return False

# Check if user has MCP scopes
scope = result.get('scope', '')
mcp_scopes = [s for s in scope.split(' ') if s.startswith('mcp:')]

if mcp_scopes:
print(f"Token is active for user: {result.get('sub')}")
print(f"MCP scopes: {', '.join(mcp_scopes)}")
return True
else:
print(f"User has no MCP scopes")
return True

# Usage
import os
from dotenv import load_dotenv

load_dotenv()

token = os.getenv('GUARDHOUSE_TOKEN')
is_valid = introspect_token(
os.getenv('GUARDHOUSE_CLIENT_ID'),
os.getenv('GUARDHOUSE_CLIENT_SECRET'),
token
)

if is_valid:
print("Token is valid")
else:
print("Token is invalid")

.NET Example

using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

public class TokenIntrospectionService
{
private readonly HttpClient _httpClient;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _introspectionUrl;

public TokenIntrospectionService(string clientId, string clientSecret, string authority)
{
_httpClient = new HttpClient();
_clientId = clientId;
_clientSecret = clientSecret;
_introspectionUrl = $"{authority}/oauth/introspect";
}

public async Task<IntrospectionResponse> IntrospectAsync(string token)
{
// Create Basic auth header
var authValue = Convert.ToBase64String(
$"{_clientId}:{_clientSecret}",
Encoding.ASCII
);

var request = new HttpRequestMessage(
HttpMethod.Post,
new Uri(_introspectionUrl),
{
Headers = {
{ HttpRequestHeader.Authorization, $"Bearer {token}" },
{ HttpRequestHeader.ContentType, "application/x-www-form-urlencoded" }
},
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "token", token },
{ "token_type_hint", "access_token" }
})
}
);

var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();

return JsonSerializer.Deserialize<IntrospectionResponse>(responseBody);
}
}

public class IntrospectionResponse
{
public bool Active { get; set; }
public string ClientId { get; set; }
public string Subject { get; set; }
public string[] Audiences { get; set; }
public string Scope { get; set; }
public string Issuer { get; set; }
public long Exp { get; set; }
public long Iat { get; set; }
public string Jti { get; set; }
}

// Usage
var service = new TokenIntrospectionService(
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
authority: "https://your_tenant.guardhouse.cloud"
);

var response = await service.IntrospectAsync(accessToken);

if (response.Active)
{
Console.WriteLine($"Token is active for: {response.Subject}");
Console.WriteLine($"MCP scopes: {string.Join(", ", response.Scope.Split(' ').Where(s => s.StartsWith("mcp:")))}");
}

FastAPI (Python) Middleware

from fastapi import FastAPI, HTTPException, Depends, Request
import requests
import base64
import os
from dotenv import load_dotenv

load_dotenv()

app = FastAPI()

def verify_introspection_client(client_id: str, client_secret: str) -> str:
"""Verify introspection client credentials"""
expected_client_id = os.getenv('INTROSPECTION_CLIENT_ID')
expected_client_secret = os.getenv('INTROSPECTION_CLIENT_SECRET')

if client_id != expected_client_id or client_secret != expected_client_secret:
raise HTTPException(status_code=401, detail="Invalid introspection client")

return client_id

@app.middleware("http")
async def validate_introspection_client(request: Request):
"""Middleware to validate introspection client credentials"""

auth_header = request.headers.get('authorization')
if not auth_header or not auth_header.startswith('Basic '):
raise HTTPException(status_code=401, detail="Missing Basic auth header")

try:
decoded = base64.b64decode(auth_header.replace('Basic ', ''))
client_id, client_secret = decoded.decode('ascii').split(':', 1)

if not verify_introspection_client(client_id, client_secret):
raise HTTPException(status_code=401, detail="Invalid introspection client credentials")
except Exception:
raise HTTPException(status_code=401, detail="Invalid Basic auth header")

@app.post("/introspect")
async def introspect_token(request: TokenIntrospectionRequest):
"""Introspection endpoint"""

# Validate introspection client
auth_header = request.headers.get('authorization')
if not auth_header or not auth_header.startswith('Basic '):
raise HTTPException(status_code=401, detail="Missing Basic auth header")

try:
decoded = base64.b64decode(auth_header.replace('Basic ', ''))
client_id, client_secret = decoded.decode('ascii').split(':', 1)
verify_introspection_client(client_id, client_secret)
except Exception:
raise HTTPException(status_code=401, detail="Invalid Basic auth header")

# Make introspection request to Guardhouse
introspection_response = requests.post(
'https://your_tenant.guardhouse.cloud/oauth/introspect',
auth=(client_id, client_secret),
headers={
'Content-Type': 'application/x-www-form-urlencoded'
},
data={
'token': request.token,
'token_type_hint': 'access_token'
}
)

return introspection_response.json()

class TokenIntrospectionRequest(BaseModel):
token: str

MCP-Specific Introspection

When validating tokens for AI agents, check for MCP-specific scopes:

async function validateForMCP(token) {
const response = await introspectToken(token);

if (!response.active) {
throw new Error('Token is inactive or expired');
}

// Check if token has MCP scopes
const mcpScopes = response.scope.split(' ').filter(s => s.startsWith('mcp:'));

if (mcpScopes.length === 0) {
throw new Error('Token does not have MCP scopes');
}

// Log relevant scopes
console.log('MCP Read:', response.scope.includes('mcp:read:documents'));
console.log('MCP Write:', response.scope.includes('mcp:write:documents'));
console.log('MCP Execute:', response.scope.includes('mcp:execute:tools'));

return {
authorized: true,
scopes: mcpScopes,
user: response.sub
};
}

Token Revocation

For immediate token revocation without relying on expiration:

async function revokeToken(token) {
const response = await fetch('https://your_tenant.guardhouse.cloud/oauth/revoke', {
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token,
token_type_hint: 'refresh_token'
})
});

// Immediately verify token is revoked
const checkResponse = await introspectToken(token);

if (checkResponse.active) {
throw new Error('Token revocation failed');
}

console.log('Token revoked successfully');
}

Caching Introspection Results

Implement caching to reduce Guardhouse API calls:

class IntrospectionCache {
constructor(ttl = 5000) { // 5 seconds TTL
this.cache = new Map();
}

async introspect(token, clientId, clientSecret) {
const cacheKey = `${clientId}:${token}`;

// Check cache
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);

// Check if expired
if (Date.now() < cached.timestamp + this.ttl) {
return cached.data;
}
}

// Make API call
const result = await this.introspectAPI(token, clientId, clientSecret);

// Cache result
this.cache.set(cacheKey, {
data: result,
timestamp: Date.now()
});

return result;
}

async introspectAPI(token, clientId, clientSecret) {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

const response = await fetch('https://your_tenant.guardhouse.cloud/oauth/introspect', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token,
token_type_hint: 'access_token'
})
});

return await response.json();
}
}

// Usage
const cache = new IntrospectionCache();

// First call: Goes to API
const result1 = await cache.introspect(token, clientId, clientSecret);

// Second call: Returns from cache
const result2 = await cache.introspect(token, clientId, clientSecret);

Best Practices

1. Security

  • Always Use HTTPS - Never send credentials over HTTP
  • Validate Client Credentials - Store and transmit securely
  • Implement Caching - Reduce API calls and protect against rate limits
  • Use Appropriate Auth - Use Basic auth for introspection clients

2. Performance

  • Cache Introspection Results - Implement TTL-based caching (5-30 seconds)
  • Batch Requests - Validate multiple tokens in one call if Guardhouse supports it
  • Connection Pooling - Reuse HTTP connections
  • Lazy Loading - Load introspection only when needed

3. Error Handling

  • Handle Inactive Tokens - Revoke or request new ones
  • Handle Network Errors - Implement retry logic with exponential backoff
  • Log All Introspection Calls - Track token validation for audit trail
  • Monitor Rate Limits - Implement caching to stay within limits

4. AI Agent Specific

  • Validate MCP Scopes - Check for mcp:* scopes before granting access
  • Log Agent Activity - Track which agents are accessing resources
  • Implement Graceful Degradation - Handle introspection service failures
  • Use Separate Identities - Consider per-agent vs shared identities
  • Context-Aware Validation - Validate based on requested operation

Common Patterns

MCP Server Token Validation

// MCP server validates all incoming requests
const express = require('express');

app.use(async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');

if (!token) {
return res.status(401).json({ error: 'Missing token' });
}

// Introspect with Guardhouse
const isValid = await introspectToken(token);

if (isValid.active && isValid.scope?.includes('mcp:read:documents')) {
// User has MCP read permission - proceed
req.user = { id: isValid.sub, scopes: isValid.scope };
return next();
}

if (isValid.active && isValid.scope?.includes('mcp:write:documents')) {
// User has MCP write permission - proceed
req.user = { id: isValid.sub, scopes: isValid.scope };
return next();
}

// Token is invalid or lacks required scopes
req.user = null;
return res.status(403).json({ error: 'Unauthorized' });
});

AI Agent Token Management

// AI agent manages its own tokens
class AIAgent {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = null;
this.expiresAt = null;
}

async authenticate() {
// Get access token using client credentials
const response = await fetch('https://your_tenant.guardhouse.cloud/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
audience: 'https://api.yourcompany.com/mcp'
})
});

this.accessToken = response.data.access_token;
this.expiresAt = Date.now() + (response.data.expires_in * 1000);

return this.accessToken;
}

async callMCP(resource, operation, parameters) {
// Check if token needs refresh
if (!this.accessToken || Date.now() >= this.expiresAt) {
await this.authenticate();
}

// Make API call to your MCP server
const response = await fetch(`https://api.yourcompany.com/mcp/${resource}`, {
method: operation,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(parameters)
});

return response.json();
}

async validateToken() {
const response = await this.introspectWithGuardhouse();

if (!response.active) {
throw new Error('Token is inactive or expired');
}

return response;
}
}

Troubleshooting

Common Issues

Issue: "401 Unauthorized"

  • Verify Client ID and Secret are correct
  • Check Basic auth encoding
  • Ensure credentials match Guardhouse records

Issue: "Token is inactive"

  • Token has been revoked
  • Token has expired
  • User has been disabled
  • Check with issuer if appropriate

Issue: "No MCP scopes"

  • Token doesn't have required MCP scopes
  • Check client configuration in Guardhouse
  • Ensure correct audience is being used

Issue: "Rate limited"

  • Implement caching to reduce API calls
  • Add proper backoff logic
  • Check Guardhouse rate limits for your plan

Support

For issues, questions, or contributions: