OAuth 2.1
ServerFS v0.16.0 adds optional OAuth 2.1 authentication for remote Streamable HTTP. It is disabled by default and does not apply to native Windows/macOS stdio. In v0.17+, built-in OAuth is mutually exclusive with SERVERFS_HTTP_API_KEY; choose one ServerFS-owned HTTP authentication mode per instance.
OAuth answers who may enter this ServerFS instance. Existing workdir policy still decides what the admitted caller may do.
GitHub / DingTalk / Feishu / WeChat │ ▼ serverfs-auth identity + exact allowlist ServerFS opaque tokens │ private introspection ▼ serverfs-mcp │ ▼ existing workdir policyserverfs-auth owns upstream identity-provider egress and credentials. serverfs-mcp receives neither provider credentials nor general Internet egress.
Deployment boundary
Section titled “Deployment boundary”OAuth is currently a Linux Docker Compose capability:
docker compose --env-file .env \ -f compose.yml \ -f compose.http.yml \ -f compose.oauth.yml \ up -dAdd compose.agent.yml only when Agent delegation is needed.
A typical public deployment uses separate HTTPS origins:
https://mcp.example.com/mcp -> 127.0.0.1:8000/mcphttps://auth.example.com -> 127.0.0.1:8082Keep Docker publications loopback-only and terminate public TLS at a reverse proxy. The proxy must preserve the external host/scheme semantics used by OAuth metadata and callbacks.
Minimal .env settings:
SERVERFS_OAUTH_ENABLED=trueSERVERFS_OAUTH_RESOURCE_URL=https://mcp.example.com/mcpSERVERFS_AUTH_PUBLIC_URL=https://auth.example.comSERVERFS_OAUTH_ISSUER_URL=https://auth.example.comSERVERFS_OAUTH_REQUIRED_SCOPES=serverfs:accessSERVERFS_OAUTH_INTROSPECTION_SECRET=<long-random-secret>SERVERFS_AUTH_ALLOWED_PRINCIPALS=<provider:stable-subject,...>SERVERFS_OAUTH_ISSUER_URL must exactly equal SERVERFS_AUTH_PUBLIC_URL. Public URLs must use HTTPS outside explicit loopback tests.
The auth state directory must exist before Compose starts and be writable by the configured non-root SERVERFS_UID:GID:
mkdir -p .serverfs-auth-statechmod 700 .serverfs-auth-stateDefault-deny admission
Section titled “Default-deny admission”SERVERFS_AUTH_ALLOWED_PRINCIPALS accepts exact comma-separated stable identities only. Wildcards are rejected.
| Provider | Principal | Stable source |
|---|---|---|
| GitHub | github:<numeric-id> |
immutable numeric user id |
| DingTalk | dingtalk:<unionId> |
Open Platform unionId |
| Feishu | feishu:<open_id> |
app-scoped open_id |
wechat:<unionid> |
Open Platform unionid |
Display names, email addresses, mobile numbers and nicknames are not authorization keys.
Provider callbacks
Section titled “Provider callbacks”Enable only the providers you need; each enabled provider requires a client ID and secret.
External live-acceptance prerequisite: ServerFS cannot ship real GitHub, Feishu/Lark, DingTalk or WeChat Open Platform applications or operator credentials. v0.16.0 validates all four adapters with deterministic protocol/security tests; before production use, create the relevant provider application, register the exact callback below, keep real credentials outside Git, and complete a real browser login for each provider you enable.
GitHub OAuth App
Section titled “GitHub OAuth App”Callback:
https://auth.example.com/oauth/provider/github/callbackSERVERFS_OAUTH_GITHUB_ENABLED=trueSERVERFS_OAUTH_GITHUB_CLIENT_ID=<client-id>SERVERFS_OAUTH_GITHUB_CLIENT_SECRET=<client-secret>ServerFS deliberately requests no repository scope and authorizes by immutable numeric GitHub user ID.
Feishu / Lark
Section titled “Feishu / Lark”Callback:
https://auth.example.com/oauth/provider/feishu/callbackSERVERFS_OAUTH_FEISHU_ENABLED=trueSERVERFS_OAUTH_FEISHU_CLIENT_ID=<app-id>SERVERFS_OAUTH_FEISHU_CLIENT_SECRET=<app-secret>The flow requests contact:user.base:readonly, omits offline_access, and authorizes by open_id.
DingTalk
Section titled “DingTalk”Callback:
https://auth.example.com/oauth/provider/dingtalk/callbackSERVERFS_OAUTH_DINGTALK_ENABLED=trueSERVERFS_OAUTH_DINGTALK_CLIENT_ID=<client-id>SERVERFS_OAUTH_DINGTALK_CLIENT_SECRET=<client-secret>The flow requests openid, consumes DingTalk’s authCode callback field, and authorizes by unionId.
WeChat Open Platform
Section titled “WeChat Open Platform”This is WeChat Open Platform Website Application QR login, not WeCom/企业微信 and not Official Account webpage authorization.
Callback:
https://auth.example.com/oauth/provider/wechat/callbackSERVERFS_OAUTH_WECHAT_ENABLED=trueSERVERFS_OAUTH_WECHAT_CLIENT_ID=<app-id>SERVERFS_OAUTH_WECHAT_CLIENT_SECRET=<app-secret>The flow uses snsapi_login, verifies user-info openid against the token subject, and authorizes by Open Platform unionid.
Provider access/refresh tokens remain provider-local ephemeral material; ServerFS issues its own opaque resource-bound credentials after admission.
Client registration
Section titled “Client registration”ServerFS supports three client-identification paths:
- Pre-registration through the optional read-only
SERVERFS_AUTH_CLIENTS_FILE. - CIMD (Client ID Metadata Documents) — preferred for standards-current clients; fetched with strict HTTPS, public-address and TLS checks.
- DCR (Dynamic Client Registration) — compatibility-only and disabled by default. Enable only for clients that require it:
SERVERFS_AUTH_DCR_ENABLED=truePublic clients use Authorization Code + PKCE S256. Prefer CIMD or explicit pre-registration over DCR when possible.
Token and resource model
Section titled “Token and resource model”ServerFS issues opaque access/refresh tokens and stores only digests, not raw credential values.
- tokens are bound to
SERVERFS_OAUTH_RESOURCE_URL; - private introspection validates issuer, resource, client, subject, scopes and expiry;
- provider-native tokens cannot authenticate directly to MCP;
- authorization codes/provider state are single-use;
- refresh tokens rotate and replay fails closed;
- OAuth scope defaults to
serverfs:access; workdir read/write/Agent policy remains separate.
OpenAI Secure MCP Tunnel
Section titled “OpenAI Secure MCP Tunnel”OAuth-disabled Tunnel operation remains supported with the repository-tested tunnel-client v0.0.16.
If serverfs-auth uses a separate origin, explicitly trust that exact origin:
MCP_OAUTH_TRUSTED_ORIGINS=https://auth.example.comKnown upstream limitation
Section titled “Known upstream limitation”As verified on 2026-10-19, OpenAI tunnel-client issue #78 remains open: ChatGPT may replace the canonical RFC 9728 resource with its hosted Tunnel resource during authorization.
ServerFS does not weaken audience/resource validation, accept wildcard resources, or hard-code undocumented OpenAI gateway hosts to work around this.
Therefore direct/generic OAuth and OAuth-disabled Tunnel are supported ServerFS paths; OAuth-through-Tunnel still requires live acceptance for the OpenAI behavior in use and may be blocked by the upstream resource rewrite.
Proxy routing
Section titled “Proxy routing”Only serverfs-auth needs provider/CIMD egress:
SERVERFS_AUTH_USE_PROXY=trueSERVERFS_PROXY_HOST=proxy.example.internalSERVERFS_PROXY_PORT=7890This is independent from Tunnel, Agent and Jev proxy switches.
Backup and restore
Section titled “Backup and restore”SERVERFS_AUTH_STATE_HOST_DIR contains SQLite authorization state. Treat backups as security-sensitive even though raw tokens are not persisted.
For a consistent backup:
- stop only
serverfs-authduring a maintenance window; - copy the whole auth-state directory with permissions preserved;
- start
serverfs-auth; - verify
/healthz.
Restore only while serverfs-auth is stopped; restore the state directory as a unit rather than mixing SQLite files from different snapshots.
Revocation and secret rotation
Section titled “Revocation and secret rotation”/revokerevokes ServerFS-issued credentials when the client has the credential.- Removing a principal prevents future admission but does not itself revoke already issued access tokens.
- Provider secret rotation requires recreating/restarting
serverfs-authonly. SERVERFS_OAUTH_INTROSPECTION_SECRETis shared only byserverfs-mcpandserverfs-auth; rotate it atomically across both services. A mismatch fails closed.- Emergency instance-wide invalidation can replace the OAuth state directory while
serverfs-authis stopped. This intentionally invalidates all issued ServerFS credentials and loses dynamic registration/state, so it is a deliberate recovery action.
Troubleshooting
Section titled “Troubleshooting”401 before login — expected when OAuth is enabled without a valid ServerFS bearer. Verify the protected-resource metadata and canonical SERVERFS_OAUTH_RESOURCE_URL.
Auth server will not start — verify at least one provider is enabled with credentials, public/issuer URLs match, HTTPS is used outside loopback, the introspection secret is set, and the state directory is writable.
Login succeeds but access is denied — the stable provider subject is not in the exact allowlist. Do not substitute email/display name.
Resource mismatch — fix the canonical external URL/client request; do not broaden the audience. For Tunnel, also check the known resource-rewrite limitation above.
Provider timeout — enable SERVERFS_AUTH_USE_PROXY when required; do not give serverfs-mcp general Internet egress.
Tunnel cannot discover a separate auth origin — set MCP_OAUTH_TRUSTED_ORIGINS to the exact authorization-server origin, never a wildcard.
Native platforms
Section titled “Native platforms”Windows and macOS native stdio continue to require no OAuth configuration. v0.16 shared OAuth modules are import-safe on both platforms, but OAuth deployment itself remains the Linux remote-HTTP shape.
The repository’s canonical operator contract is docs/oauth.md. The versioned environment-variable surface remains .env.example.