Skip to content

feat!: delegated IdP, identity chaining, and capability-driven scope model#330

Open
igrigorik wants to merge 2 commits intomainfrom
feat/identity-linking-v2
Open

feat!: delegated IdP, identity chaining, and capability-driven scope model#330
igrigorik wants to merge 2 commits intomainfrom
feat/identity-linking-v2

Conversation

@igrigorik
Copy link
Copy Markdown
Contributor

Redesign identity linking for multi-merchant agentic commerce. Businesses can offer delegated identity providers, allowing buyers to authenticate through a trusted IdP (e.g., Google, Shop) instead of creating a per-merchant account. Platforms can support these IdPs to streamline identity linking — authenticate once, reuse across businesses that trusts the same provider.

This PR carries builds on / supersedes #265, reverted in #329.

Delegated Identity Providers

The current identity linking spec assumes 1:1 — each business operates its own OAuth authorization server, requiring a full OAuth dance per merchant. In agentic commerce, where platforms shop across many businesses on a buyer's behalf, this means N merchants = N handoffs.

Businesses can now declare trusted identity providers in config.providers, keyed by reverse-domain identifier:

"dev.ucp.common.identity_linking": [{
  "config": {
    "providers": {
      "com.google": {
        "auth_url": "https://accounts.google.com/"
      },
      "com.example.merchant": {
        "auth_url": "https://merchant.example.com/"
      }
    }
  }
}]

Here the business trusts Google as an external IdP and also lists itself (com.example.merchant) for business-hosted OAuth — both use the same discovery mechanism. Each provider's auth_url is resolved via RFC 8414, with OIDC /.well-known/openid-configuration fallback on 404.

Two distinct flows:

  • Account Linking: One-time OAuth flow between platform and IdP. Standard Authorization Code + PKCE, with support for additional grant types (device authorization for headless agents via RFC 8628).
  • Accelerated IdP Flow: Platform already holds a valid IdP token (e.g., from Google), chains the buyer's identity to a new business without a browser redirect — per draft-ietf-oauth-identity-chaining-08.

Identity chaining

The accelerated flow implements a two-phase pattern:

  1. Platform requests a JWT authorization grant from the IdP via token exchange (RFC 8693), using the resource parameter to identify the target business
  2. Platform presents the JWT grant to the business's token endpoint via JWT bearer assertion (RFC 7523)
  3. Business validates the grant, resolves buyer identity, issues its own access token

The IdP controls consent — it MUST NOT issue grants for businesses the buyer has not authorized. Businesses MAY auto-provision accounts or return a continue_url for buyer onboarding (terms acceptance, etc.). Two independent token lifecycles with independent revocation. Refresh tokens SHOULD NOT be issued per the chaining draft.

Capability-driven scope model

Scopes define the permissions an identified buyer grants to a platform within a capability. Unlike #265's approach of annotating individual capability schemas with identity_scopes, scope declarations live centrally on the identity linking config. This is because whether a capability requires buyer auth is a business decision — a B2B wholesaler gates catalog access, a B2C retailer serves it publicly.

Three access levels

Level Authentication Example
Public None Browse a public catalog
Agent-authenticated Platform credentials Guest checkout, read agent's own orders
Buyer-authenticated Platform + buyer identity Saved addresses, full order history, personalized pricing

Identity linking upgrades capabilities to buyer-authenticated access. Capabilities are never pruned from the intersection based on identity linking; there is no capability pruning algorithm.

Config shape

Building on the provider example, businesses declare which capabilities offer buyer-scoped features alongside their trusted IdPs:

"dev.ucp.common.identity_linking": [{
  "config": {
    "providers": {
      "com.google": {
        "auth_url": "https://accounts.google.com/"
      }
    },
    "capabilities": {
      "dev.ucp.shopping.checkout": {},
      "dev.ucp.shopping.order": {
        "required": true,
        "scopes": ["read", "manage"]
      }
    }
  }
}]
  • Absent from capabilities map: no buyer-scoped features (fully public)
  • Present, no required: accessible without buyer auth, identity upgrades the experience
  • required: true: business returns identity_required error without buyer identity
  • scopes: sub-scope operation groups defined by each capability's spec, joined with colon on the wire (e.g., dev.ucp.shopping.order:read)
  • Scope tokens use capability names directly — no separate scope namespace

Carried forward from #265

  • PKCE MUST (platforms and businesses)
  • iss validation MUST (RFC 9207, both sides)
  • Exact redirect_uri matching MUST (businesses)
  • scopes_supported MUST in RFC 8414 metadata
  • 2-step discovery hierarchy: RFC 8414 primary, OIDC fallback on 404 only, strict abort on other errors
  • JWT authorization grants: 60s lifetime, jti single-use enforcement

