Claude.ai, ChatGPT, Cursor — they all speak OAuth, not API keys. We built native OAuth 2.1 into OpenViking so these clients can connect without a proxy.
Before this change, connecting an MCP client to OpenViking required a Cloudflare Worker proxy. You deploy a Worker, configure two KV namespaces, and bridge OAuth tokens to API keys. It works. It also means you are trusting a third party with your credentials, paying for another service, and debugging across two systems when something breaks.
What the user sees
The entire flow, from the user side:
No API key pasted anywhere. No proxy deployed. No extra services to maintain.
Design decisions
Six choices shaped the implementation. Each one is a tradeoff:
| Decision | Choice | Why |
|---|---|---|
| Protocol | mcp.server.auth SDK | Zero crypto code on our side. DCR, PKCE, redirect validation all handled upstream. |
| Token format | Opaque + SQLite | No signing keys to manage. Revocation is one UPDATE. SHA-256 hash index, microsecond lookups. |
| Auth flow | Device-flow style | "I approve that request from a place I trust." Users confirm in an already-authenticated browser. |
| Quick auth | localStorage + explicit click | Convenient but not automatic. User always knows what they are approving. |
| Redirect whitelist | None (DCR accepts any URI) | Matches MCP ecosystem behavior. SDK does strict-equal validation internally. |
| Revocation scope | Per (account, user) | API key rotation revokes all OAuth state for that user. Clean break. |
Tokens
Four token types, each with a prefix for fail-closed routing:
Deployment
The PR adds a Caddy reverse proxy that merges two ports into one:
:1934 { handle /console/* { reverse_proxy :8020 } handle { reverse_proxy :1933 }}Why merge?
Same-origin guarantee. Console and OAuth pages share one origin, so Quick Authorize works out of the box.
HTTPS?
OAuth 2.1 requires HTTPS for non-localhost issuers. Add a domain block to Caddyfile and uncomment 3 lines in docker-compose. Done.
Backwards compatibility
- Off by default —
oauth.enabled: falsemeans zero side effects. No routes mounted, no bearer inspection. - Bearer routing — Only tokens with
ovat_prefix go through OAuth lookup. Plain bearer tokens still hit the API key path. - Fail-closed — If a token has the OAuth prefix but is not found, it is 401. No fallback to API key. No ambiguity.
{ "oauth": { "enabled": true }}Verified clients
| Client | Status |
|---|---|
| ChatGPT | Working |
| Claude Desktop | Working |
| MCP Inspector | Working |
| Cursor | Pending |
37 new tests cover the full flow: storage atomics, device-flow happy path, bearer fail-closed, refresh rotation, replay detection, and WWW-Authenticate header injection. Zero regressions in existing auth tests.

