Threat model
TL;DR — 16 named threats spanning auth-code interception, token theft, client impersonation, session hijacking, JWKS spoofing, admin API abuse, key compromise, CIMD tampering, vault token theft, unauthorized upstream vending, DPoP replay + algorithm confusion, and token-exchange privilege escalation. Each has a specific mitigation (usually multiple layers) and a metric or log signal to alert on. This is the operator-facing summary; the full mitigation source lives in
authserver/docs/security/threat-model.md.
Trust boundaries
flowchart TD
subgraph Internet
Client["MCP Client<br/>(Claude)"]
User["User<br/>Browser"]
end
AS9000["AuthPlane :9000<br/>public OAuth API"]
AS9001["AuthPlane :9001<br/>Admin API (internal only)"]
DB["Database (SQLite/PG)"]
MCP["MCP Server<br/>Protected resource"]
Client -->|TLS boundary| AS9000
User -->|TLS boundary| AS9000
AS9000 --> AS9001
AS9001 --> DB
Three trust boundaries matter:
- Internet → TLS termination — everything outside is untrusted; TLS terminates before AuthPlane (reverse proxy or LB).
- Public → Admin API —
:9001must NEVER be reachable from the internet; separate listener specifically to firewall. - AuthPlane → Database — if the DB is compromised, most bets are off; encrypted upstream refresh grants (
broker_grants) remain protected by the encryption layer.
The 16 threats
T1 · Authorization code interception
Attacker grabs the auth code from the browser redirect, races to exchange it.
- Mitigations: PKCE-S256 (verifier never leaves client), single-use atomic code consumption, 10-min expiry, exact
redirect_urimatching (no wildcards). - Signal: spike in
authserver_auth_denied_total{reason="invalid_pkce"}.
T2 · Refresh token theft and replay
Attacker steals a refresh token from a leaked log / disk / memory.
- Mitigations: mandatory rotation, family-based reuse detection — if the same refresh token is used twice, the entire family is revoked immediately.
- Signal:
rate(authserver_refresh_token_reuse_total[5m]) > 0— critical alert. Every trigger = confirmed theft or client bug.
T3 · Client impersonation
Attacker crafts a fake OAuth client to trick users.
- Mitigations: confidential clients require secret verification (bcrypt); DCR
approved_redirectsmode restricts registrations; suspended clients can’t issue tokens. - Signal: unexpected
authserver_clients_registered_totalgrowth indcr.mode: opendeployments.
T4 · Session hijacking
Attacker steals the user’s session cookie.
- Mitigations:
secure+HttpOnlycookies (production requirement), signed withsession.secret(32+ bytes required in prod), IP/UA-tracked, expires persession.max_age. - Signal: unusual
authserver_login_attempts_total{result="stolen_session_reuse"}.
T5 · Credential brute force
Attacker guesses passwords via /login.
- Mitigations: rate limiter tracks per-IP failures (
rate_limit.auth_fail_max, default 10 per 10min), lockout forrate_limit.auth_lockout(default 15min), bcrypt cost factor makes each guess slow. - Signal:
rate(authserver_login_attempts_total{result="failure"}[5m]) > threshold.
T6 · Open redirect
Attacker crafts a redirect_uri that sends the user (and their code) somewhere they control.
- Mitigations: exact string matching against registered
redirect_uris;connect.allowed_return_urlsfor the Connect flow. - Signal:
authserver_auth_denied_total{reason="redirect_uri_mismatch"}.
T7 · JWKS spoofing
Attacker MITMs the JWKS fetch to serve a key they control.
- Mitigations: SDKs use HTTPS by default (SSRF guards + HTTPS-only unless
dev_mode); operators must front AuthPlane with TLS; JWKS signatures anchor to a staticissuerclaim the SDK matches. - Signal: SDK-side
authplane_jwks_fetch_failures_total; MCP server observability.
T8 · Admin API abuse
Attacker reaches :9001 and creates rogue clients / rotates keys / exfiltrates data.
- Mitigations: API key auth (
AUTHPLANE_ADMIN_API_KEY, 32+ bytes in prod), separate listener (:9001not exposed publicly), Kubernetes NetworkPolicy or reverse-proxy IP allowlist, per-action audit log. - Signal:
authserver_admin_ops_totalper action; audit log query for unusual patterns.
T9 · Signing key compromise
Attacker steals the current signing key and forges tokens.
- Mitigations:
keyfilerestricted to authserver user (chmod 600);postgres_keyencrypts at rest withdata_encryptiondriver;vault_transitkeeps private key server-side (never on AuthPlane host); rotation on suspicion. - Signal: unusual
authserver_key_rotation_total(rotations you didn’t trigger) — investigate the process that did.
T10 · CIMD document tampering
Attacker serves a fake CIMD document to trick DCR into registering with attacker-controlled metadata.
- Mitigations:
cimd.require_https: true(default); fetched documents cached and revalidated; content-type + JSON validation on fetch. - Signal:
authserver_cimd_fetch_duration_secondsoutliers; consider disabling CIMD if not used (cimd.enabled: false).
T11 · Stored vault token theft
Attacker with DB access reads upstream refresh grants.
- Mitigations: all
broker_grantsrows encrypted at rest with AES-256-GCM (data_encryption: aes_master) or Vault Transit; encryption key stored via env var, never in DB. - Signal: DB access itself is the primary detection — instrument at storage layer.
T12 · Unauthorized upstream token vending
Attacker gets AuthPlane to vend an upstream token for a resource/scope they shouldn’t have.
- Mitigations: three-bound consent model —
consent_grants(per-agent attestation) +broker_grants(per-provider grant) + scope narrowing at exchange time; per-resourcepolicy.exchange.allowed_client_ids. - Signal:
authserver_upstream_token_issued_totalunusual by (client, provider) combo; audit log filtered bytoken_vended.
T13 · Connect-flow CSRF
Attacker tricks a logged-in user into connecting the attacker’s account.
- Mitigations: HMAC-signed state tokens using
connect.state_secret(32+ bytes), single-use per Connect; state-bound to session cookie;allowed_return_urlsguards post-Connect redirect. - Signal:
authserver_connect_denied_total{reason="state_invalid"}.
T14 · DPoP proof replay
Attacker captures a DPoP proof and reuses it.
- Mitigations: per-proof
jtistored indpop_noncestable, replay detection at verify time, 60sproof_lifetime, optional server-issued nonce (dpop.require_nonce: true) narrows the replay window. - Signal:
rate(authplane_dpop_proofs_rejected_total{reason="replay"}[5m]).
T15 · DPoP algorithm confusion
Attacker crafts a proof with alg: none or a symmetric algorithm.
- Mitigations: allowed algorithms whitelisted at boot (
ES256,RS256,PS256);alg: noneand all symmetric algorithms always rejected; private key injwkheader always rejected. - Signal:
authplane_dpop_proofs_rejected_total{reason="disallowed_alg"}should always be 0.
T16 · Token exchange privilege escalation
Client A exchanges its token for a token for resource B they shouldn’t have access to.
- Mitigations: per-resource
policy.exchange.allowed_client_ids, scope narrowing (requested ⊆ subject scope), chain-depth limit (token_exchange.max_chain_depth, default 5), audit trail preserves fullactchain. - Signal:
authplane_token_exchange_denied_totalspikes on specific (client, resource) pairs.
Residual risks (not fully mitigated)
- Physical database access — encryption at rest is limited to the
broker_grantstable; the rest of the DB is trusted. Compromise = broad blast radius. - Admin API key leak — no automatic key rotation for admin key; leaked key = full instance takeover. Manual rotation is fast (
admin.api_keyenv var change + restart). - Compromised MCP server — AuthPlane can’t detect a rogue MCP server logging bearer tokens. DPoP-bound tokens narrow this significantly; without DPoP, MCP-server-side security is your concern.
Operational hardening checklist
- Set
admin.api_keyto 32+ bytes; expose:9001internal-only. - Set
session.secretto 32+ bytes; setsession.secure: true. - Enable
data_encryption(required ifbroker_providersset). - Use
vault_transitsigning for multi-instance production;postgres_keyat minimum. - Wire the audit log to a long-term store (SIEM, S3+Glacier).
- Alert on the 6 rules in Guides: Monitoring → alerts.
- Rotate signing keys on suspicion or every 90 days.
- Restrict CORS (
server.allowed_origins) to the browser origins that actually need it.
Related
- Security: Token design — why 15-min access tokens, opaque refresh, no ROPC
- Security: DPoP — deep dive on RFC 9449 mitigations
- Security: Key management — key lifecycle + rotation policy
- Security: Reporting vulnerabilities
- Guides: Monitoring — every signal above with concrete PromQL
- Full source in the authserver repo — extended narrative per threat