Skip to content

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

Open
amithanda wants to merge 15 commits intoUniversal-Commerce-Protocol:mainfrom
amithanda:feature/identity-linking-extensibility
Open

feat!: redesign identity linking with mechanism registry and capability-driven scopes#265
amithanda wants to merge 15 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

amithanda and others added 15 commits March 14, 2026 08:08
…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.
@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.

Comment on lines +175 to +179
*Issuer Normalization*: To prevent brittle integrations, the platform **MUST**
normalize both the locally configured `issuer` and the remotely returned `issuer`
by stripping any trailing slashes (`/`) before performing the exact string match
comparison (i.e., `https://auth.example.com` and `https://auth.example.com/`
**MUST** be evaluated as identical).

Choose a reason for hiding this comment

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

Under RFC 8414 3.3 it mentions that the string must be an identical match.
I understand that the intent to reduce brittle integrations due to a simple slash, but this can have other consequences. In the case of a trailing slash, the exact string would be incldued in the the signed iss claim of the JWT access token, and standard JWT validation libraries will check for equality (not stripping). If UCP strips the slash during discovery and passes the normalized string, the equality will fail.

Can you drop the normalization step? And can we instead add a mandate that the issuer string in UCP must match exactly the metadata, and the iss parameter?

Comment on lines +122 to +136
### Scope Naming Convention

Scopes **MUST** use **reverse DNS dot notation**, consistent with UCP capability
names, to prevent namespace collisions:

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

Example capability-to-scope mapping based on UCP schemas:

| Resources | Operation | Scope Action |
| :-------------- | :-------------------------------------------- | :----------------------------------------- |
| CheckoutSession | Get, Create, Update, Delete, Cancel, Complete | `dev.ucp.shopping.scopes.checkout_session` |

Choose a reason for hiding this comment

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

+1 for the change from colon-separation to reverse DNS. It aligns with what I've seen in PR 250 for eligibility and other UCP changes.

From an OAuth perspective, RFC 6749 3.3 restricts scope tokens to %x21 / %x23-5B / %x5D-7E, which makes dots perfectly valid and widely supported by Authorization Servers.

(Optionally) add a short note mentioning this format strictly adheres to the scope token syntax defined in RFC 6749 Section 3.3.

Comment on lines +103 to +104
3. **Authorization:** The platform initiates the connection requesting **only**
the derived scopes. If a capability (e.g., `order`) is excluded from the

Choose a reason for hiding this comment

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

Are we intentionally pushing platforms toward incremental authorization here? If a platform negotiates both checkout and order, but only needs checkout for the current session, requesting a subset of scopes means they'll have to trigger a re-linking flow later for order management. This introduces UX friction compared to requesting the full negotiated union upfront.

To clarify the intent, I recommend replacing "only" with either "at most" (if we want to allow subsets for progressive consent) or "exactly" (if we expect platforms to request the full derived set to minimize re-prompting).

- **MUST** adhere to [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)
to declare the location of their OAuth 2.0 endpoints
(`/.well-known/oauth-authorization-server`)
- **SHOULD** populate `scopes_supported` in their RFC 8414 metadata to allow

Choose a reason for hiding this comment

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

Should this be MUST? Given that the Platform will only request the derived scopes.

Comment on lines +165 to +169
3. **OIDC Fallback (Lowest Priority)**: If and only if the RFC 8414 fetch
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.

Choose a reason for hiding this comment

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

no actionable comment, just wanted to mention that having this as a fallback is a great idea

Comment on lines +220 to +221
- **MUST** strictly implement Proof Key for Code Exchange (PKCE)
([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)) using the `S256`

Choose a reason for hiding this comment

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

This seems like an extension going towards what OAuth 2.1 is pushing for. OAuth 2.0 doesn't specifiy it, and OAuth 2.1 isn't, as of today, an official RFC. Do you want to already include PKCE as an extension of UCP or hold off and add it later? Oauth 2.1 Draft

I suspect this may be significant overhead, especially as a MUST, for platforms and businesses while it's not-quite-yet industry standard.

Choose a reason for hiding this comment

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

Feel free to dismiss this one. PKCE S256 is supported more widely than I anticipated.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants