[OPIK-6666] [BE] feat: MCP OAuth 2.1 foundations (PR 1/7) — schema, config, domain, persistence, service#6979
Conversation
…main, persistence, service PR 1 of 7 splitting #6900 (MCP OAuth 2.1 Authorization Server) into shippable chunks per review. This is the shared inner foundation that every OAuth resource depends on — no REST surface, no app wiring, no behavior change. Includes: - Liquibase migration 000080: mcp_oauth_clients/_codes/_tokens on db-app-state - McpOAuthConfig (env-gated by MCP_OAUTH_ENABLED) + config.yml mcpOAuth block + OpikConfiguration.mcpOAuth field - Domain models + token utils (clients, codes, tokens, ValidatedToken, etc.) - JDBI DAOs (clients/codes/tokens) with atomic mark-used, rotation, revoke-family - OAuthClientStrategy seam + DbOAuthClientStrategy + OAuthClientService - McpOAuthService — code exchange (PKCE), refresh rotation w/ reuse detection, revoke, validate Deferred to PR 6 (scrub job): the MutableJobConfiguration switch and the config.yml jobs.mcpOAuthScrubCron entry, which only the scrub job needs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
andrescrz
left a comment
There was a problem hiding this comment.
Many of the comments are mentioned once, but they apply to other parts of the code base.
I still recommend breaking this down into smaller chunks, to progress and deliver faster.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…IdGenerator Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ct mapper Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on the record Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…apper Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kenResponse Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
code_challenge is NOT NULL in the schema and PKCE is mandatory, so the value cannot be null here; annotate the params @nonnull instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dation Defaults were duplicated between McpOAuthConfig and config.yml. Remove the Java-side defaults (durations, rate limit, enabled) so they live only in YAML, and enforce presence via @NotNull / @min so a missing value fails fast at boot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
andrescrz
left a comment
There was a problem hiding this comment.
The PR mostly LGTM now.
There's still one blocking comment in the MySQL table definitions regarding the type of client_id fields. We cannot change the migrations one pushed, I'd rather it address it now.
The rest of comments are optional. Many of them are nice to have, so I recommend tackling them as you have to send a new revision anyway.
Many comments are reflected once, but applies to multiple places (like record validations).
Let's do a final iteration and this should be good to go.
Replace the hand-written builders in McpOAuthService with a MapStruct mapper (toCode/toToken/toRotatedToken), building each record complete in one pass to respect the @nonnull record guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge revoke and revokeFamily into a single revokeBy query that swaps the WHERE column via @define; keep both as thin default delegators passing a hardcoded column literal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clients-table primary key follows the table-local id convention; the client_id foreign keys in mcp_oauth_codes/mcp_oauth_tokens are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement HasValue with @jsonvalue values and register an AbstractEnumColumnMapper so persistence/serialization no longer rely on the enum constant name. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
andrescrz
left a comment
There was a problem hiding this comment.
LGTM.
This was along and complex piece of work, with many moving pieces. But the implementation looks clean and solid.
Well done & thanks for putting it together!
Details
PR 1 of 7 splitting #6900 (MCP OAuth 2.1 Authorization Server) into smaller, shippable chunks per review feedback — working inner-foundations outward, schema first, deployment last. #6900 stays open as the full reference.
This PR is the shared inner foundation every OAuth resource depends on: schema + config + domain types + persistence + token-lifecycle service. It has no REST surface, no guicey bundle, no
OpikApplicationwiring — nothing is exposed and there is zero behavior change. The feature is gated byMCP_OAUTH_ENABLED(defaultsfalse) and stays inert until the full chain lands.What's in the box
000080—mcp_oauth_clients/_codes/_tokensondb-app-state(no seeded clients; all MCP hosts register via DCR in a later chunk)McpOAuthConfig(env-gated byMCP_OAUTH_ENABLED) +config.ymlmcpOAuth:block +OpikConfiguration.mcpOAuthfieldOAuthConstants,McpOAuthClient,McpOAuthCode,McpOAuthToken,McpOAuthTokens(token utils),ValidatedToken,CreateOAuthCodeCommand,TokenResponse,ClientRegistrationRequestMcpOAuthClientDAO/McpOAuthCodeDAO/McpOAuthTokenDAO— atomic mark-used, refresh rotation, revoke-familyOAuthClientStrategyseam +DbOAuthClientStrategy+OAuthClientService;McpOAuthService— code exchange (PKCE/S256 verify), refresh rotation w/ reuse detection + grace, revoke, validateDeliberately deferred
MutableJobConfigurationswitch onOpikConfigurationand theconfig.ymljobs.mcpOAuthScrubCronentry → move with the scrub job (PR 6), which is the only thing that needs them.McpOAuthBundlekill-switch,AuthFilter/AuthServiceintegration, tests, and helm/nginx deployment → later chunks.The split (7 PRs)
/.well-known/oauth-authorization-server) + guicey bundle wiring/oauth/authorize) +AuthServiceworkspace resolution/oauth/token,/oauth/revoke)/oauth/register, RFC 7591)AuthFilter) + validator (/opik/auth-oauth) + scrub job + E2EChange checklist
Neither applies: this chunk has no REST surface and zero behavior change — nothing is user-facing and there is nothing to document until the endpoints land in later PRs.
Testing
mvn -o compile— green. Only out-of-set dependencies (IdGenerator,SetFlatArgumentFactory) already exist onmain.Issues
Documentation
N/A for this chunk — no user-facing surface. End-user OAuth setup/usage docs land with the deployment PR (7/7).
🤖 Generated with Claude Code