Skip to content

feat!: redesign identity linking with mechanism registry and capability-driven scopes#265

Merged
amithanda merged 16 commits intoUniversal-Commerce-Protocol:mainfrom
amithanda:feature/identity-linking-extensibility
Mar 19, 2026
Merged

feat!: redesign identity linking with mechanism registry and capability-driven scopes#265
amithanda merged 16 commits intoUniversal-Commerce-Protocol:mainfrom
amithanda:feature/identity-linking-extensibility

Conversation

@amithanda
Copy link
Contributor

Overview

This PR redesigns the Identity Linking capability from a hardcoded OAuth 2.0
specification into an extensible, security-hardened system built on three
core principles: a Mechanism Registry pattern for future-proof
authentication negotiation, capability-driven least-privilege scope
derivation
, and hardened OAuth 2.0 security requirements aligned with
current best practices.


What Changed

Mechanism Registry Pattern (identity-linking.md, identity_linking.json)

The capability no longer hardcodes OAuth 2.0 as the only authentication
mechanism. Instead, businesses declare an ordered supported_mechanisms
array in their capability config, and platforms select the first mechanism
type they support (business-preference ordering, mirroring TLS cipher suite
negotiation).

A formal Mechanism Selection Algorithm is defined:

  • Iterate from index 0 (highest business preference)
  • Select the first type the platform supports
  • Abort if no supported type is found — no partial or fallback flows permitted

This enables future mechanism types (e.g., verifiable_credential) without
breaking changes to the protocol.

Capability-Driven Least-Privilege Scope Negotiation (overview.md, checkout.json, identity_linking.json)

Authorization scopes are no longer hardcoded or pre-declared in the identity
linking config. Instead:

  • Each capability schema declares its own required scopes via a top-level
    identity_scopes annotation (e.g., checkout.json declares
    dev.ucp.shopping.scopes.checkout_session)
  • Scopes are derived only from the finalized capability intersection —
    capabilities excluded during negotiation contribute zero scopes
  • The capability intersection algorithm gains two new steps:
    • Step 3 — Scope Dependency Pruning: capabilities declaring
      identity_scopes are removed if dev.ucp.common.identity_linking is not
      in the intersection
    • Step 5 — Derive Scopes (Final Pass): the authorization scope set is
      computed from the stabilized intersection only

This makes over-permissioning impossible by construction, not convention.

Scope Naming Convention

Scopes now use reverse DNS dot notation with a mandatory .scopes.
segment, consistent with UCP capability names:

  • UCP-defined: dev.ucp.<domain>.scopes.<capability>
    (e.g., dev.ucp.shopping.scopes.checkout_session)
  • Third-party: <reverse-dns>.scopes.<capability>
    (e.g., com.example.loyalty.scopes.points_balance)

A regex pattern enforcing this convention is defined in
$defs/identity_scopes in identity_linking.json as the canonical
reference.

Hardened OAuth 2.0 Security (identity-linking.md)

The OAuth 2.0 mechanism requirements are significantly tightened:

Requirement Before After
PKCE (RFC 7636) Not required MUST use S256
iss validation (RFC 9207) Not mentioned MUST validate to prevent Mix-Up Attacks
redirect_uri matching Not mentioned MUST enforce exact string match
Issuer normalization Not mentioned MUST strip trailing slashes before comparison
Discovery error handling Unspecified Only HTTP 404 triggers OIDC fallback; all other failures abort

Discovery Resolution Hierarchy (identity-linking.md)

A formal 3-tier hierarchy replaces the previous unspecified discovery:

  1. Explicit discovery_endpoint (hard abort on any failure)
  2. RFC 8414 /.well-known/oauth-authorization-server (only 404
    triggers fallback; 5xx, timeouts abort)
  3. OIDC fallback /.well-known/openid-configuration (abort on non-2xx)

Schema (identity_linking.json)