Type of change

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as expected, including removal of schema files
    or fields
    )
  • I have added ! to my PR title (e.g., feat!: remove field).

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

…model

  Redesign identity linking for multi-merchant agentic commerce.
  Businesses can offer delegated identity providers, allowing buyers to
  authenticate through a trusted IdP (e.g., Google, Shop) instead of
  creating per-merchant accounts. Platforms can support these IdPs to
  streamline identity linking — authenticate once, reuse across every
  business that trusts the same provider.

  ## Delegated Identity Providers

  Businesses declare trusted identity providers in config.providers,
  keyed by reverse-domain identifier. Each provider specifies an auth_url
  for metadata discovery (RFC 8414, OIDC fallback on 404). A business MAY
  list itself as a provider, unifying business-hosted OAuth with delegated
  auth under the same discovery mechanism.

  ## Identity Chaining (Accelerated IdP Flow)

  When a platform already holds an IdP token, it chains the buyer's
  identity to a new business without a browser redirect, per
  draft-ietf-oauth-identity-chaining-08:

  1. Platform requests a JWT authorization grant from the IdP (RFC 8693)
     using the resource parameter
  2. Platform presents the grant to the business (RFC 7523 jwt-bearer)
  3. Business validates, resolves buyer identity, issues its own token

  The IdP controls consent — it MUST NOT issue grants for businesses the
  buyer has not authorized. Businesses MAY auto-provision accounts or
  return a continue_url for buyer onboarding. Refresh tokens SHOULD NOT
  be issued per the chaining draft §5.4. Two independent token lifecycles
  with independent revocation.

  ## Capability-Driven Scope Model

  Scopes define the permissions an identified buyer grants to a platform.
  Declarations live centrally on the identity linking config, not on
  individual capability schemas:

  - config.capabilities map keyed by capability name declares which
    capabilities offer buyer-scoped features
  - required: true signals buyer identity is mandatory (identity_required
    UCP business error if absent, with continue_url for onboarding)
  - Sub-scopes use colon separator (e.g. dev.ucp.shopping.order:read),
    defined by each capability spec as operation groups
  - Scope tokens use capability names directly — no separate namespace
  - Capabilities are never pruned from the intersection
  - Platforms should request scopes incrementally

  Three access levels: public (no auth), agent-authenticated (platform
  credentials), buyer-authenticated (identity linking upgrades or gates
  access depending on the business's required flag).

  ## Security

  - PKCE MUST (platforms and businesses)
  - iss validation MUST (RFC 9207, both sides)
  - Exact redirect_uri matching MUST (businesses)
  - scopes_supported MUST in RFC 8414 metadata
  - 2-step discovery: RFC 8414 primary, OIDC fallback on 404 only
  - JWT grants: 60s lifetime, jti single-use enforcement
  - Strict abort on non-404 discovery errors
@igrigorik igrigorik self-assigned this Apr 2, 2026
@igrigorik igrigorik added the TC review Ready for TC review label Apr 2, 2026
@igrigorik igrigorik requested review from a team as code owners April 2, 2026 04:48
@igrigorik igrigorik requested review from DanielFalconGuedes, mmohades, ptiper and wry-ry and removed request for a team April 2, 2026 04:48
@nearlyforget nearlyforget removed the request for review from DanielFalconGuedes April 2, 2026 18:38
Base automatically changed from revert/identity-linking-265 to main April 3, 2026 04:33
@aglazer
Copy link
Copy Markdown

aglazer commented Apr 3, 2026

Thanks @igrigorik. Agree with the changes. Great addition to UCP!

* Before initiating identity chaining with a business, the platform
**SHOULD** offer the buyer a choice of available identity providers and
indicate that the selected provider's identity will be shared with the
business.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is SHOULD the right keyword here if there is only one in-common provider, which would effectively be a no-op?

Also worth considering that a platform may have a preferred IdP (or operate its own), which isn't just a UX convenience, it has potential security advantages around revocation, since a platform controlling its IdP can enforce consistent token revocation across all trusting businesses. That said, the intent to preserve buyer choice and visibility over which identity is shared is an important counterbalance.

business.
* Revocation and security events
* **SHOULD** revoke tokens at both layers when a buyer initiates an
unlink action (see [Token Lifecycle](#token-lifecycle)).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider splitting the platform's revocation obligation into two levels:

  • Revoking the business-issued token should be a MUST as that's the direct relationship the buyer explicitly asked to sever.
  • Revoking the IdP token is a broader action that would cascade across all business relationships established through that IdP, so MAY feels more appropriate, leaving the platform to determine the right blast radius.

* **MUST** implement standard Token Revocation as defined in
[RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009){ target="_blank" }.
* **MUST** revoke the specified token and **SHOULD** recursively revoke
all associated tokens (e.g., revoking a `refresh_token` **MUST** also
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The example here promotes the SHOULD guidance into a MUST. Should revocation really be optional?

automatically propagate to the other.

**Business-issued tokens.** Access tokens issued by the business follow the
business's token lifecycle. Businesses **SHOULD NOT** issue refresh tokens
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Architecturally, discouraging refresh tokens for the JWT bearer grant flow makes a lot of sense as it keeps the IdP as the ongoing trust anchor. But that makes the access token lifetime the effective trust re-verification interval, should we be explicit that it should be minutes not hours?


When multiple identity providers are available, the platform **SHOULD** offer
the buyer a choice of providers. Before initiating identity chaining, the
platform **SHOULD** inform the buyer which business will receive their
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm left wondering if strengthening this obligation would be a buyer-centric privacy improvement. The IdP's consent gate covers who and where: it knows the buyer and the target business, and actively gates that relationship. But the fine-grained scope negotiation happens between the platform and the business, outside the IdP's visibility. That makes the platform the only participant that sees both sides: which business receives the identity and what scopes that identity unlocks. The buyer should have visibility into both, not just where their identity is going, but what it will be used for.

}
},
"additionalProperties": true
"additionalProperties": false
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there a reason we're closing off extensibility here and leaving it open on provider?

]
},
"additionalProperties": true
"business_schema": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: Should config be required here as it was previously? The old schema required it, and declaring identity linking with no config is effectively a no-op: no providers, no capability scope requirements. It feels like a state that's more likely to be a misconfiguration than an intentional choice. That said, there's an argument for allowing it as an incremental rollout path, where a business signals infrastructure support before capabilities opt in.

