Security
AuthPlane is self-hosted, AGPL-3.0-licensed software: the authserver source is auditable, and it runs entirely inside your network boundary. This page summarizes the threat model the design is built around, how signing keys are managed, and what the independent audit covered. The full threat model — including incident-response playbooks — lives in the repository and is linked at the bottom.
Trust boundaries
Section titled “Trust boundaries”Three boundaries matter in a production deployment:
- Internet → TLS termination. Everything outside your network is untrusted. TLS must terminate before
authserver(reverse proxy or load balancer). - Public API → Admin API. The public OAuth surface runs on
:9000; the admin API runs on:9001as a separate listener specifically so you can firewall it. The admin port must never be reachable from the internet. authserver→ Database. The server trusts its database — but stored upstream tokens are encrypted, so a database breach yields ciphertext, not credentials.
What the design defends against
Section titled “What the design defends against”| Attack | Defense |
|---|---|
| Token replay | DPoP (RFC 9449): every proof carries a single-use jti consumed atomically in a database-backed replay store; server-issued nonces (DPoP-Nonce) bind proofs to a narrow window; iat freshness is enforced (default proof lifetime: 2 minutes). alg: none, all HMAC algorithms, and private keys in the jwk header are rejected outright. |
| Refresh token theft and reuse | Rotation on every use; presenting a consumed refresh token revokes the entire token family, forcing re-authentication for attacker and victim alike. Refresh tokens are stored as SHA-256 hashes, and consumption is gated by client authentication — a token holder without the client secret can’t burn the legitimate client’s token. |
| Audience confusion | RFC 8707 resource indicators with exact string matching (trailing slashes matter); the resource URI becomes the token’s aud claim, and the SDK-side JWT middleware rejects tokens whose aud doesn’t contain the configured resource URI. |
| Authorization code interception | PKCE S256 only; codes are single-use (atomic consumption, safe under concurrent requests) and expire in 10 minutes; redirect URIs match exactly — no wildcards, no prefix matching. |
| Open redirect | Invalid redirect URIs render an error page instead of redirecting; internal next parameters are validated. |
| Client impersonation | DCR modes (admin_only, approved_redirects), exact redirect URI matching, CIMD document validation, and one-call client suspension via the admin API. |
| SSRF on CIMD and provider fetches | Outbound fetches (CIMD documents, upstream provider endpoints, IdP JWKS discovery) pass through an SSRF-guarded transport that blocks private, loopback, and link-local ranges plus the full IANA special-use registries — CGNAT (RFC 6598), TEST-NET (RFC 5737), benchmark (RFC 2544), multicast (RFC 5771), Class E, and IPv6 special-use ranges. CIMD URLs require HTTPS in production. |
| Session attacks | HMAC-signed cookies with HttpOnly, Secure, and SameSite=Lax; startup validation enforces a strong session secret for non-localhost issuers. Login brute force is slowed by per-IP rate limiting, account lockout (default: 10 failures in 10 minutes → 15-minute lockout), and bcrypt hashing. |
| Token exchange privilege escalation | Exchanged scopes must be a subset of the subject token’s; delegation chain depth is capped; self-exchange is blocked by default; the requesting client must pass the may_act claim or a per-resource allowlist. |
| Stored upstream token theft | Third-party tokens are encrypted at rest with AES-256-GCM (or Vault Transit), with per-purpose keys derived via HKDF and the master key held in an environment variable — never in the database. |
Key management
Section titled “Key management”Signing keys live in one of three stores, selected by signing.key_store:
| Store | How it works |
|---|---|
keyfile (default) | Keys on disk with restrictive file permissions. |
postgres_key | Keys in Postgres, encrypted with a key-encryption key from an environment variable. Suited to multi-instance/HA deployments that need a shared key. |
vault_transit | The private key never leaves Vault — authserver sends data to Vault Transit for signing and only receives the signature back. |
Rotation is an online operation — POST /admin/keys/rotate or authserver admin key rotate — with no restart and no downtime: the new key signs immediately while the old key remains in the JWKS for verification of existing tokens. MCP servers pick up the new key on their next JWKS refresh (default: 5 minutes), and tokens signed with the old key age out within the default 15-minute access-token expiry.
Independent security audit
Section titled “Independent security audit”An independent security audit of authserver was performed on 2026-05-18. Findings in the audience-confusion and DPoP-proof-replay classes — on the resource-server verification side — were fixed: the shared JWT middleware that AuthPlane resource servers build on now rejects tokens whose aud claim doesn’t contain the configured resource URI, and consumes each DPoP proof’s jti in a resource-server replay store after binding validation.
Hardening shipped alongside those fixes: consent owner binding is enforced server-side (a user can only grant or deny consent for an authorization session they own), refresh-token consumption is gated by client authentication, and the SSRF blocklist was extended to the full IANA special-use registries. Details are in the changelog.
What to monitor
Section titled “What to monitor”The signals that tell you something might be wrong:
| Signal | Metric | What it means |
|---|---|---|
| Refresh token reuse | authplane_refresh_token_reuse_total | Someone replayed a consumed token. Possible theft — investigate immediately. |
| DPoP replay attempts | authplane_dpop_proofs_rejected_total{reason=replay} | Someone is replaying captured DPoP proofs. |
| Auth failure spike | authplane_auth_failures_total | Brute force or credential stuffing. |
| Token exchange denials | authplane_token_exchange_denied_total | Unauthorized exchange attempts — check the reason label. |
| Machine token denials | authplane_client_credentials_denied_total | Clients trying grants they’re not authorized for. |
Known limitations
Section titled “Known limitations”Design decisions, documented so you can plan around them:
| Limitation | What you can do |
|---|---|
| Single signing key per instance | Use Vault Transit or the postgres_key store for HA deployments that need a shared key. |
| No client authentication for public clients | MCP clients (Claude Code, Claude Desktop) can’t keep secrets; security relies on PKCE + refresh rotation, per OAuth 2.1 and the MCP spec. |
| In-memory rate limiting | Counters reset on restart and aren’t shared across instances — use an external rate limiter for multi-instance deployments. |
| No TLS termination | Run behind Caddy, nginx, or a load balancer with proper TLS configuration. |
| JWT revocation is not instant | A revoked JWT stays valid until expiry unless the resource server checks introspection. Keep token expiry short (15 minutes default). |
Incident response, condensed
Section titled “Incident response, condensed”The full playbooks live in the threat model; the short versions:
- Stolen access or refresh token — revoke via
POST /oauth/revoke, then checkGET /admin/audit?client_id=…to see what the token was used for. If a refresh token was stolen, reuse detection has already revoked the family; force re-authentication. If tokens are being stolen in transit, enable DPoP. - Signing key compromise — rotate immediately (
authserver admin key rotate). MCP servers refresh JWKS within 5 minutes by default, and access tokens signed with the old key expire within 15 minutes. Correlatejtivalues with the token store to check for forged tokens. - Admin API key leak — rotate
AUTHPLANE_ADMIN_API_KEYand restart, review the audit log for what the attacker did, and suspend any clients they created. - Database breach — the attacker gets ciphertext, not raw upstream tokens. Still: rotate the data-encryption master key (the old key serves as a decrypt-only fallback during the rotation window), rotate the signing key, reset user passwords, and re-issue client secrets.
Responsible disclosure
Section titled “Responsible disclosure”Report vulnerabilities privately to security@authplane.ai. Do not open public GitHub issues for security problems.
The repository’s SECURITY.md describes the private disclosure process in full.