New schema file with:

  • platform_schema: passthrough only — platforms are consumers of
    mechanisms, not providers
  • business_schema: requires config with supported_mechanisms
    (minItems: 1)
  • mechanism: open base type (consistent with payment_credential.json)
    type required, additionalProperties: true
  • oauth2: named $def for explicit strict validation
  • identity_scopes: canonical annotation definition with enforced regex
    pattern

End-to-End Workflow Example (identity-linking.md)

A complete walkthrough added: AI Shopping Agent (platform) + Merchant
(business), covering discovery, scope derivation, mechanism selection,
OAuth authorization request (with PKCE and iss parameters), and token
exchange.


Files Changed

File Change
docs/specification/identity-linking.md Major rewrite
docs/specification/overview.md Pruning algorithm + Step 5 scope derivation
source/schemas/common/identity_linking.json New file
source/schemas/shopping/checkout.json Adds identity_scopes annotation
docs/index.md Scope value updated to reverse DNS notation

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • Documentation update

Is this a Breaking Change or Removal?

If you checked "Breaking change" above, or if you are removing any schema
files or fields:

  • I have added ! to my PR title (e.g., feat!: remove field).
  • I have added justification below.
## Breaking Changes / Removal Justification
The change from hardcoded OAuth 2.0 to a registry and new scope naming conventions constitutes a breaking change for the protocol

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

@douglasborthwick-crypto

Great design. The mechanism registry pattern is exactly right — hardcoding OAuth as the only path was the main limitation of the previous spec.

This pairs well with #264 (attestation extension for eligibility). The two address different surfaces:

But there's also a natural overlap: wallet attestation as an identity mechanism type. Some commerce flows don't need OAuth account linking at all — the wallet is the identity. A business selling token-gated merchandise needs to verify holdings, not link an account.

The registry already supports this cleanly:

"supported_mechanisms": [
    {
        "type": "oauth2",
        "issuer": "https://auth.merchant.example.com"
    },
    {
        "type": "wallet_attestation",
        "provider_jwks": "https://verifier.example.com/.well-known/jwks.json",
        "attestation_endpoint": "https://verifier.example.com/v1/attest"
    }
]

The platform calls the attestation endpoint, receives a signed payload with kid + sig, and the business verifies offline via JWKS — the same pattern from #264 and the signals work in #203. No redirect flow, no token exchange, no account creation. The base mechanism schema's additionalProperties: true means this works today without schema changes.

For scope derivation: a wallet_attestation mechanism wouldn't need OAuth scopes — verification is stateless and self-contained. The pruning algorithm in Step 3 already handles this correctly: capabilities without identity_scopes contribute zero scopes.

One question on the spec: the Discovery Bridging section (RFC 8414 / OIDC fallback hierarchy) is tightly coupled to OAuth. Would it make sense to scope that hierarchy under the oauth2 mechanism section rather than presenting it as general? Other mechanism types would define their own resolution — JWKS endpoint for attestation, DID resolution for verifiable credentials, etc.

Copy link

@cusell-google cusell-google left a comment

Choose a reason for hiding this comment

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

This redesign is a massive improvement, left some comments. The only real blocker is related to the issuer normalization. Thanks Amit!

@ptiper ptiper removed request for dwdii and ptiper March 17, 2026 10:31
amithanda and others added 9 commits March 17, 2026 19:01
…efine a metadata resolution hierarchy, and clarify scope derivation in capability negotiation.
… discovery failure, and introduce scope dependency pruning.
…idation requirements, and remove `identity_scopes` from the capability schema.
…identity linking documentation with mechanism selection algorithm and scope naming conventions.
…runing rules and enhancing identity linking schema description with annotation conventions.
…on rules and enhance schema definition for authentication mechanisms.
…and update scope derivation language in documentation.
…zation details and enhance JSON schema by adding required config property.
…tion to ensure clear and grouped permission presentation for users.
@amithanda amithanda force-pushed the feature/identity-linking-extensibility branch from 0d3fc0a to 086f2cb Compare March 18, 2026 02:14
… by specifying exact scope requests and enforcing strict issuer comparison without normalization.
@amithanda
Copy link
Contributor Author

Great design. The mechanism registry pattern is exactly right — hardcoding OAuth as the only path was the main limitation of the previous spec.