Comment thread docs/index.md
"revocation_endpoint": "https://example.com/oauth2/revoke",
"scopes_supported": [
"dev.ucp.shopping.scopes.checkout_session"
"ucp:scopes:checkout_session",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: This scope format doesn't match the new scope naming convention defined in the Scope Naming section.

`urn:ietf:params:oauth:grant-type:jwt-bearer` in
`grant_types_supported` in its
[RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414){ target="_blank" } metadata.
* **MUST** provide an account creation flow if the buyer does not already have
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.

Thoughts on moving this to MAY? I'm thinking of the scenario: "as a Business, I want to automatically sign in existing users, but don't want to interrupt the flow and force users to sign up."

@douglasborthwick-crypto
Copy link
Copy Markdown

The delegated IdP architecture is a big improvement — especially the identity chaining flow for reducing N-merchant handoffs in agentic commerce.

One question on provider extensibility, building on @gsmith85's point: the provider schema currently requires auth_url and resolves via RFC 8414 / OIDC discovery. This works for OAuth IdPs, but in agentic commerce there are non-OAuth identity signals that follow a similar trust model — signed claims, public-key discovery via .well-known, offline verification — without an OAuth authorization flow.

Concrete example: wallet attestation. An agent presents a signed credential proving it meets specific conditions (holds assets, passes compliance checks). The verifier fetches the issuer's JWKS from /.well-known/jwks.json, verifies the ES256 signature, checks expiry — no redirect, no token exchange, no browser. The trust model is the same as an IdP (public-key discovery → signature verification), but the flow is attestation-based rather than authorization-based.

This maps to the "agent-authenticated" access level in #330's three-tier model — the agent proves something about itself without a buyer-identity OAuth flow.

Would it make sense for provider to accommodate non-OAuth mechanisms alongside the current OAuth flow? The old mechanism base in #265 had a type discriminator with additionalProperties: true for exactly this kind of extensibility. Even if the initial implementation is OAuth-only, a type field on provider would keep the door open without adding complexity to the current spec.

@proshoumma
Copy link
Copy Markdown

Really like the direction here, the delegated IdP model and identity chaining solve a real pain point for multi-merchant agentic commerce. We've been working through this exact problem on our end and this is a big step forward.

One thing we keep coming back to is per-business buyer consent in the Accelerated IdP Flow.

With the traditional Account Linking flow, the buyer sees the business's consent screen and explicitly approves. The Accelerated IdP Flow removes that touchpoint - identity chains to new businesses server-to-server, which is great for UX but means the buyer never explicitly consents to (or may not even be aware of) each individual business receiving their identity.

The spec has the right instincts here - the MUST NOT on IdPs issuing grants for unauthorized businesses, and the SHOULD on platforms informing the buyer, but neither defines a concrete mechanism. And the underlying draft-ietf-oauth-identity-chaining-08 doesn't help either. It's entirely silent on consent, which makes sense for its enterprise federation roots, but consumer commerce is a different world. Under GDPR especially, per-purpose consent and data subject awareness aren't optional.

A few things we're thinking about:

  • Should the spec provide guidance on what "buyer has not authorized" actually means in practice? Right now it's a strong requirement with no defined path to satisfy it.
  • Would it make sense to strengthen buyer awareness from SHOULD to MUST for consumer-facing contexts?
  • Happy to help think through what a workable consent model could look like here.

@drewolson-google drewolson-google self-requested a review April 10, 2026 17:41
igrigorik added a commit that referenced this pull request Apr 16, 2026
  Brings forward the delegated identity provider design from #330 into
  this PR's OAuth 2.0 foundation. The core capability-driven scope model
  and security posture from #354 are unchanged — this commit adds the
  multi-merchant identity layer on top.

  ## Added

  ### Delegated Identity Providers (`config.providers`)

  Businesses can declare trusted external OAuth identity providers in
  `config.providers`, keyed by reverse-domain identifier. A business MAY
  also list itself as a provider, unifying business-hosted OAuth and
  delegated IdP under the same discovery mechanism. When `config.providers`
  is absent, platforms fall back to RFC 8414 discovery on the business
  domain — preserving the baseline behavior already specified in this PR.

  ### Identity Chaining (Accelerated IdP Flow)

  When a platform already holds a valid IdP token and encounters a new
  business that trusts the same IdP, it can chain the buyer's identity
  without a browser redirect. Implements
  draft-ietf-oauth-identity-chaining-08:

  1. Platform obtains a JWT authorization grant from the IdP via token
     exchange (RFC 8693, `resource` parameter identifies target business)
  2. Platform presents the JWT grant to the business via JWT bearer
     assertion (RFC 7523)
  3. Business validates, resolves buyer identity, issues its own token

  This solves the N-merchant = N-OAuth-handoff problem for agentic
  commerce.

  ### Supporting sections

  - **Account Linking**: one-time OAuth flow between platform and IdP,
    reusable across businesses that trust the same provider
  - **Headless and Agentic Contexts**: RFC 8628 device authorization for
    CLI agents and voice assistants
  - **JWT Authorization Grant**: claims table (iss, sub, aud, exp, iat,
    jti), 60s lifetime recommendation, single-use enforcement
    fail-closed on JWKS retrieval failure
  - **Token Lifecycle**: dual-layer management — business tokens and IdP
    tokens have independent lifecycles and revocation; businesses SHOULD
    NOT issue refresh tokens on JWT bearer grants
  - **IdP Requirements**: metadata requirements (revocation_endpoint,
    jwks_uri, token-exchange grant type), token exchange processing rules
  - **Buyer Awareness**: provider choice UX, consent disclosure
  - **Chaining Error Handling**: error table mapping JWT validation
    failures to standard OAuth error responses

  ## Changed

  - **Overview**: removed "v1 auth mechanism" hedging; identity chaining
    is part of the spec, not a future extension
  - **Participants table**: added Identity Provider (IdP) role
  - **General Guidelines — Platforms**: added provider selection and
    chaining disclosure guidance
  - **General Guidelines — Businesses**: added JWT bearer assertion MUST
    when `config.providers` is present
  - **Scopes**: added "Scopes and External Identity Providers" subsection
    clarifying that UCP scopes are requested from the business, not the IdP
  - **Security Considerations**: added JWT grant lifetime, jti single-use,
    and grant replay items
  - **Future Extensibility**: removed `config.providers` subsection (now
    normative); only `config.mechanisms` remains as future work
  - **Auth server metadata example**: added jwt-bearer grant type,
    explanatory note
  - **Business Profile example**: added providers map, fixed `required` →
    `auth_required` to match schema field name
  - **overview.md**: added providers to business profile example

  ### Schema

  - Added `provider` $def (object with `auth_url` URI)
  - Added `providers` property to business config (optional, map keyed by
    reverse-domain)
  - Restructured $defs to nest platform_schema/business_schema under
    `dev.ucp.common.identity_linking`, required by the composition
    algorithm
  - Removed `"version": "Working Draft"` from schema top-level
  - Updated $comment to reflect providers as shipped (not reserved)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants