Skip to content

[OPIK-8252] [BE] feat: Report expires_at from MCP OAuth token introspection - #8153

Open
awkoy wants to merge 2 commits into
mainfrom
awkoy/OPIK-8252/introspect-expires-at
Open

[OPIK-8252] [BE] feat: Report expires_at from MCP OAuth token introspection#8153
awkoy wants to merge 2 commits into
mainfrom
awkoy/OPIK-8252/introspect-expires-at

Conversation

@awkoy

@awkoy awkoy commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Details

opik-mcp, the resource server for the hosted MCP connector, now validates every OAuth access token against POST /opik/auth-oauth and caches a "valid" answer for a short TTL (comet-ml/opik-mcp#182). Without knowing when the token actually expires it can only guess, and a token that dies inside that window is still forwarded once. ValidatedToken now carries the token row's expiresAt, serialized as ISO-8601 expires_at, so the resource server caches until the real expiry and that window disappears.

Purely additive: the field is NON_NULL-included, every other construction site leaves it unset and every consumer (AuthFilter, AuthService.authorizeOAuth) reads only userName / workspaceName. Resource servers that predate the field ignore it. The OpenAPI spec and generated SDKs pick the field up through the scheduled "Update automatically OpenAPI spec and Fern code" workflow, as with previous DTO changes in this package.

Change checklist

  • User facing
  • Documentation update

Issues

  • Resolves #
  • OPIK-8252

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Fable 5.1
  • Scope: the expiresAt field, its population in McpOAuthService.validateAccessToken, and the two resource-test changes
  • Human verification: reviewed the diff and the test run locally; companion opik-mcp change exercised manually against dev

Testing

  • mvn -Dtest=OAuthValidateTokenResourceTest test (JDK 25) → 11 run, 0 failures, including the new reportsExpiresAtOnTheWire asserting "expires_at":"2026-09-04T10:15:30.123Z" on the wire.
  • mvn spotless:check clean.
  • Not run: the full backend suite (needs the MySQL/ClickHouse/Redis container stack). expires_in on the token endpoint and expires_at here derive from the same accessTokenTtl in the same transaction, so they agree by construction.

Documentation

None: the field is consumed by opik-mcp, not by end users.

🤖 Generated with Claude Code

…ection

opik-mcp, the resource server for the hosted MCP connector, now validates
every OAuth access token against POST /opik/auth-oauth and caches a "valid"
answer for a short TTL. Without knowing when the token actually expires it
can only guess, and a token that dies inside that window is still forwarded
once. ValidatedToken now carries the token row's expiresAt (ISO-8601 on the
wire as expires_at), so the resource server caches until the real expiry and
that window disappears.

Purely additive: resource servers that predate the field ignore it. The
OpenAPI spec and generated SDKs pick the field up through the usual
automated update.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQuKKW95kFTeHAW3jVyTj
@awkoy
awkoy requested a review from a team as a code owner September 4, 2026 10:25
@github-actions github-actions Bot added java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. 🟢 size/S labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 4.70s
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting 1.84s
Total (2 ran) 6.54s
⏭️ 42 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 14

 30 files   - 21   30 suites   - 21   4m 26s ⏱️ +23s
282 tests  - 60  282 ✅  - 58  0 💤  - 2  0 ❌ ±0 
282 runs   - 38  282 ✅  - 36  0 💤  - 2  0 ❌ ±0 

Results for commit 5759852. ± Comparison against base commit d53961c.

This pull request removes 263 and adds 203 tests. Note that renamed tests count towards both.
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$FailureTests ‑ shouldAckAndRemoveNonRetryableFailures(String, RuntimeException)[1]
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$FailureTests ‑ shouldAckAndRemoveNonRetryableFailures(String, RuntimeException)[2]
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$FailureTests ‑ shouldAckAndRemoveNonRetryableFailures(String, RuntimeException)[3]
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$FailureTests ‑ shouldAckAndRemoveNonRetryableFailures(String, RuntimeException)[4]
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$FailureTests ‑ shouldContinueProcessingAfterFailedMessages
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$FailureTests ‑ shouldRecoverFromNoGroupOnReadAndContinueProcessing
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$LifecycleTests ‑ shouldHandleExistingConsumerGroup
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$LifecycleTests ‑ shouldRemoveConsumerOnStop
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$RetryTests ‑ shouldAckAndRemoveAfterMaxRetries
com.comet.opik.api.resources.v1.events.BaseRedisSubscriberTest$RetryTests ‑ shouldHandleMixedSuccessRetryableAndNonRetryableMessagesInSameBatch
…
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsByNameSearch
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsNoFiltersReturnsAll
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsWithDefaultParameters
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsWithEmptyResult
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsWithFilters(Function, Function)[1]
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsWithFilters(Function, Function)[2]
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsWithFilters(Function, Function)[3]
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboardsWithPagination
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboards__whenSortingByValidFields__thenReturnDashboardsSorted(Comparator, SortingField)[1]
com.comet.opik.api.resources.v1.priv.DashboardsResourceFindProjectDashboardsTest ‑ findProjectDashboards__whenSortingByValidFields__thenReturnDashboardsSorted(Comparator, SortingField)[2]
…