This pairs well with #264 (attestation extension for eligibility). The two address different surfaces:

But there's also a natural overlap: wallet attestation as an identity mechanism type. Some commerce flows don't need OAuth account linking at all — the wallet is the identity. A business selling token-gated merchandise needs to verify holdings, not link an account.

The registry already supports this cleanly:

"supported_mechanisms": [
    {
        "type": "oauth2",
        "issuer": "https://auth.merchant.example.com"
    },
    {
        "type": "wallet_attestation",
        "provider_jwks": "https://verifier.example.com/.well-known/jwks.json",
        "attestation_endpoint": "https://verifier.example.com/v1/attest"
    }
]

The platform calls the attestation endpoint, receives a signed payload with kid + sig, and the business verifies offline via JWKS — the same pattern from #264 and the signals work in #203. No redirect flow, no token exchange, no account creation. The base mechanism schema's additionalProperties: true means this works today without schema changes.

For scope derivation: a wallet_attestation mechanism wouldn't need OAuth scopes — verification is stateless and self-contained. The pruning algorithm in Step 3 already handles this correctly: capabilities without identity_scopes contribute zero scopes.

One question on the spec: the Discovery Bridging section (RFC 8414 / OIDC fallback hierarchy) is tightly coupled to OAuth. Would it make sense to scope that hierarchy under the oauth2 mechanism section rather than presenting it as general? Other mechanism types would define their own resolution — JWKS endpoint for attestation, DID resolution for verifiable credentials, etc.

Hi Douglas,

wallet_attestation would be a good idea to add in a future PR. I wanted to submit the redesign for OAuth for this PR, so that other schemes can be added in future.
For the second point, I think it is already nested under oauth2 mechanism. Please feel free to check again and let me know if I am missing something.

@amithanda amithanda added the TC review Ready for TC review label Mar 18, 2026
@douglasborthwick-crypto

For the second point, I think it is already nested under oauth2 mechanism.

You're right — I re-read the updated spec and Discovery Bridging is correctly scoped as a subsection of ### OAuth 2.0 ("type": "oauth2"). The RFC 8414 → OIDC fallback hierarchy is mechanism-specific, not general. A future mechanism type would define its own resolution path at the same level. Looks good.

Copy link
Collaborator

@drewolson-google drewolson-google left a comment

Choose a reason for hiding this comment

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

This generally seems good to me. The one concern I have is around negotiated scopes and linking failures. See my comment below.


1. **Schema Declaration:** Each individual capability schema explicitly defines
its own required identity scopes (e.g., `dev.ucp.shopping.checkout` declares
`dev.ucp.shopping.scopes.checkout_session`).
Copy link
Collaborator

Choose a reason for hiding this comment

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

Help me understand this a bit. Capabilities will work with a linked identity and without a linked identity -- checkout is a good example of this. Is this idea that if an identity is linked then the link must contain all of the scopes from the negotiated set of capabilities?

If so, this feels a bit odd, in that the capabilities worked before identity linking, when there were no scopes at all, and could work post-identity linking even without the appropriate scopes by falling back to the behavior without a linked identity.

Thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@drewolson-google
No, This is to facilitate identity linking itself not the case where identity is already linked.
Today, the discovery of identity linking url's and scopes is not very well defined via UCP. This PR allows us to have a UCP capability dev.ucp.common.identity_linking , where you can find out the auth scheme, config that both platform and business can support, algorithm for agents to facilitate identity_linking. The scopes are used during this initial identity_linking flow based on the capabilities.

Once the identities are linked, this configuration is not used as the tokens are used directly to facilitate authentication can call operations on capabilties. Let me know if that makes sense or if you suggest I add some more documentation to clarify this piece better somewhere.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Got it, makes sense, thanks!

Copy link
Contributor

@jingyli jingyli left a comment

Choose a reason for hiding this comment

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

Added some nits/naive questions but overall looks good!

Non-blocking, but one side question I'm curious about is how do we think about extension application (if any) on this new identity linking capability moving forward given the schema is focused on the discovery & negotiation of authorization/linking between platforms & businesses?

