feat: capture upstream identity from ID tokens on remote session grants - #6104
Conversation
🦋 Changeset detectedLatest commit: 6a328a5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
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. |
There was a problem hiding this comment.
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
07cebfb to
d6c6b10
Compare
a995d37 to
dc45ad5
Compare
d6c6b10 to
f8792c3
Compare
dc45ad5 to
29da3da
Compare
f8792c3 to
37b1a5b
Compare
29da3da to
5cd0701
Compare
37b1a5b to
d9448e7
Compare
5cd0701 to
8f8a7e2
Compare
d9448e7 to
a4bbc78
Compare
8f8a7e2 to
c1428ae
Compare
a4bbc78 to
20ea3ee
Compare
c1428ae to
7fc24af
Compare
20ea3ee to
facecd4
Compare
7fc24af to
cf60d1e
Compare
facecd4 to
3d49da2
Compare
cf60d1e to
94e1dd3
Compare
3d49da2 to
cd7ca8c
Compare
|
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. |
cd7ca8c to
5c292e7
Compare
94e1dd3 to
c64d36e
Compare
5c292e7 to
a5c7d1a
Compare
c64d36e to
ce1d9c9
Compare
a5c7d1a to
c170945
Compare
This comment has been minimized.
This comment has been minimized.
ce1d9c9 to
fd1236b
Compare
c170945 to
a62f997
Compare
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
a62f997 to
a13bad2
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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
…te sessions Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
38db55c to
3c35700
Compare
…en keys by algorithm Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
f459ffe to
6a328a5
Compare
AIM-203, part 2 of 2. Stacked on #6100; merge that first.
Summary
nonce. When the token response has anid_token, it is verified (signature via the issuer'sjwks_urithrough the sharedjwks.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'sid_token_signing_alg_values_supportedwhen it advertises any, and the signing key is picked by the token'salgamong keys sharing akid(declaredalgfirst, else key type and curve).remote_sessionsidentity columns (upstream_subject,upstream_email,upstream_display_name,identity_source).enrichmentkeeps 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.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 (theid_tokenandtoken_responsemembers merge key by key). A refresh ID token naming a differentsubis rejected like any other invalid token and the identity verified at the exchange stands. Any soft-delete clears identity and enrichment.IDTokenVerifieris 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.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