♻️ This comment has been updated with latest results.

.workspaceId(row.workspaceId())
.workspaceName(row.workspaceName())
.resource(row.resource())
.expiresAt(row.expiresAt())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated clients omit token expiry

expiresAt is now emitted in the backend response, but the checked-in OpenAPI ValidatedToken schema isn't updated, so Fern-generated models lack expires_at and typed SDK consumers can't access it — should we update the API definition and regenerate the SDK artifacts as part of this change?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/domain/mcpoauth/McpOAuthService.java
around lines 186-186, the validateAccessToken response now includes expiresAt without
updating the checked-in OpenAPI ValidatedToken schema. Update the API definition to
declare the expiry field, then regenerate the Fern TypeScript and Python SDK artifacts
so they expose expires_at and the TypeScript serializer maps it correctly. Include the
schema and generated-client changes in the same release change and run the relevant
generation or validation checks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 5759852 addressed this comment by adding expires_at to both checked-in OpenAPI schemas as an ISO date-time field. Generated SDK artifact changes are not present in the diff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added expires_at (string, date-time) to ValidatedToken in both checked-in OpenAPI specs in 5759852. SDK regeneration is left to the scheduled Fern workflow, as with previous DTO changes in this package.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, noted—the specs now include expires_at, and SDK regeneration follows the scheduled Fern workflow. I’ll save this context to memory once the PR is merged.

.workspaceId(row.workspaceId())
.workspaceName(row.workspaceName())
.resource(row.resource())
.expiresAt(row.expiresAt())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persisted expiry propagation lacks integration coverage

The expires_at assignment is only covered by mocked resource tests, so a regression here would go undetected — should we add real backend integration coverage (per AGENTS.md Tests and Organization and apps/opik-backend/AGENTS.md Tests) that mints/seeds a real MySQL token, POSTs /opik/auth-oauth through the real McpOAuthService, and asserts the response expires_at matches the stored row value?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/domain/mcpoauth/McpOAuthService.java
around lines 186-186, add backend integration coverage for the `validateAccessToken`
response mapping that now includes `expiresAt`. Seed or mint a real MySQL token, POST to
`/opik/auth-oauth` through the real `McpOAuthService`, and assert that the response's
`expires_at` exactly matches the stored token row value; do not rely on the existing
mocked resource tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 5759852 addressed this comment by adding a real MySQL-backed integration test that mints a token, calls /opik/auth-oauth, and verifies expires_at matches the persisted row expiry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added OAuthValidateTokenIntegrationTest in 5759852: real register → consent → code exchange against MySQL, then POST /opik/auth-oauth, asserting expires_at equals the persisted token row's expiry.

Comment on lines +116 to +121
var validated = ValidatedToken.builder()
.userName(RandomStringUtils.secure().nextAlphanumeric(10))
.workspaceId(UUID.randomUUID().toString())
.workspaceName(RandomStringUtils.secure().nextAlphanumeric(10))
.resource("http://localhost/api/v1/mcp")
.expiresAt(expiresAt)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New fixture bypasses Podam convention

The new wire-format and modified valid-token fixtures manually populate ValidatedToken identity fields; should we build them with PodamFactoryUtils.newPodamFactory() and override only expiresAt plus assertion-required fields, as .agents/skills/opik-backend/testing.md requires?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/test/java/com/comet/opik/api/resources/oauth/OAuthValidateTokenResourceTest.java
around lines 116-121, refactor the `reportsExpiresAtOnTheWire` fixture to create
`ValidatedToken` through `PodamFactoryUtils.newPodamFactory()` or its utilities,
overriding only `expiresAt` and any fields genuinely required by the assertion. Apply
the same fixture convention to the `acceptsValidToken` setup around lines 95-101,
retaining only scenario-relevant overrides and removing manual random generation of
identity fields.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 5759852 addressed this comment by using PodamFactoryUtils.newPodamFactory() for both fixtures and overriding only expiresAt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched both ValidatedToken fixtures to PodamFactoryUtils.newPodamFactory().manufacturePojo(...) with only expiresAt overridden, in 5759852.

.resource("http://localhost/api/v1/mcp")
.expiresAt(expiresAt)
.build();
when(mcpOAuthService.validateAccessToken(ACCESS_TOKEN)).thenReturn(Optional.of(validated));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token expiry mapping lacks regression coverage