returns exactly `404 Not Found`, the platform **MUST** append
`/.well-known/openid-configuration` to the defined `issuer` string and fetch.
If this final fetch returns any non-2xx response or a network error, the
platform **MUST** abort the identity linking process.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This should also be abort the discovery process for consistency with the steps above..?

Copy link
Contributor Author

@amithanda amithanda Mar 19, 2026

Choose a reason for hiding this comment

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

This is the last stage of discovery so if this fails, we want platform to abort the identity linking process. We could have said "abort the discovery process" but I think both are valid. Let me know if you feel strongly about changing this. I don't have a strong opinion and both seem ok to me.


#### For businesses

- **MUST** implement OAuth 2.0
Copy link
Contributor

Choose a reason for hiding this comment

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

A super naive QQ: Here we are saying business MUST implement OAuth 2.0, a few lines above we also mention **MUST** implement the OAuth 2.0 Authorization Code flow for platforms.

Given these 2 bullets, wouldn't the negotiated mechanism type always be oauth2 (which feels like it's going against the main design point around the mechanism registry)..?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are both mentioned under OAuth 2.0 ("type": "oauth2") heading, so they apply only when Supported Mechanism is oauth2. When we add new mechanisms types, we will have a separate section under Supported Mechanisms heading for that mechanism, at the same level as OAuth 2.0. Does that make sense?

the finalized intersection. If a capability (e.g., `order`) is excluded from
the active capability set, its respective scopes **MUST NOT** be requested by
the platform. If the final derived scope list is completely empty, the platform
**SHOULD** abort the identity linking process, as there are no secured resources
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why this guideline is a SHOULD rather than a MUST if there are no secured resources to authorize as part of the linking process (is there ever a use case for showing an empty consent screen)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.
The only weak argument for keeping SHOULD could be forward-compatibility with future mechanism types that don't use scopes — but the spec already handles that through the mechanism registry: future non-scope-based mechanisms would define their own requirements in their own section and wouldn't be constrained by this sentence.
I will flip it to MUST.

- For single-parent extensions (`extends: "string"`): parent must be present
- For multi-parent extensions (`extends: ["a", "b"]`): at least one parent
must be present
- **Scope Dependencies**: Remove any capability declaring `identity_scopes`
Copy link
Contributor

Choose a reason for hiding this comment

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

Naive QQ on this: Given the UCP checkout capability will declare identity_scopes per the change on checkout.json schema below, does this mean it will always be removed if dev.ucp.common.identity_linking is not part of the intersection between business & platform?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think that is how it should work. identity_scopes on checkout.json is checkout's declaration that it requires an authorized Bearer token to operate. Without identity_linking in the intersection, there's no negotiated mechanism to obtain that token — so keeping checkout active would leave the platform with a capability it can't securely call. Pruning it is the correct outcome.

In practice this means identity_linking acts as a soft prerequisite for any capability declaring identity_scopes. Both parties need to advertise it for checkout to survive the intersection.

Good callout though, let me know if we should add something more to the documentation to make it clearer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean you can only use the checkout capability if you also use the identity linking capability?

…tion of the process when no secured resources are available and refine scope mapping for CheckoutSession.
@amithanda
Copy link
Contributor Author

Added some nits/naive questions but overall looks good!

Non-blocking, but one side question I'm curious about is how do we think about extension application (if any) on this new identity linking capability moving forward given the schema is focused on the discovery & negotiation of authorization/linking between platforms & businesses?

Yes, that may not make sense for this capability, given the purpose is to define the mechanism/config for identity_linking. It will just extend via new mechanism types. If you were thinking about the loyalty use cases, I think the extensions there would probably apply to cart/checkout/order etc. For identity management, I think we will probably need a new capability (e.g Profile or Account) which will be a separate from this capability (identity_linking).

Let me know if that makes sense.

@amithanda amithanda merged commit 31b7f86 into Universal-Commerce-Protocol:main Mar 19, 2026
6 checks passed
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