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:

  1. Internet → TLS termination — everything outside is untrusted; TLS terminates before AuthPlane (reverse proxy or LB).
  2. Public → Admin API:9001 must NEVER be reachable from the internet; separate listener specifically to firewall.
  3. 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_uri matching (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_redirects mode restricts registrations; suspended clients can’t issue tokens.
  • Signal: unexpected authserver_clients_registered_total growth in dcr.mode: open deployments.

T4 · Session hijacking

Attacker steals the user’s session cookie.

  • Mitigations: secure + HttpOnly cookies (production requirement), signed with session.secret (32+ bytes required in prod), IP/UA-tracked, expires per session.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 for rate_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_urls for 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 static issuer claim 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 (:9001 not exposed publicly), Kubernetes NetworkPolicy or reverse-proxy IP allowlist, per-action audit log.
  • Signal: authserver_admin_ops_total per action; audit log query for unusual patterns.

T9 · Signing key compromise

Attacker steals the current signing key and forges tokens.

  • Mitigations: keyfile restricted to authserver user (chmod 600); postgres_key encrypts at rest with data_encryption driver; vault_transit keeps 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_seconds outliers; 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_grants rows 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-resource policy.exchange.allowed_client_ids.
  • Signal: authserver_upstream_token_issued_total unusual by (client, provider) combo; audit log filtered by token_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_urls guards 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 jti stored in dpop_nonces table, replay detection at verify time, 60s proof_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: none and all symmetric algorithms always rejected; private key in jwk header 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 full act chain.
  • Signal: authplane_token_exchange_denied_total spikes on specific (client, resource) pairs.

Residual risks (not fully mitigated)

  • Physical database access — encryption at rest is limited to the broker_grants table; 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_key env 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_key to 32+ bytes; expose :9001 internal-only.
  • Set session.secret to 32+ bytes; set session.secure: true.
  • Enable data_encryption (required if broker_providers set).
  • Use vault_transit signing for multi-instance production; postgres_key at 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.