Only 8.5% of MCP servers use OAuth. For those that do, every token lifecycle event becomes a verifiable witness anchor. For those that do not, this is why they should.
Who this is for: MCP server implementers, platform security engineers, and compliance teams responsible for OAuth integration in AI tool ecosystems. Assumes familiarity with OAuth 2.0 flows and the Model Context Protocol.
The MCP OAuth crisis: Only 8.5% of MCP servers implement OAuth authentication. Over 30 CVEs were disclosed against MCP implementations in 60 days. The confused deputy problem (OWASP MCP-07) enables one-click account takeover through malicious proxy servers. SWT3 does not fix these vulnerabilities. It witnesses the authentication lifecycle so that when things go wrong, the evidence trail is already there.
The Model Context Protocol enables AI assistants to call external tools. These tools access databases, APIs, file systems, and cloud services on behalf of the user. Authentication is the foundation of this access. When it fails, the consequences cascade.
The current state of MCP authentication:
SWT3 does not prevent these attacks. Prevention is the responsibility of the OAuth implementation itself (audience validation, token binding, DPoP). What SWT3 does is produce an independent witness record of every OAuth lifecycle event, creating forensic evidence that persists regardless of whether the attack is detected in real time.
The OAuth lifecycle in MCP has eight distinct events. Each event produces an AI-MCP.5 witness anchor with a specific event_type code.
| Event Type | Code | What Is Attested | Why It Matters |
|---|---|---|---|
| Discovery | oauth_discovery |
Authorization server metadata URL, issuer, supported flows | Proves which authorization server was selected and that discovery was not intercepted |
| Authorization Request | oauth_authz_request |
Scopes requested, redirect URI, PKCE code challenge | Records the exact permissions requested before user consent |
| Token Issuance | oauth_token_issued |
Token hash (never the token itself), scopes granted, expiry, audience | Proves what access was actually granted versus what was requested |
| Token Refresh | oauth_token_refresh |
New token hash, previous token hash, scopes unchanged/changed | Detects scope creep during refresh cycles |
| Token Use | oauth_token_use |
Resource accessed, scope consumed, audience validated | Links each API call to the token that authorized it |
| Audience Validation | oauth_audience_check |
Expected audience, actual audience, match result | The critical confused deputy defense. Proves the token was validated against the intended service. |
| Scope Reduction | oauth_scope_reduce |
Original scopes, reduced scopes, reason for reduction | Evidence of least-privilege enforcement when the server narrows granted scopes |
| Revocation | oauth_revoke |
Token hash revoked, revocation reason, initiator | Proves the token was explicitly invalidated and by whom |
Important: SWT3 never records raw tokens, client secrets, or authorization codes. All token references are SHA-256 hashes. The witness layer records metadata about the OAuth lifecycle, not the credentials themselves.
| Procedure | Name | Factor A | Factor B | Factor C |
|---|---|---|---|---|
AI-MCP.5 |
OAuth Token Binding | Event type code and MCP server identifier | Token hash (SHA-256), scopes (comma-separated list), audience | Scope count, validation result (pass/fail), expiry epoch |
All eight event types use the same procedure (AI-MCP.5) with the event type encoded in Factor A. This keeps the procedure namespace clean while allowing fine-grained event differentiation in the ledger.
The fingerprint is computed as SHA256("WITNESS:{tenant}:{proc}:{fa}:{fb}:{fc}:{ts_ms}").hex()[:12]. This formula is locked across all seven SDK languages and produces identical fingerprints for identical inputs.
The confused deputy attack is the most dangerous OAuth vulnerability in MCP. Here is how it works and where witnessing provides evidence.
Witnessing does not block the confused deputy attack. Token audience validation (RFC 9068), DPoP (RFC 9449), and sender-constrained tokens are the preventive controls. What witnessing provides is an independent evidence trail that persists after the attack, enabling forensic reconstruction and proving whether audience validation was performed on every token use.
from swt3_ai import Witness
import hashlib
witness = Witness(
endpoint="https://sovereign.tenova.io",
api_key="axm_live_...",
tenant_id="ACME_DEFENSE",
agent_id="mcp-gateway-v2",
)
# Witness OAuth token issuance
token_hash = hashlib.sha256(access_token.encode()).hexdigest()[:16]
witness.witness_oauth_token_binding(
event_type="oauth_token_issued",
server_id="github-mcp-server",
token_hash=token_hash,
scopes="repo,read:org",
audience="https://api.github.com",
scope_count=2,
expiry_epoch=1774900000,
)
# Witness audience validation on every token use
witness.witness_oauth_token_binding(
event_type="oauth_audience_check",
server_id="github-mcp-server",
token_hash=token_hash,
expected_audience="https://api.github.com",
actual_audience="https://api.github.com",
validation_result="pass",
)
# Witness token revocation
witness.witness_oauth_token_binding(
event_type="oauth_revoke",
server_id="github-mcp-server",
token_hash=token_hash,
reason="session_end",
initiator="user",
)
import { Witness } from "@tenova/swt3-ai";
import { createHash } from "crypto";
const witness = new Witness({
endpoint: "https://sovereign.tenova.io",
apiKey: "axm_live_...",
tenantId: "ACME_DEFENSE",
agentId: "mcp-gateway-v2",
});
const tokenHash = createHash("sha256")
.update(accessToken).digest("hex").slice(0, 16);
// Witness OAuth token issuance
await witness.witnessOAuthTokenBinding({
eventType: "oauth_token_issued",
serverId: "github-mcp-server",
tokenHash,
scopes: "repo,read:org",
audience: "https://api.github.com",
scopeCount: 2,
expiryEpoch: 1774900000,
});
// Witness audience validation
await witness.witnessOAuthTokenBinding({
eventType: "oauth_audience_check",
serverId: "github-mcp-server",
tokenHash,
expectedAudience: "https://api.github.com",
actualAudience: "https://api.github.com",
validationResult: "pass",
});
// Witness token revocation
await witness.witnessOAuthTokenBinding({
eventType: "oauth_revoke",
serverId: "github-mcp-server",
tokenHash,
reason: "session_end",
initiator: "user",
});
The SWT3 MCP server exposes witness_oauth_token_binding as a callable tool. Any MCP client can invoke it directly.
// MCP tool call
{
"tool": "witness_oauth_token_binding",
"arguments": {
"event_type": "oauth_audience_check",
"server_id": "github-mcp-server",
"token_hash": "a1b2c3d4e5f6...",
"expected_audience": "https://api.github.com",
"actual_audience": "https://api.github.com",
"validation_result": "pass"
}
}
| Parameter | Type | Description |
|---|---|---|
event_type |
string (enum) | One of the 8 event type codes listed in Section 2 |
server_id |
string | Identifier of the MCP server involved in the OAuth flow |
token_hash |
string | SHA-256 hash of the token (first 16 hex chars). Never the raw token. |
scopes |
string | Comma-separated list of OAuth scopes (for issuance/refresh events) |
audience |
string | Intended audience URI for audience validation events |
validation_result |
string | pass or fail (for audience check events) |
| Framework | Requirement | Event Type | Evidence Produced |
|---|---|---|---|
| OWASP MCP-07 | Confused Deputy / Third-Party MCP Server Exploitation | oauth_audience_check |
Audience validation result for every token use |
| NIST IA-9 | Service Identification and Authentication | oauth_discovery, oauth_token_issued |
Authorization server discovery and token issuance chain |
| NIST AC-6 | Least Privilege | oauth_scope_reduce |
Scope narrowing evidence with original and reduced scopes |
| EU AI Act Art. 15(3) | Cybersecurity measures for high-risk AI | oauth_token_issued, oauth_revoke |
Token lifecycle from issuance through revocation |
| Five Eyes FE-3 | Authentication and access control for agent tooling | oauth_audience_check, oauth_token_use |
Per-call token validation and resource access attribution |
| ISO 42001 A.10.5 | Data management and access control | oauth_scope_reduce, oauth_token_use |
Scope governance and per-resource access records |
| NIST AC-2 | Account Management | oauth_revoke |
Token lifecycle termination with initiator and reason |
OAuth scopes define what an MCP server can do on behalf of the user. Witnessing scope governance produces evidence of least-privilege enforcement that auditors can verify independently.
oauth_authz_request): Records the exact permissions the MCP server asked for. If a server requests repo,admin:org,delete:repo, this is permanently recorded.oauth_token_issued): Records what the authorization server actually granted. If the user or policy narrows the scopes to repo,read:org, the gap between requested and granted is visible.oauth_scope_reduce): Records voluntary scope narrowing by the MCP server. A well-behaved server that only needs read access can reduce its granted scopes, and the reduction is witnessed.oauth_token_use): Records which scope was actually used on each API call. Over time, this reveals unused scopes (permissions granted but never exercised), which are candidates for removal.The scope_count field in Factor C provides a numeric measure of privilege breadth. An assessor can query the ledger for anchors where scope_count exceeds a policy threshold, identifying MCP servers with excessive permissions.
By comparing oauth_token_issued anchors across refresh cycles, scope drift becomes visible. If a token refresh silently adds new scopes (a known attack vector), the anchors will show a scope_count increase that was not preceded by a new authorization request. This pattern is a finding.
Witnesses the complete OAuth lifecycle for MCP server connections. Eight event types cover discovery through revocation. Each anchor records the event type, server identity, token hash, scope list, audience, and validation result without ever storing raw credentials.
Factors: Factor A = event type code and MCP server identifier. Factor B = token hash, scopes, and audience. Factor C = scope count, validation result, and expiry epoch.
Query AI-MCP.5 anchors filtered by oauth_audience_check event type. Every anchor should show validation_result=pass. Any fail result indicates a confused deputy attempt. Cross-reference the server_id with known-good MCP server registrations. Check scope_count trends across refresh cycles for scope creep.