One OAuth engine: OauthClient in druks.services, MCP rewired onto it - #285
Merged
Conversation
czpython
force-pushed
the
oauth-client-primitive
branch
3 times, most recently
from
August 19, 2026 07:12
cf656a7 to
0522dd5
Compare
The MCP subsystem's OAuth 2.0 authorization-code + PKCE flow — the token cache, the single-refresher lock election, and refresh-token rotation — generalizes into a reusable primitive for any provider with fixed endpoints and a pre-registered client. The engine exists exactly once: mcp/oauth.py keeps its discovery/registration head and its model writes, and delegates the PKCE/state/consent tail, the code exchange, and the whole mint path. Rotation now commits independently of the enclosing transaction: the provider invalidates the old refresh token the moment it rotates, so save_refresh_token writes on its own session before the cache fills — a step that rolls back later can no longer brick the grant. load_refresh_token runs under the refresh lock and re-reads past the identity map, observing rotations other processes committed. MCP's Redis key strings stay byte-identical, so a rolling deploy's old and new processes elect one refresher per grant.
czpython
force-pushed
the
oauth-client-primitive
branch
from
August 19, 2026 07:15
0522dd5 to
18f4304
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
OauthClient, exported fromdruks.services: a reusable OAuth 2.0 authorization-code + PKCE flow with rotation-safe refresh, for service providers with fixed endpoints and a pre-registered client. The MCP subsystem's OAuth connect and mint now run on this engine, so the security-sensitive part — the Redis token cache, the single-refresher lock election (SET NX + poll), and refresh-token rotation persisted before the cache fill — exists exactly once.mcp/oauth.pykeeps its discovery (RFC 9728/8414) and dynamic-registration (RFC 7591) head and its model writes, and contains no cache/lock/rotation logic of its own.Contracts
save_refresh_token(token)must have committed before the engine fills the cache — a consumer minting inside a durable step that later rolls back can no longer brick the grant. MCP's wrapper writes on its own session and keeps the enclosing transaction's copy true.load_refresh_token()runs under the refresh lock and must observe rotations other processes committed; a naive re-select can return identity-mapped stale attributes, so MCP re-reads withpopulate_existing.extra_authorize_params/extra_token_paramscarry audience binding (MCP's RFC 8707resource) through the authorize query and both token bodies;basic_authswitches HTTP Basic vs body credentials on exchange and refresh.complete_connectrejects a token response withoutrefresh_token— offline grants only; single-use state via Redis GETDEL, TTL'd. Completion needs onlyprovider: the begun flow's client identity rides the stashed state.mcp:oauth:connect:/mcp:oauth:access_token:/mcp:oauth:refresh_lock:), derived from the provider namespace, so a rolling deploy cannot elect two refreshers for one grant across old and new processes.Also
Service.get_oauth_client(). A service that declaresauthorization_endpoint/token_endpoint/basic_authon the class hands back the connected identity as a configuredOauthClient, keyed by the service name; itsSettingsmust declareclient_idandclient_secretfields (loudTypeErrorat class definition otherwise). Scopes are per authorization, not per client —begin_connect(scopes=...)asks for each flow's own, and the grant keeps what the user approved, so one registered app serves consumers with different scopes.OauthExchangeError/OauthRefreshErrorindruks.services; MCP translates them into its own operator-facing errors, unchanged on the wire.druks.secrets.fields'EncryptedJsonField/SecretsMappingjoin the stable-import table for storing grants ciphertext at rest.basic_authswitches auth style on both exchange and refresh.Existing MCP tests pass unchanged, except the author-surface pin, which gains the three new exported names.
Verification
uv run ruff check backend/uv run ruff format --check backendcleanuv run pytest backend/— 1257 passed🤖 Generated with Claude Code