Debugging checklist

TL;DR — When something is broken and you don’t know where, work top-down through this checklist. Each step is a 30-second check. Nine of ten broken deployments turn out to be discovery misconfig, resource-URI mismatch, or a missing env var — this page catches those fast.

Step 1 — Is AuthPlane reachable?

$ curl -s http://localhost:9000/health
{"status":"ok","time":"2026-07-01T00:00:00Z","db":"ok"}
  • Response OK → AuthPlane is running and can reach its DB. Next step.
  • Connection refused → AuthPlane isn’t running. Check docker ps / systemctl status authserver / pod status.
  • "status":"degraded","db":"error" → DB unreachable. For Postgres check the DSN + network reachability; for SQLite check the file exists and is writable by the AuthPlane user.

Step 2 — Does discovery work?

AS metadata:

$ curl -s http://localhost:9000/.well-known/oauth-authorization-server | jq .issuer
"http://localhost:9000"
  • issuer matches your AUTHPLANE_SERVER_ISSUER → OK.
  • issuer doesn’t match → wrong AUTHPLANE_SERVER_ISSUER at boot; every downstream check (JWT iss validation) will fail.
  • 404 → wrong URL. Discovery is on port 9000 (public), not 9001.

JWKS:

$ curl -s http://localhost:9000/.well-known/jwks.json | jq '.keys | length'
2
  • ≥ 1 keys → OK. current + optionally previous.
  • 0 keys → key store misconfig. Check signing.key_store and the key path/DB/Vault backing it.

PRM (on your MCP server, not AuthPlane):

$ curl -s https://mcp.example.com/.well-known/oauth-protected-resource | jq .
  • Full JSON with resource, authorization_servers, scopes_supported → OK.
  • 404 → SDK didn’t mount PRM handler (Go: http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler()); Python/TS auto-mount).
  • authorization_servers empty → wrong issuer passed to your SDK.

Step 3 — Do scopes advertise correctly?

$ curl -s http://localhost:9000/.well-known/oauth-authorization-server | jq .scopes_supported
["tools/read","tools/write"]

Should list every scope from every resource you’ve registered. Empty = no resources registered, or scopes not declared on any resource. Check: authserver admin resource list.

Step 4 — Is your token signed by the expected key?

Decode the JWT (never do this in production without redacting logs):

$ echo "<access_token>" | cut -d. -f1 | base64 -d | jq .
{
  "alg": "ES256",
  "typ": "at+jwt",
  "kid": "kid_current_abc"
}
  • kid should be in the JWKS from step 2. If not → JWKS is stale on the SDK side (rare; refresh happens every 5 min).
  • typ: at+jwt → RFC 9068 compliant (AuthPlane’s default).

Step 5 — Does the token’s aud match your resource?

$ echo "<access_token>" | cut -d. -f2 | base64 -d | jq '{iss, aud, sub, scope, exp}'
{
  "iss": "http://localhost:9000",
  "aud": ["http://mcp.example.com/mcp"],
  "sub": "user-42",
  "scope": "tools/read",
  "exp": 1708762500
}
  • aud doesn’t contain your resource URI → RFC 8707 mismatch. Client didn’t send resource=<your-uri> on /oauth/authorize. The fix is on the client (or your PRM advertisement) — AUTHPLANE_OAUTH_REQUIRE_SCOPE only affects scope defaulting, not audience enforcement, and toggling it will not repair an audience mismatch.
  • iss doesn’t match AuthPlane’s issuer → wrong AuthPlane URL in the SDK’s config.
  • scope — check it contains what require_scope() demands in your tool handler.
  • exp — is the token expired? Check clock skew (default tolerance is 30 s).

Step 6 — Is your MCP server’s SDK reading the token?

Structured slog logs (JSON format) from your SDK:

level=INFO msg="verified" sub=user-42 scope=tools/read aud=http://mcp.example.com/mcp
  • Log present → SDK is reading + verifying tokens.
  • No log at all → SDK isn’t wired into the request path. Check middleware ordering. Ensure bearerAuth (or equivalent) runs BEFORE your tool handlers.
  • Log with error → follow the error code — likely one of the top-20 in Common errors.

Step 7 — Are metrics telling you a story?

$ curl -s http://localhost:9001/metrics | grep -E "^(authserver|authplane)_" | head -20
authserver_tokens_issued_total 42
authserver_auth_denied_total{reason="invalid_pkce"} 3
authplane_dpop_proofs_validated_total 15
authplane_dpop_proofs_rejected_total{reason="replay"} 0
  • auth_denied_total{reason="…"} growing → clients failing for a reason you can query. Filter by reason label to narrow.
  • dpop_proofs_rejected_total > 0 → DPoP clients hitting issues (proof format, replay, htu mismatch). See Guides: Enable DPoP → troubleshooting.
  • upstream_token_refresh_total{outcome="failed"} > 0 → broker refresh failing (user revoked upstream, or provider is down).

Step 8 — Correlate a specific failing request end-to-end

Every AuthPlane log line carries request_id, trace_id, span_id. Grab one from a failure:

level=ERROR msg="token issuance failed" error=ErrInvalidPKCE code=invalid_grant
  client_id=my-client resource=http://mcp.example.com/mcp
  request_id=r_abc123 trace_id=t_def456 span_id=s_ghi789

Query all logs with that trace_id:

$ kubectl logs -l app=authplane --tail=1000 | grep trace_id=t_def456

Or if you have OTEL traces wired (Guides: Monitoring → OpenTelemetry), find the trace in your tracing backend — every span from HTTP handler → service → storage → crypto is there.

Step 9 — Reproduce with MCP Inspector

If nothing above turns up the issue, run MCP Inspector against your MCP server:

$ npx @modelcontextprotocol/inspector https://mcp.example.com/mcp

Inspector shows every request/response of the OAuth flow inline — see Guides: Testing with MCP Inspector. If Inspector works but your target client (Claude Code, Cursor, etc.) doesn’t, it’s a client-specific quirk — see Guides: Connect an MCP client → Known Claude Code quirks and authserver/docs/compatibility.md.

Step 10 — Check the audit log

Every security-relevant event writes an audit_events row. There’s no admin audit … CLI subcommand — query via the Admin REST API:

$ curl -s "http://localhost:9001/admin/audit?since=2026-07-01T00:00:00Z&user_id=user-42&limit=20" \
    -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY"

You’ll see token_issued, token_revoked, consent_granted, broker_grant_updated, etc. — the full sequence of what actually happened for that user in AuthPlane.

When to escalate

If you’ve gone through all ten steps and can’t pin the failure — file a bug at github.com/authplane/authserver/issues with:

  • Version (authserver version or the container tag)
  • Redacted config
  • Redacted request/response of the failing OAuth call
  • Log lines with trace_id
  • Metric values from /metrics if relevant

See Getting help.