The new wire-format test mocks mcpOAuthService.validateAccessToken, so it only verifies Jackson serialization of a hand-built DTO and can pass if McpOAuthService.validateAccessToken omits or mis-maps the persisted token row’s expiresAt — should we add a service/integration regression test asserting ValidatedToken.expiresAt from a token row, or use the real service path here?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/api/resources/oauth/OAuthValidateTokenResourceTest.java`
around line 123, update `reportsExpiresAtOnTheWire` because mocking
`mcpOAuthService.validateAccessToken` bypasses the expiry mapping under test. Exercise
the real service path, or add a service/integration regression test that persists a
token with a known `expiresAt` and asserts the returned `ValidatedToken.expiresAt`
before verifying the serialized `expires_at` field.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 5759852 addressed this comment by adding an integration regression test that exercises the real OAuth flow, reads the persisted token expiry, and asserts ValidatedToken.expiresAt matches it. The service now maps row.expiresAt() into the DTO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered by the new OAuthValidateTokenIntegrationTest in 5759852, which reads the value through the real validateAccessToken path rather than a mock.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

TS SDK E2E Tests - Node 22

317 tests   - 1   315 ✅ +2   17m 38s ⏱️ + 3m 24s
 38 suites ±0     2 💤  - 2 
  1 files   ±0     0 ❌  - 1 

Results for commit 5759852. ± Comparison against base commit d53961c.

This pull request removes 1 test.
tests/integration/runner/runner.test.ts ‑ Runner Integration Tests

♻️ This comment has been updated with latest results.

… schema

Addresses the review on the previous commit. The mocked resource test only
proved the wire format of a hand-built DTO; OAuthValidateTokenIntegrationTest
now drives the real register → consent → code-exchange flow against MySQL and
asserts that POST /opik/auth-oauth reports exactly the persisted row's
expiry (plus resource and workspace). ValidatedToken fixtures in the resource
test come from Podam like the rest of the suite. The checked-in OpenAPI
schemas gain expires_at (string, date-time) so generated clients stop
lagging the backend.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@awkoy
awkoy requested a review from a team as a code owner September 4, 2026 13:43
@github-actions github-actions Bot added documentation Improvements or additions to documentation 🟡 size/M and removed 🟢 size/S labels Sep 4, 2026
@CometActions

Copy link
Copy Markdown
Collaborator

Already covered by a test in this PR.

Additive expires_at on the introspection response, and the PR ships the test for it: OAuthValidateTokenIntegrationTest mints a token through register → consent → code exchange and asserts POST /opik/auth-oauth returns the persisted token row's expiry (plus resource and workspace), while OAuthValidateTokenResourceTest pins the wire format to a snake_case ISO-8601 expires_at — an epoch number or a camelCase key fails it. Either would fail if the mapping in McpOAuthService regressed, so there is nothing for a Playwright spec to add: the endpoint is off on a fresh OSS install (MCP_OAUTH_ENABLED defaults to false), no Opik page reads it, and the consumer that caches on the value is opik-mcp. Worth noting for QA rather than for you: the coverage taxonomy has no MCP-OAuth area at all, so this surface has no capability key — that is taxonomy work on our side, not a gap in this PR.

also touches Backend (Java API / internal)

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://opik-preview-01a06caa-240f-74f4-9736-abe789af79df.docs.buildwithfern.com/docs/opik

No broken links found

Unverified links (timeout / rate-limited / server error — not failing the check)

https://aistudio.google.com/apikey (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://beeai.dev/ (503)
↳ on page: /docs/opik/integrations/beeai
https://chat.deepseek.com/sign_up (403)
↳ on page: /docs/opik/integrations/deepseek
https://console.cloud.google.com/iam-admin/iam (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/roles (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/serviceaccounts (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.mistral.ai/api-keys/ (timeout)
↳ on page: /docs/opik/integrations/mistral
https://console.x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok
https://docs.predibase.com/integrations/comet (403)
↳ on page: /docs/opik/integrations/predibase
https://portal.azure.com/ (403)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok


📌 Results for commit 6f883db

Comment on lines +108 to +111
var validated = response.readEntity(ValidatedToken.class);
assertThat(validated.expiresAt()).isEqualTo(persistedExpiry);
assertThat(validated.resource()).isEqualTo(RESOURCE_URI);
assertThat(validated.workspaceName()).isEqualTo(minted.tokens().workspaceName());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identity regressions pass integration tests

The integration test verifies only expiresAt, resource, and workspaceName from ValidatedToken, so regressions in the other identity fields still pass unnoticed — should we compare the complete returned object with an expected ValidatedToken built from the minted artifacts?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/domain/mcpoauth/OAuthValidateTokenIntegrationTest.java`
around lines 108-111, update `introspectionReportsPersistedExpiry` so it does not verify
only `expiresAt`, `resource`, and `workspaceName`. Build an expected `ValidatedToken`
from the minted artifacts and persisted expiry, then compare the complete returned
object, or explicitly assert every meaningful field, including the user identity fields.

@LifeXplorer LifeXplorer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend documentation Improvements or additions to documentation java Pull requests that update Java code 🟡 size/M tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants