Skip to content

feat: request spec-aligned scopes, retry without resource indicators, and validate iss on remote login - #6109

Merged
daviddanialy merged 4 commits into
mainfrom
aim-202-issuer-scope-resource
Sep 9, 2026
Merged

feat: request spec-aligned scopes, retry without resource indicators, and validate iss on remote login#6109
daviddanialy merged 4 commits into
mainfrom
aim-202-issuer-scope-resource

Conversation

@daviddanialy

@daviddanialy daviddanialy commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

AIM-202. Stacked on #6107.

Summary

Makes the upstream OAuth request adapt to each issuer.

  • Scopes. Client.RequestedScopes: scope_override verbatim if set; else the client's stored scope, falling back to the issuer's scopes_supported; then openid, email, profile, offline_access appended when the issuer advertises them. Widening logs at debug; an empty override means none. The requested set is recorded on the session when the token response omits scope (RFC 6749 §5.1).
  • RFC 8707. resource is sent on authorize, exchange, and refresh unless resource_indicator_supported is false. resource_indicator_supported is purely an operator override; Gram never writes it from a login. On invalid_target the callback retries that login once without resource, and the refresh grant gets the same one-shot retry inside its single-flight lease; any other error is final and nothing is generalised to the issuer or to other grants, since a rejection can be about one resource rather than RFC 8707 support. The resource stays on the session for routing.
  • RFC 9207. When the issuer advertises authorization_response_iss_parameter_supported, the callback rejects a response whose iss is missing or different, before the denial branch and the exchange. Compared to the discovery document's issuer, or the stored URL without its trailing slash when no document exists.
  • Callback hygiene. A callback with neither code nor error leaves the login state alone. Denials echo only IETF-registered error codes. Upstream and browser text is capped before logging. Token-endpoint error bodies are parsed with oautherr.ParseTokenError.
  • Consent card. "Reconnect to enable identity" on a connected grant that lacks openid while a reconnect would request it; the first-party page no longer auto-closes over such a card.
  • Unchanged-behaviour suite. challenge_unchanged_regression_test.go and its mcp sibling pin the exact authorize URL, exchange and refresh bodies, callback handling without the iss capability, verbatim response-scope storage, pre-PR login states, and first-party auto-close for issuers the new rules do not touch. Two changes apply to every issuer and are pinned as such: a denial consumes the login state, and a token response without scope records the requested set.
  • API. Both columns on the issuer type, draft, and create/update forms at all tiers; update keeps scope_override when omitted or null and clears it on []; the form field serializes without omitempty at every tier so generated clients can send [], and the OpenAPI marks it nullable. SDK regenerated. Dashboard exposure deferred; an omitted field keeps the stored value, so dashboard saves cannot clobber them.

Motivation

Linear never returned an ID token because openid was never requested. Vercel sessions died after an hour because offline_access was never requested. WorkOS never connected because it rejects resource. The callback never checked iss. Scope and iss are per-issuer rules, so those knobs live on the issuer row. Resource-indicator support is learned per grant at runtime instead, with one retry so a misbehaving provider cannot loop a user.

🤖 Generated with Claude Code

@daviddanialy
daviddanialy requested review from a team as code owners September 4, 2026 23:58
@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-202

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9ee890f

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

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

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 — Running ultrareview automatically — This change rewrites core remote-login OAuth behavior (scope resolution, RFC 8707 resource handling with retry, RFC 9207 iss validation) across the callback and issuer API, so a subtle bug could break auth or route grants.. I'll post findings when complete.

@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from d887aa1 to 64677dc Compare September 5, 2026 00:13
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from e1d8f0b to 273bc07 Compare September 5, 2026 00:13

@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 25m 14s

Review completed against the latest diff

Linked issue analysis

Linked issue: AIM-202: Spec-aligned per-issuer scope and resource-indicator strategy for upstream authorization

Status Acceptance criteria Notes
⚠️ Resolve upstream scopes from the configured override or client/issuer scope source, and append advertised openid, email, profile, and offline_access scopes. The PR documents and tests the override/base precedence and advertised standard-scope appending, but the provided evidence does not clearly demonstrate the issue's required merge semantics between ClientScope and issuer scopes_supported.
When an issuer rejects RFC 8707 resource with invalid_target, learn that capability, retry once without resource, and retain the resource on the resulting session. The PR describes gating resource on both authorization and exchange, parsing invalid_target, persisting the issuer flag, retrying exactly once, and retaining the resource; an end-to-end test covers the strategy.
Validate the RFC 9207 iss callback parameter when the issuer advertises authorization_response_iss_parameter_supported. The callback compares the returned iss with the expected issuer and rejects missing or mismatched values; the strategy end-to-end test covers this behavior.
Show a reconnect hint when a live grant lacks openid but reconnecting would request it. Scopes are plumbed into session state and card rendering, the template adds the requested copy and reconnect action, and dedicated tests cover the behavior.
Expose scope_override and resource_indicator_supported as issuer configuration across supported API forms and models. The PR adds the fields to the issuer design, generated API schemas and models, create/update payloads, persistence, and view mapping.

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

Fix all with cubic | Re-trigger cubic

Comment thread server/internal/mcp/consent_template.html
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 64677dc to 2d19fa6 Compare September 8, 2026 17:09
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from 273bc07 to 4f70ebc Compare September 8, 2026 17:13
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 2d19fa6 to 8f0eace Compare September 8, 2026 17:37
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from 4f70ebc to 1980e25 Compare September 8, 2026 17:42
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 8f0eace to 346a35d Compare September 8, 2026 18:05
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from 1980e25 to 11ff909 Compare September 8, 2026 18:05
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 346a35d to 50e14fc Compare September 8, 2026 18:31
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from 11ff909 to 5c01fac Compare September 8, 2026 18:31
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 50e14fc to c96f6e6 Compare September 8, 2026 19:07
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch 3 times, most recently from 0ab74e3 to d01a1a5 Compare September 8, 2026 19:34
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from c96f6e6 to 0be7d0e Compare September 8, 2026 19:52
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch 2 times, most recently from 5dc471c to 85cc31c Compare September 8, 2026 19:57
@blacksmith-sh

This comment has been minimized.

@daviddanialy

Copy link
Copy Markdown
Contributor Author

Testing summary:

Reviews. Three independent passes (security, behavioural correctness against AIM-202 and the RFCs, data and API surface). One blocker found and closed: the resource-indicator flag is now learned only from a token-endpoint invalid_target, never from the browser-forgeable redirect. All should-fixes applied.

Unit and end to end (remotesessions, mcp): scope precedence rows including NULL and empty scopes_supported and empty override; refresh grant with the flag NULL, true, and false; forged redirect invalid_target leaves the flag NULL; token-endpoint invalid_target learns false; retry leg refused on a second invalid_target and when no resource was recorded; response scope stored over the requested set; iss fallback with and without a trailing slash; consent hint with empty stored scopes; denial echoes only registered codes; bare ?state= does not consume a login; the flag write hits org rows and refuses global and foreign-org rows; scope_override create [] stores NULL, update [] clears, omitted keeps, at project and org tiers; first-party page stays open over a reconnectable card.

Unchanged-behaviour suite (24 cases): exact authorize URL key set and scope for issuers advertising nothing, exchange and refresh bodies, callbacks without the iss capability, verbatim response-scope storage, clients already carrying the standard scopes, pre-PR login states read from Redis, and first-party auto-close for grants that already have openid.

Browser, local stack with dev-idp as the upstream issuer: baseline connect widened email to email openid profile and stored it; reconnect hint shown, page did not auto-close, Reconnect cleared it; scope_override sent verbatim; auto-close still fires for a grant with openid. iss validation and invalid_target not exercisable against dev-idp (it advertises neither) and are covered by the Go tests above.

Suites. remotesessions and mcp: 1,430 tests. Full server suite: 15,455 (two contention flakes outside these packages, pass in isolation). Dashboard unit suite: 3,230. Server lint clean.

@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from 85cc31c to fb0ea22 Compare September 8, 2026 20:37
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 0be7d0e to 3d8af5d Compare September 8, 2026 21:54
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from fb0ea22 to c57e88d Compare September 8, 2026 22:02
@blacksmith-sh

This comment has been minimized.

@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from 3d8af5d to cb4e77f Compare September 9, 2026 16:23
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from c57e88d to 3167847 Compare September 9, 2026 16:29
…ssion_issuers

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource-mig branch from cb4e77f to 665405e Compare September 9, 2026 16:51
…and validate iss on remote login

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the aim-202-issuer-scope-resource branch from 3167847 to e116c30 Compare September 9, 2026 16:56
Base automatically changed from aim-202-issuer-scope-resource-mig to main September 9, 2026 18:02

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

Couple Astra findings. I didn't see anything else.

Comment thread server/internal/remotesessions/tokenservice.go Outdated
Comment thread server/internal/remotesessions/challenge.go Outdated
Comment thread server/gen/http/remote_session_issuers/client/types.go Outdated
…e_override empty slices on the wire

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daviddanialy daviddanialy changed the title feat: request spec-aligned scopes, learn resource-indicator support, and validate iss on remote login feat: request spec-aligned scopes, retry without resource indicators, and validate iss on remote login Sep 9, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

@daviddanialy
daviddanialy added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 10ef761 Sep 9, 2026
81 of 83 checks passed
@daviddanialy
daviddanialy deleted the aim-202-issuer-scope-resource branch September 9, 2026 20:23
@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