Skip to content

feat: capture upstream identity from ID tokens on remote session grants - #6104

Merged
daviddanialy merged 3 commits into
mainfrom
aim-203-id-token-enricher
Sep 9, 2026
Merged

daviddanialy merged 3 commits into
mainfrom
aim-203-id-token-enricher

Conversation

@daviddanialy

@daviddanialy daviddanialy commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

AIM-203, part 2 of 2. Stacked on #6100; merge that first.

Summary

  • Authorize requests carry a nonce. When the token response has an id_token, it is verified (signature via the issuer's jwks_uri through the shared jwks.KeyResolver, iss, aud/azp, exp, nonce; 5s budget per verification, fetch budgets charged per issuer row). Accepted algorithms are the intersection of the shared allowlist and the issuer's id_token_signing_alg_values_supported when it advertises any, and the signing key is picked by the token's alg among keys sharing a kid (declared alg first, else key type and curve).
  • A verified token fills the new remote_sessions identity columns (upstream_subject, upstream_email, upstream_display_name, identity_source). enrichment keeps the full ID-token claim set (so picture, sid, auth_time, and email_verified are retained without a column) and, from the token response, only an allowlist of known provider metadata members (Notion workspace fields, Slack team and bot ids, PostHog region, and similar), with credential-shaped members stripped at any depth as a second layer. A document over 16 KiB is dropped with a warning, and the stored merge is capped at the same size: when a merge would exceed it, the newest document replaces it. The raw ID token is never stored or logged, and provider-controlled text in log lines is capped.
  • New grant: overwrites identity, clears validation_*. Refresh: tokens persist under the lease first; identity is verified and written afterwards, on a context detached from the request, with a CAS on the token write. The CAS also drops the restatement when another refresh rotated the tokens meanwhile; that is deliberate best-effort, since nothing on the row distinguishes a rotation from a reconnect and a retry could stamp a previous grant's identity onto a new one. The stored identity is untouched in that case and the next ID token restates it. Omitted claims keep their stored value in both the columns and the document (the id_token and token_response members merge key by key). A refresh ID token naming a different sub is rejected like any other invalid token and the identity verified at the exchange stands. Any soft-delete clears identity and enrichment.
  • Consent card shows "Connected as {email or name}". IDTokenVerifier is an interface with a no-op implementation, so both services always hold a usable verifier; the API server and the keepalive worker wire the JWKS-backed one. Verification failures never fail a login or refresh; they log at warn.
  • Tests: claim helpers, extras allowlist and claim denylist, enrichment document, size cap and merged cap under repeated refreshes, algorithm policy and shared-kid key selection, overlapping refreshes, template escaping, and end-to-end logins against a TLS key-set server covering capture, nine rejection cases, refresh restatement (no token, stranger twice, partial claims, renamed), and two revocation paths.

Motivation

The card could say a grant existed but not whose. Issuers with OpenID Connect already say so, signed, at the exchange; a quarter of prod sessions in the last 30 days were granted with openid. Verifying rather than decoding matters because these columns will feed session-to-agent grants.

Not in this PR: a verification-outcome metric and reactive metadata refresh on unknown kid (AIM-210).

🤖 Generated with Claude Code

@daviddanialy
daviddanialy requested a review from a team as a code owner September 4, 2026 21:56
@daviddanialy daviddanialy added the enhancement New feature or request label Sep 4, 2026
@linear-code

linear-code Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

AIM-203

@changeset-bot

changeset-bot Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6a328a5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Running ultrareview automatically — This PR adds OpenID Connect ID-token verification and identity capture across remote-session grant/refresh flows, with new database columns, SQL, key handling, and consent UI changes — a subtle bug could break authentication or misattribute sessions.. I'll post findings when complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

Ultrareview completed in 9m 4s

3 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="server/internal/remotesessions/queries.sql">

<violation number="1" location="server/internal/remotesessions/queries.sql:899">
P2: According to linked Linear issue AIM-203, `validation_status` must be `NOT NULL DEFAULT 'unknown'`. This conflict branch writes `NULL`, so under that required schema every re-auth upsert of an existing session violates the constraint; write `'unknown'` instead.</violation>
</file>

<file name="server/internal/remotesessions/tokenservice.go">

<violation number="1" location="server/internal/remotesessions/tokenservice.go:686">
P1: According to linked Linear issue AIM-203, refresh verification must require the same `sub` as the stored identity. For a legacy row with a NULL `upstream_subject`, this passes an empty expectation and skips the check, allowing a valid refresh ID token for another subject to populate the session; require a stored non-empty subject before accepting a refresh ID token.</violation>
</file>

<file name="server/internal/remotesessions/identity.go">

<violation number="1" location="server/internal/remotesessions/identity.go:204">
P2: According to linked Linear issue AIM-203, ID-token validation must check `iss` against the grant's issuer; OIDC Core requires that match to be exact. Compare the issuer strings exactly, or compare values canonicalized once at grant configuration, rather than reusing the discovery helper's trailing-slash tolerance.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread server/internal/remotesessions/queries.sql
Comment thread server/internal/remotesessions/tokenservice.go Outdated
Comment thread server/internal/remotesessions/tokenservice.go Outdated
Comment thread server/internal/remotesessions/queries.sql
Comment thread server/internal/remotesessions/queries.sql
Comment thread server/internal/remotesessions/token_response.go Outdated
Comment thread server/internal/remotesessions/identity.go Outdated
Comment thread server/internal/remotesessions/identity.go
Comment thread server/internal/mcp/consent_template.html Outdated
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch 2 times, most recently from 07cebfb to d6c6b10 Compare September 4, 2026 22:23
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from a995d37 to dc45ad5 Compare September 4, 2026 23:33
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from d6c6b10 to f8792c3 Compare September 4, 2026 23:35
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from dc45ad5 to 29da3da Compare September 8, 2026 16:58
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from f8792c3 to 37b1a5b Compare September 8, 2026 17:01
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from 29da3da to 5cd0701 Compare September 8, 2026 17:35
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from 37b1a5b to d9448e7 Compare September 8, 2026 17:37
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from 5cd0701 to 8f8a7e2 Compare September 8, 2026 18:24
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from d9448e7 to a4bbc78 Compare September 8, 2026 18:24
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from 8f8a7e2 to c1428ae Compare September 8, 2026 18:43
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from a4bbc78 to 20ea3ee Compare September 8, 2026 18:43
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from c1428ae to 7fc24af Compare September 8, 2026 18:49
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from 20ea3ee to facecd4 Compare September 8, 2026 18:54
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from 7fc24af to cf60d1e Compare September 8, 2026 19:08
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from facecd4 to 3d49da2 Compare September 8, 2026 19:23
@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from cf60d1e to 94e1dd3 Compare September 8, 2026 19:52
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from 3d49da2 to cd7ca8c Compare September 8, 2026 19:52
@daviddanialy

Copy link
Copy Markdown
Contributor Author

Testing summary:

Reviews. Four independent passes (security, concurrency and ordering, data correctness and query hygiene, code quality and conventions). No blockers; every should-fix is applied: subject-mismatch policy, cancel-free identity write, enrichment size cap and key-by-key merge, nested credential stripping, log truncation, verifier interface with a no-op default.

Unit. Claim helpers, credential-name denylist, enrichment document (nested strip, large-integer preservation, 16 KiB cap), token-response extras, consent template escaping.

End to end, against a TLS key-set server per test: identity captured and surfaced on the card; nine rejection cases (wrong nonce, audience, issuer, expiry, missing subject, multi-audience without azp, wrong azp, unpublished key, verifier not configured) each storing the session without identity; refresh with no ID token keeps identity; a stranger's token is rejected twice in a row; a partial-claims refresh keeps omitted values in both columns and document; a restating refresh replaces them; two revocation paths clear identity and enrichment while keeping credentials.

Suites. remotesessions, mcp, and background: 2,242 tests. Full server suite: 15,436 tests (one telemetry contention flake, passes in isolation). Server lint clean.

@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from c64d36e to ce1d9c9 Compare September 8, 2026 21:53
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from a5c7d1a to c170945 Compare September 8, 2026 21:58
@blacksmith-sh

This comment has been minimized.

@daviddanialy
daviddanialy force-pushed the aim-203-session-enrichment-mig branch from ce1d9c9 to fd1236b Compare September 9, 2026 16:23
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from c170945 to a62f997 Compare September 9, 2026 16:32
Base automatically changed from aim-203-session-enrichment-mig to main September 9, 2026 16:45
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from a62f997 to a13bad2 Compare September 9, 2026 16:57
@blacksmith-sh

This comment has been minimized.

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 11 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread server/internal/remotesessions/identity.go Outdated
Comment thread server/internal/remotesessions/queries.sql Outdated
…te sessions

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from 38db55c to 3c35700 Compare September 9, 2026 17:57

@danielkov danielkov 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.

Directionally looks right.

Comment thread server/internal/remotesessions/queries.sql Outdated
Comment thread server/internal/remotesessions/token_response.go
Comment thread server/internal/remotesessions/tokenservice.go
Comment thread server/internal/remotesessions/identity.go Outdated
Comment thread server/internal/remotesessions/identity.go Outdated
Comment thread server/internal/remotesessions/identity.go
…en keys by algorithm

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the aim-203-id-token-enricher branch from f459ffe to 6a328a5 Compare September 9, 2026 18:28
@daviddanialy
daviddanialy added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 4e1bec0 Sep 9, 2026
81 of 83 checks passed
@daviddanialy
daviddanialy deleted the aim-203-id-token-enricher branch September 9, 2026 19:12
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants