Configuration
TL;DR — AuthPlane loads config in three layers, highest priority last: built-in defaults → YAML file →
AUTHPLANE_*env vars. Everything you need to boot is optional (SQLite + auto-generated keys + no config file); everything you need for production is explicit and validated at boot. This page lists every section, every key, and every env var.
Precedence
- Defaults — sensible values for local development, built into the binary.
- YAML file —
authserver serve --config config.yaml. - Environment variables —
AUTHPLANE_*prefix; override YAML.
Env vars win because you want the same YAML file across environments with per-deployment overrides for secrets and issuer URLs.
Comma-separated list variables (like AUTHPLANE_OIDC_SCOPES=openid,email,profile) split on commas. Duration variables accept Go duration strings (10s, 5m, 1h, 24h). Bool variables accept true, false, 1, 0.
Minimal dev config
Zero config works — the binary starts on :9000 with SQLite, auto-generated signing keys, and no resource. The moment you need to accept a real MCP client, add a resource:
resources:
- slug: my-mcp-server
uri: http://localhost:3000/
backend_kind: mint
display_name: My MCP Server
scopes:
- name: tools/echo
description: Echo tool
Or a single Mint resource via env vars:
export AUTHPLANE_RESOURCE_URI=http://localhost:3000/
export AUTHPLANE_RESOURCE_SCOPES=tools/echo
authserver serve
Minimal production config
Boot validation enforces these when server.issuer is not localhost:
server:
issuer: https://auth.example.com
session:
secret: "generate-a-32-byte-random-string" # openssl rand -hex 32
secure: true
admin:
api_key: "generate-a-secure-api-key" # openssl rand -hex 32
Anything missing here fails boot with a structured error — you cannot accidentally deploy a non-local AuthPlane with a random ephemeral session secret.
server
Public HTTP server settings.
server:
issuer: http://localhost:9000 # Public URL — appears in AS metadata and JWT `iss`
address: ":9000" # Listen address
read_timeout: 30s
write_timeout: 30s
idle_timeout: 120s
shutdown_wait: 10s # Graceful shutdown drain
allowed_origins: [] # CORS allowlist for browser MCP clients
issuer appears in the /.well-known/oauth-authorization-server metadata and as the iss claim in every JWT. MCP clients use it for discovery. Must match the URL clients use to reach AuthPlane exactly — no trailing slash.
allowed_origins — empty by default. Browser-based MCP clients (MCP Inspector, Claude Desktop web) fail CORS preflight silently without it. For local dev set AUTHPLANE_SERVER_ALLOWED_ORIGINS=*; for production set an explicit allowlist. Server-to-server-only deployments may leave this empty.
storage
Persistence layer. Both backends implement the same interfaces — choose by driver.
storage:
driver: sqlite # "sqlite" or "postgres"
sqlite:
path: data/authserver.db
wal: true # WAL mode for concurrent reads
postgres:
dsn: "postgres://user:pass@localhost:5432/authserver?sslmode=require"
max_conns: 25
min_conns: 5
max_conn_lifetime: 1h
max_conn_idle_time: 30m
SQLite is default and recommended for single-instance deployments. Pure Go via modernc.org/sqlite (no CGO). WAL mode enables concurrent reads while writes serialize. DB file is created automatically on first boot.
PostgreSQL is required for multi-instance (HA) deployments. Uses LISTEN/NOTIFY for cross-instance signing-key rotation. Run authserver migrate before first start.
signing
JWT signing keys and algorithms.
signing:
algorithm: ES256 # "ES256" (default) or "RS256"
key_store: keyfile # "keyfile", "postgres_key", or "vault_transit"
key_path: data/keys # Keyfile store: directory for PEM files
vault_transit: # Only when key_store: vault_transit
address: https://vault:8200
token: "" # Static token (mutually exclusive with approle)
mount: transit
key_name: authserver-signing
timeout: 10s
approle:
role_id: ""
secret_id: ""
mount: approle
Algorithms — ES256 (ECDSA P-256) is default: compact tokens, fast verification. RS256 (RSA 2048) for environments that require it (legacy IDPs, HSM constraints).
Key stores — keyfile writes PEM keys to key_path. postgres_key (HA-safe) stores keys in the storage DB with encryption at rest — required when running multiple instances so key rotation propagates via LISTEN/NOTIFY. Note: postgres (without _key) fails boot; the suffix disambiguates from the storage driver of the same name. vault_transit delegates signing to HashiCorp Vault Transit — private keys never leave Vault. See Security: Key management and Operate: Vault Transit.
Key rotation — authserver admin key rotate generates a new signing key. The old key stays in the JWKS until it expires so in-flight tokens keep verifying.
dcr — Dynamic Client Registration (RFC 7591)
dcr:
mode: open # "open", "approved_redirects", or "admin_only"
approved_redirects: # Used when mode: approved_redirects
- http://localhost:*
- https://inspector.mcp.garden/*
rate_limit: 10 # Registrations per second
rate_limit_burst: 20
default_token_expiry: 15m
default_refresh_expiry: 168h # 7 days
Modes:
open— any client can register with any redirect URI. Development only.approved_redirects— anyone can register, but the redirect URI must match a pattern inapproved_redirects. Balanced for production with well-known MCP clients.admin_only— clients must be pre-registered via the Admin API or CLI. Hardened default for internal-only deployments.
Rate limits apply per-IP to the /oauth/register endpoint. Defaults are conservative — bump for automated CI environments.
cimd — Client ID Metadata Document
Auto-registration via URL client_id (draft-ietf-oauth-client-id-metadata-document).
cimd:
enabled: true
require_https: true # Require HTTPS for CIMD document URLs
cache_ttl: 1h # Cache fetched documents for this long
fetch_timeout: 10s # HTTP timeout for fetching CIMD docs
When a client presents a URL as its client_id, AuthPlane fetches and validates the metadata document at that URL. Set require_https: false only for local dev with http:// CIMD URLs.
session
Cookie-based session for the login and consent pages.
session:
cookie_name: authserver_session
max_age: 24h
secure: false # Must be true in production
same_site: lax # "lax", "strict", or "none"
secret: "" # Required in production (32+ bytes)
The session secret signs and encrypts session cookies. For localhost dev, a random ephemeral secret is generated if none is set — sessions don’t survive restart. For production, set a stable 32+ byte value (openssl rand -hex 32).
rate_limit
Global rate limiting + brute-force protection on auth endpoints.
rate_limit:
enabled: true
requests_per_second: 100 # Global RPS limit
burst: 200 # Burst allowance
auth_fail_max: 10 # Max failed auth attempts per IP
auth_fail_window: 10m # Window for counting failures
auth_lockout: 15m # Lockout duration after exceeding max
Token-bucket global limit plus per-IP tracking of failed logins. Lockout applies to the /login endpoint only — OAuth endpoints stay open.
oauth
oauth:
require_scope: true # Require scope parameter in authorize requests
When true (default), /oauth/authorize requests without a scope parameter are rejected with invalid_scope per RFC 6749 §3.3. When false, missing scope defaults to all registered scopes for the target resource — useful for MCP clients (like Claude Code at v0.1.x) that don’t send scope on /authorize. The decision is recorded as ADR-012; the conformance rationale (both branches satisfy §3.3) lives in the authserver compliance statement.
Optional grants — on by default since v0.2.0
Defaults changed in v0.2.0 — four features flipped from
falsetotrue:client_credentials.enabled,dpop.enabled,token_exchange.enabledandxaa.enabled(the last is documented in its own section below). Set any of them tofalse— or the matchingAUTHPLANE_*_ENABLEDenv var — to keep the v0.1.x posture. The full v0.2.0 breaking-change list is in the authserver CHANGELOG.
Client credentials, DPoP and token exchange were each off by default through
v0.1.x and are on from v0.2.0. Set them to false to turn them off:
client_credentials:
enabled: true # RFC 6749 §4.4 — false through v0.1.x
token_expiry: 1h
dpop:
enabled: true # RFC 9449 — false through v0.1.x
nonce_ttl: 60s
proof_lifetime: 60s
require_nonce: false
token_exchange:
enabled: true # RFC 8693 — false through v0.1.x
allow_self_exchange: false
max_chain_depth: 5
token_expiry: 1h
dpop.enabled turns on DPoP support, not enforcement: a client that sends no
proof still receives a bearer token. Rejecting unbound requests is the SDK’s
inbound_dpop setting — see Enable DPoP end-to-end.
client_credentials— required for machine-to-machine flows and any SDK that performs server-to-server introspection. See Concepts: Grants & flows and Topologies: Backend service + MCP.dpop— sender-constrained tokens. Additive: clients that don’t send a DPoP header still get bearer tokens (unlessrequire_nonce: truelocks it down). See Concepts: DPoP.token_exchange— powers delegation, Broker resources, and Cross-App Access. Cross-client authorization is enforced per-resource viapolicy.exchange.allowed_client_idson each registered resource. See Concepts: Delegation & act-chain.
XAA and JWT Bearer
Enterprise-Managed Auth (Cross-App Access). On by default since v0.2.0 — the grant validates nothing until an operator registers a trusted IdP, because the registry starts empty. Disabled by default through v0.1.x, where the block is also YAML-only (no env-var overrides there).
xaa:
enabled: true # false through v0.1.x
jwks_cache_ttl: 1h
token_expiry: 1h
max_assertion_age: 5m
subject_mode: auto_map # "auto_map" or "strict"
require_resource: false # true = refuse exchanges that name no `resource=`
Since v0.2.0 every field also has an env var: AUTHPLANE_XAA_ENABLED,
AUTHPLANE_XAA_TOKEN_EXPIRY, AUTHPLANE_XAA_MAX_ASSERTION_AGE,
AUTHPLANE_XAA_REQUIRE_RESOURCE, AUTHPLANE_XAA_SUBJECT_MODE,
AUTHPLANE_XAA_JWKS_CACHE_TTL.
Trusted IdPs, policies, and subject mappings are managed via the Admin REST API only — POST /admin/idps, POST /admin/xaa/policies, POST /admin/xaa/subject-mappings. There is no authserver admin xaa … CLI subcommand. See Concepts: Cross-App Access and Guides: Enterprise-Managed Auth.
agents
agents:
enable_jwks_listing: false # Publish agent JWK sets on /.well-known/agents.json
Agent identity claims (agent_id, agent_chain) are emitted on every token by default. This flag controls the optional publishing of agent JWK sets for third-party verification.
admin
Admin API + Admin UI, on a separate port from public OAuth.
admin:
enabled: true
address: ":9001" # Separate port
api_key: "" # Required in production
Hosts both the REST API under /admin/* and the built-in React Admin UI at /admin/ui/. Authenticate API requests with Authorization: Bearer <api_key>. The UI uses the same key entered in-browser and stored in sessionStorage. Never expose :9001 to the public internet — keep it on an internal network or behind a firewall/NetworkPolicy.
oidc — Upstream OIDC Federation
oidc:
enabled: false
issuer: https://accounts.google.com
client_id: ""
client_secret: ""
display_name: Google # Button text on login page
scopes: [openid, email, profile]
redirect_uri: https://auth.example.com/oidc/callback
show_local_login: true # Show password form alongside OIDC button
include_groups_scope: true # Include "groups" scope if upstream supports it
connector_id: "" # Dex connector_id parameter (optional)
Validation: oidc.issuer and oidc.redirect_uri must use HTTPS in production; client_id, client_secret, and redirect_uri are required when enabled. See Guides: Federate to your IdP.
resources
Every MCP server (Mint) and every upstream provider target (Broker) is a resources entry.
resources:
- slug: my-mcp-server
uri: http://mcp-server:3000/mcp # MCP canonical form — no trailing slash
backend_kind: mint
display_name: My MCP Server
scopes:
- name: tools/echo
description: Echo a message
- name: tools/query_database
description: Query the database
policy:
exchange:
allowed_client_ids: [] # Empty = any consented client
runtime:
client_ids: [] # Empty = no runtime binding
connect:
allowed_return_urls: [] # Overrides connect.allowed_return_urls
Env vars support a single Mint resource. For multiple resources or any Broker, use YAML or the Admin API.
uri must match byte-for-byte what your MCP server publishes in its Protected Resource Metadata (RFC 9728). Compliant MCP clients echo the PRM resource field verbatim on /oauth/authorize and /oauth/token; AuthPlane uses exact string matching for audience validation. A one-character mismatch silently rejects every token. Use the MCP spec canonical form (no trailing slash, lowercase host, no default port). Watch for scheme/host case, default ports, percent-encoding — see Guides: Connect an MCP client.
backend_kind — mint (AS issues its own JWT) or broker (AS vends an upstream provider token via RFC 8693). See Concepts: Architecture.
policy.exchange.allowed_client_ids — per-resource replacement for the legacy global cross-client allowlist. Empty means any consented client can exchange into this resource.
policy.runtime.client_ids — per-resource runtime binding: whitelist of clients whose tokens are accepted at THIS resource. See Guides: Runtime client binding.
observability
Logs, traces, metrics.
observability:
logging:
level: info # debug | info | warn | error
format: json # json | text
add_source: false # Include file:line in log records
outputs:
stdout: true
otel: false
otel_endpoint: "" # OTLP gRPC endpoint
insecure: false # Allow plaintext gRPC
tracing:
enabled: false
endpoint: "" # OTLP gRPC endpoint (e.g. localhost:4317)
insecure: false
sample_rate: 1.0 # 0.0 to 1.0
metrics:
provider: prometheus # prometheus | otel | both | none
path: /metrics # Prometheus scrape endpoint
otel_endpoint: "" # OTLP endpoint when provider=otel or both
insecure: false
Full metric catalog and Prometheus setup in Guides: Monitoring and Reference: Metrics & CLI.
data_encryption
Encrypts sensitive data at rest — upstream refresh grants in broker_grants, and (when Postgres key store is enabled) signing keys.
data_encryption:
driver: aes_master # "aes_master" or "vault_transit_encrypt"
aes_master:
key_env: AUTHPLANE_VAULT_ENCRYPTION_KEY # Env var containing hex-encoded 256-bit key
old_key_env: AUTHPLANE_VAULT_OLD_KEY # Previous key for rotation (optional)
vault_transit_encrypt:
address: https://vault:8200
auth_method: token # "token" or "approle"
token_env: VAULT_TOKEN
mount_path: transit
key_name: authserver-data
approle:
role_id_env: VAULT_APPROLE_ROLE_ID
secret_id_env: VAULT_APPROLE_SECRET_ID
AES master key — local AES-256-GCM with HKDF-derived per-purpose subkeys. Generate: openssl rand -hex 32.
Vault Transit encrypt — delegates encryption to HashiCorp Vault Transit; plaintext never touches disk. Required for compliance environments.
broker_providers
Upstream OAuth / API-key / service-account providers that Broker resources reference. See Concepts: Token Vault.
broker_providers:
- slug: github
display_name: GitHub
protocol: oauth
config_data:
client_id: "your-github-app-client-id"
client_secret_ref: "CONNECTOR_GITHUB_SECRET"
authorize_url: "https://github.com/login/oauth/authorize"
token_url: "https://github.com/login/oauth/access_token"
response_format: standard
- slug: slack
display_name: Slack
protocol: oauth
config_data:
client_id: "your-slack-app-client-id"
client_secret_ref: "CONNECTOR_SLACK_SECRET"
authorize_url: "https://slack.com/oauth/v2/authorize"
token_url: "https://slack.com/api/oauth.v2.access"
response_format: form
No env-var shortcut — broker providers are managed via YAML or the Admin API/CLI (authserver admin provider create). The slug is the public ID; Broker resources reference it via broker_provider_slug.
protocol — one of oauth, apikey, service_account. Each dispatches to a different adapter under internal/brokerproto/.
Default scopes live on the Broker Resource row, not the provider — one provider can back multiple resources with different scope sets.
connect
The user-facing OAuth flow to grant your MCP server upstream access.
connect:
state_secret: "generate-a-32-byte-random-string" # HMAC key for state tokens
allowed_return_urls: # Global default (per-resource override available)
- http://localhost:*
- https://myapp.example.com/*
redirect_base_url: http://localhost:9000 # Base URL for OAuth callbacks
state_secret — HMAC key for signing state tokens during the connect flow. Must be at least 32 characters (openssl rand -hex 32). Different key from session.secret.
allowed_return_urls — global default whitelist for connect-flow return URLs. Each resource can override via resources[].policy.connect.allowed_return_urls for finer control.
Validation rules enforced at boot
Boot fails with a structured error if any of these are violated:
session.secretrequired whenserver.issueris not localhostsession.secure: truerequired whenserver.issueris not localhostadmin.api_keyrequired whenadmin.enabled: trueandserver.issueris not localhostoidc.client_id+client_secret+redirect_uriall required whenoidc.enabled: trueoidc.issuer+oidc.redirect_urimust use HTTPS in production- Every enabled feature’s required-config combination is validated up-front (partial config → boot fails at startup, not at first request)
resources[].urimust be a well-formed URL
Config precedence in practice
Debug your effective config by comparing all three sources:
# See defaults + YAML
$ authserver serve --config config.yaml --dry-run
# See what env vars are set
$ env | grep AUTHPLANE_
# See the final self-check report in the boot log
$ authserver serve --config config.yaml 2>&1 | grep -A20 "feature self-check"
Every enabled feature logs a one-liner at boot indicating what config it picked up. Misconfigurations are fatal — the process exits with a structured error naming the missing keys.
Related
- Concepts: Architecture — the layers each config section maps to
- Guides: Federate to your IdP — full OIDC setup by provider
- Guides: Enable DPoP end-to-end — tuning the DPoP knobs above
- Guides: Wire up the Token Vault — putting
broker_providers+connecttogether - Operate: Vault Transit — key store + data encryption
- Reference: Metrics & CLI — what
observabilityproduces - Security: Key management — rotation, algorithm choice, HSM