feat!: identity linking OAuth 2.0 foundation with capability-driven scopes#354
Conversation
- Replace "consumer surfaces/platforms" with "consumer platforms" and "businesses" with "business platforms" for consistency. - Enhance the definitions of consumer and business platforms, emphasizing their roles in capability consumption and exposure. - Revise key goals and responsibilities to reflect updated terminology and clarify the interaction dynamics within the UCP framework. - Introduce a new section on capabilities, detailing their structure and examples to improve understanding of UCP's functionality.
- Clarified the role of Payment & Credential Providers to emphasize the secure handling of sensitive user data. - Enhanced the description of Agentic Commerce to include various modalities for AI agents. - Revised terminology for distinct actors in the UCP framework to improve clarity. - Updated capability negotiation process to specify version selection and mutual agreement. - Improved examples and descriptions for capabilities and transport bindings to align with current standards.
- Updated terminology to replace "consumer platforms" with "clients" and "business platforms" with "providers" for consistency and clarity. - Enhanced descriptions of the roles and responsibilities of clients and providers in the UCP framework. - Revised key goals and capabilities to reflect the updated terminology and improve understanding of UCP's functionality.
- Replaced "Client" with "Platform" and "Provider" with "Business" for consistency.
- Updated the identity linking specification to clarify the role of platforms and businesses in buyer-authenticated commerce experiences. - Introduced a new JSON schema for identity linking, detailing the configuration for capabilities that require buyer identity. - Revised the overview and general guidelines sections to reflect the updated terminology and structure for identity linking capabilities. - Added new error code for identity requirements in the shopping types schema.
Restores docs/documentation/core-concepts.md to match Universal-Commerce-Protocol/ucp upstream main. The local changes belong to a separate PR and should not be included here.
|
Thanks for laying the extensibility groundwork explicitly. The reserved Once this lands, we can follow up with a concrete Happy to wait for this PR to merge before opening the follow-up so we're not creating extension-point churn. |
- Renamed the `required` field to `auth_required` in the identity linking specification and JSON schema to enhance clarity regarding buyer identity requirements.
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)
igrigorik
left a comment
There was a problem hiding this comment.
@amithanda nice work on this, lots of good improvements!
We need to land support for delegated IdPs. This is a key and painfully missing feature, which we proposed the shape for in #330. Comparing the two branches, we need to reconcile and merge, and I think it's easier to build on what you've scoped here.
I took a run at bringing over additional logic from #330 in 603f4c8. Because your PR is against your own fork I can't open a stacked PR. That said, if the commit looks good, I can push it into your branch directly and we can continue iterating against your current branch and close out #330. PTAL, any objections?
With above in place, I think we can collapse providers vs mechanisms into single primitive. They are both attempting to answer the same question: "who does the business trust to vouch for buyers, and what proof protocol do they speak?"
An OAuth IdP and a wallet attestor both have:
- A reverse-domain trust anchor (com.google, com.example.attestor)
- A discovery mechanism (auth_url → RFC 8414, provider_jwks → JWKS endpoint)
- A proof format (OAuth tokens, ES256 signed payloads)
- An explicit trust relationship (business lists them)
Keeping them as separate keys creates ambiguity: are mechanisms alternatives to OAuth (platform picks one) or complements (identity via OAuth + wallet proof for eligibility)? The TLS cipher-suite negotiation analogy on mechanisms implies the former, but your actual use case in #264/#280 looks more like the latter.
Proposed reconciliation
Unify under providers with a type discriminator. Default type is oauth2 when absent — zero wire-format change for existing entries:
"config": {
"providers": {
"com.google": {
"auth_url": "https://accounts.google.com/"
},
"com.example.merchant": {
"auth_url": "https://merchant.example.com/"
},
"com.example.attestor": {
"type": "wallet_attestation",
"provider_jwks": "https://attestor.example.com/.well-known/jwks.json",
...
}
},
"capabilities": { ... }
}What this gets us:
- Single trust surface. Business declares all trusted identity/attestation sources in one map.
- Clean forward-compat. Platforms skip provider entries whose
typethey don't recognize. No separate "ignore unknown config fields" rule for a sibling key.
What we drop: The config.mechanisms reserved extension point. It's replaced by new type values on provider entries, which is a strictly simpler extension model.
The relationship between providers then becomes clear from context: a platform that finds both type: "oauth2" and type: "wallet_attestation" entries knows these are different proof protocols. Whether they're alternatives or complements depends on how config.capabilities references them — that's the follow-up design question for when wallet attestation lands, not something we need to over-specify now.
Four targeted fixes to prepare this PR for backport to 04/08 and
clean stacking of the delegated IdP follow-up.
## 1. Nest $defs under capability name (convention alignment)
Restructures the schema to match the established pattern required by
the composition algorithm: `ext_schema["$defs"][root.name]`
Before:
$defs:
After:
$defs:
capability_identity_config
dev.ucp.common.identity_linking:
platform_schema, business_schema
Why: capability-scoped schemas live under the capability's reverse-domain
name so future tooling can resolve them predictably as
`schema#/$defs/{capability-name}/business_schema`.
## 2. Fix `required` → `auth_required` in overview.md
The business profile example in `overview.md` used `"required": true`
while the schema and spec text use `"auth_required"`. Anyone copying the
overview example would hit a validation error.
## 3. Remove top-level `version` field from schema
No other capability schema in the repo carries a top-level `version`
field — version lives on the capability entry in the UCP profile, not
on the schema file itself. Removed for consistency with `checkout.json`,
`fulfillment.json`, `cart.json`, etc.
## 4. Tighten `iss` validation language
Removed the "if present" hedge in two places (For Platforms bullet and
Account Linking Flow step 3). Since the spec requires businesses to
MUST return `iss` in every authorization response, the hedge was
unnecessary and could be read as making `iss` validation conditional.
## 5. $comment updated to reflect unified providers model
The schema-level `$comment` previously described `providers` and
`mechanisms` as two separate reserved extension points. Updated to
describe a single `providers` map with a `type` discriminator defaulting
to `oauth2` — aligning with feedback on Universal-Commerce-Protocol#354 that these are the same
concept (a trust-anchored identity source with a discovery mechanism
and proof protocol), not separate keys. This is $comment-only — no
schema behavior change — and gives the follow-up IdP PR a clean model
to add `providers` onto without rewriting the Future Extensibility
section.
|
One gotcha that's bothering me with scopes as flat array of strings... "dev.ucp.shopping.order": {
"auth_required": true,
"scopes": ["read", "manage"]
}With above contract every scope inherits uniform requirements from a single B2C retailer
B2B wholesaler
Same capability ( Rec: scope level policies + explicit scopes (no bare capability scope)Each listed capability declares its scopes explicitly. Each scope is an object. Wire format is always "dev.ucp.common.identity_linking": [{
"config": {
"providers": { ... },
"capabilities": {
"dev.ucp.shopping.catalog": {
"scopes": {
"search": { "auth_required": false },
"vip": { "auth_required": true }
}
},
"dev.ucp.shopping.order": {
"scopes": {
"read": { "auth_required": true },
"manage": { "auth_required": true, "min_acr": "urn:tl:TL3" }
},
"some_future_foo": "bar"
}
}
}
}]The contract is:
What this unlocks
Alt: flat
|
…t for both delegated identity providers and non-OAuth authentication mechanisms through a unified `config.providers` extension point.
|
How about an alternate option where we can extend at the capability entry level, The per-scope flexibility concern is valid — capability-level The change
"dev.ucp.shopping.order": {
"auth_required": true,
"scopes": ["read", "manage"]
}A future PR adds "dev.ucp.shopping.order": {
"auth_required": true,
"scopes": ["read", "manage"],
"scope_config": {
"manage": { "min_acr": "urn:tl:TL3" }
}
}The B2C case (public catalog search, buyer-gated VIP access): "dev.ucp.shopping.catalog": {
"auth_required": false,
"scopes": ["search", "vip"],
"scope_config": {
"vip": { "auth_required": true }
}
}
Why this over changing
|
|
@igrigorik , while I agree with you about the use-cases such as B2C or B2B, I feel some of those need to be modeled at a higher level of granularity (hierarchical or flat structure). with B2C or B2B, it not just access to capabilities but there could be variations to data returned as part of existing operations. For example, the price returned could be different, and with the below suggestion, if the agent chooses to shop as B2B, then it would enforce authorization. BTW, I am split on whether "dev.ucp.shopping" treated as a capability or something else, but this should give an idea of how i see individual capability authorization and uber authorization are kinda orthogonal. |
Overview
This PR establishes the OAuth 2.0 foundation for the Identity Linking capability
(
dev.ucp.common.identity_linking). It is an intentionally scoped first step —business-hosted OAuth 2.0 only — designed so that delegated identity providers
and non-OAuth auth mechanisms can be added in future PRs as non-breaking
extensions.
Context: #265 introduced a mechanism registry and capability-driven scopes
but shipped a critical bug where the intersection algorithm's scope-dependency
pruning rule gated checkout behind identity linking, breaking guest checkout.
#329 reverted it. #330 proposed a full redesign including delegated IdP and
identity chaining. This PR carries forward the improvements from both without
the delegated IdP complexity, and lays explicit extensibility groundwork for
that work to land cleanly.
What Changed
New:
source/schemas/common/identity_linking.jsonSchema for the identity linking capability. Two context-specific views, nested
under
"dev.ucp.common.identity_linking"in$defsper the project'scapability schema convention:
platform_schema: passthrough — platforms advertise support; noauth-specific config needed.
business_schema: requiresconfig.capabilities— a map declaringwhich capabilities offer buyer-scoped features and whether buyer identity
is required for each.
The
configobject usesadditionalProperties: truewith a$commentnaming one reserved extension point:
providers— map of trusted identity providers keyed by reverse-domain,with a
typediscriminator defaulting tooauth2. This single extensionpoint covers delegated IdP, identity chaining, and future non-OAuth
mechanisms such as wallet attestation (future PR).
New:
identity_requirederror code (error_code.json)Standard protocol signal for the case where a capability is configured with
auth_required: trueand a request arrives without a buyer identity token.Businesses MAY include a
continue_urlin the error body for buyeronboarding flows.
Rewrite:
docs/specification/identity-linking.mdMajor rewrite of the spec. Key sections:
Access levels. Capabilities operate at three access levels — public,
agent-authenticated, buyer-authenticated. Identity linking upgrades capabilities
to buyer-authenticated access. Capabilities are never excluded from
capability negotiation based on identity linking. This directly addresses the
root cause of the #265 bug.
Security hardening (carried forward from #265 and #330):
S256: MUST for all authorization code flows;plainMUST NOT be usedissvalidation: MUST (RFC 9207) — prevents Mix-Up Attacks; validation isunconditional, not gated on presence of the
issparameterredirect_urimatching: MUST — no partial/prefix matchingissuerbyte-for-byte match: MUST — no normalization (trailing slash strippingis a known
issvalidation bypass)scopes_supported: MUST in RFC 8414 metadata — enables early scope mismatchdetection before consent screen
Strict discovery hierarchy (carried forward from #265 and #330):
/.well-known/oauth-authorization-server) —2xx: use it;404:proceed to step 2; any other non-2xx response, network error, or timeout:
MUST abort, MUST NOT fall through to step 2
/.well-known/openid-configuration) —2xx: use it; anynon-2xx response, network error, or timeout: MUST abort
Capability-driven scope model (redesigned from #265, aligned with #330):
Scope declarations live in
config.capabilitieson the identity linking config,not as
identity_scopesannotations on individual capability schemas. This isarchitecturally correct because whether a capability requires buyer auth is a
business decision — a B2B wholesaler gates catalog access, a B2C retailer
doesn't. Per-schema annotations can't express this variance.
Scope naming uses capability names directly (
dev.ucp.shopping.checkout,dev.ucp.shopping.order:read) — reusing UCP's existing reverse-DNS governanceand eliminating a separate scope namespace. Replaces the old
ucp:scopes:checkout_sessionformat.Future Extensibility section — explicit normative spec for how future
versions extend this capability without breaking v1 implementations (see below).
Updated:
docs/specification/overview.mdIdentity linking added back to both business and platform profile examples
using the new config shape, including the
auth_requiredfield naming.Fixed:
docs/index.mdScope naming in the RFC 8414 metadata example updated from
ucp:scopes:checkout_sessiontodev.ucp.shopping.checkout.Forward Compatibility Design
The schema and spec are explicitly designed for non-breaking extension via a
single
config.providersextension point.config.providers— Delegated Identity Providers and Mechanism ExtensibilityA future PR will add a
config.providersmap (keyed by reverse-domainidentifier) allowing businesses to declare trusted external identity providers
alongside their own hosted OAuth server. Each provider entry carries a
typediscriminator defaulting to
oauth2, making the map extensible to non-OAuthmechanisms (wallet attestation, verifiable credentials) without introducing a
separate
mechanismsarray.This covers two use cases:
to new businesses without a browser redirect (per
draft-ietf-oauth-identity-chaining-08), solving the N-merchant = N-OAuth-dances
problem for agentic commerce.
oauth2typevalueenable wallet attestation and similar schemes. Platforms select the first
entry whose
typethey support — analogous to TLS cipher suite negotiation,preserving business-preference ordering.
This addition is non-breaking because:
configusesadditionalProperties: true— the schema will not reject thenew field
recognize
config.providersMUST ignore it and fall back to RFC 8414discovery on the business domain
Type of Change
ucp:scopes:*format)Breaking Changes Justification
The scope naming convention change (
ucp:scopes:checkout_session→dev.ucp.shopping.checkout) is breaking for any implementation that hardcodedthe old format. The old format was defined in the pre-#265 spec and was never
part of a stable release — the capability was at Working Draft status throughout.
The new format is consistent with UCP's reverse-DNS naming governance and
eliminates the need for a parallel scope namespace.
Checklist