Delegation API

Draft Community Group Report,

This version:
https://w3c-fedid.github.io/delegation/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google Inc.)

Abstract

This specification extends the Federated Credential Management API Federated Credential Management API so that an identity provider can delegate the presentation of identity assertions to the user agent. The identity provider acts as an issuer of a selectively disclosable credential bound to a key that the user agent generates, and the user agent acts as a holder that discloses only the claims the user agreed to share, bound to the relying party (the verifier). This way, users can sign in to relying parties without the identity provider learning which relying parties they sign in to.

Status of this document

This specification was published by the Federated Identity Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

In Federated Credential Management API, the user agent mediates sign-in between an RP and an IDP, but the IDP still issues an identity assertion for a specific RP at its identity assertion endpoint. That request tells the IDP which RP the user is signing in to (the IdP Tracking Problem).

This specification lets an IDP split itself in two, following the issuer-holder-verifier model of Verifiable Credentials:

An IDP opts in by using a VC issuance endpoint instead of an identity assertion endpoint in its config file:

{
  "accounts_endpoint": "/accounts",
  "login_url": "/login",
  "vc_issuance_endpoint": "/issue"
}

An RP opts in by choosing the config URL of such a config file. Nothing else in its request changes, except that it passes a nonce for the key binding:

const {token} = await navigator.credentials.get({
  identity: {
    providers: [{
      configURL: "https://issuer.example/vc.json",
      fields: ["email"],
      nonce: "5678"
    }]
  }
});

The flow is the same as in Federated Credential Management API until the user selects an account. Then:

  1. The user agent generates an ephemeral key pair, the holder key.

  2. The user agent sends the selected account’s ID and the public holder key to the VC issuance endpoint, with the user’s cookies but without anything that identifies the RP.

  3. The issuer returns an SD-JWT bound to the public holder key, with the user’s claims as selective disclosures.

  4. The user agent keeps only the disclosures that the RP asked for and the user agreed to share, and signs a key binding JWT with the RP’s origin as its audience and the RP’s nonce. Then it discards the private holder key.

  5. The RP receives the SD-JWT+KB in token.

Note: This specification is meant to compose with other FedCM extensions. For example, with IdP Registration API, an RP could accept a delegated credential from any registered issuer, while keeping that issuer from learning who the RP is.

Should issuers be able to issue delegated credentials ahead of time (the "push model"), so that the user agent can present them without contacting the issuer during sign-in?

This specification is written as a set of monkey patches to Federated Credential Management API, shown like this:

A change to an algorithm or definition in another specification.

2. Issuers

2.1. The config file

partial dictionary IdentityProviderAPIConfig {
  USVString vc_issuance_endpoint;
};
vc_issuance_endpoint, of type USVString

The URL of the issuer’s VC issuance endpoint, relative to the config file.

The vc_issuance_endpoint and the id_assertion_endpoint are mutually exclusive: a config file has one or the other. A config file that has a vc_issuance_endpoint is a delegated config. If a config file has both, the vc_issuance_endpoint is used, and the id_assertion_endpoint is ignored.

Note: An RP chooses whether it gets an identity assertion or a delegated credential by choosing a config URL. An IDP that supports both publishes two config files, for example config.json and vc.json. Both can pass the well-known file check if the well-known file lists the accounts_endpoint and login_url that they share, rather than a single provider_urls entry.

In Federated Credential Management API, remove the required keyword from the id_assertion_endpoint member of IdentityProviderAPIConfig.

In fetch the config file, after the step that converts the response to an IdentityProviderAPIConfig stored in config, add the following step:

  1. If neither config["id_assertion_endpoint"] nor config["vc_issuance_endpoint"] exists, set config to failure.

2.2. The VC issuance endpoint

The VC issuance endpoint is like the identity assertion endpoint, except that it doesn’t learn who the RP is. The user agent sends it a POST request, with the user’s cookies, with an opaque Origin, and without a referrer. The request body is application/x-www-form-urlencoded with the following parameters, and nothing else:

account_id

The id of the account the user selected, as for the identity assertion endpoint.

holder_key

The public holder key, serialized as a JSON Web Key JSON Web Key (JWK).

POST /issue HTTP/1.1
Host: issuer.example
Origin: null
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Sec-Fetch-Dest: webidentity
Cookie: 0x23223

account_id=123&holder_key=%7B%22kty%22%3A%22EC%22%2C%22crv%22%3A%22P-256%22%2C%22x%22%3A%22...%22%2C%22y%22%3A%22...%22%7D

Note: Unlike the request to the identity assertion endpoint, this request doesn’t include client_id, params, fields, disclosure_text_shown, disclosure_shown_for, or is_auto_selected, and its Origin is opaque, because any of those could reveal the RP.

The endpoint responds as the identity assertion endpoint does, with an IdentityAssertionResponse. Its token is a delegated credential. The issuer can also use continue_on to show its own UI first, as with the identity assertion endpoint, or error.

A delegated credential is an SD-JWT Selective Disclosure for JWTs (SD-JWT), serialized as a string in the compact form <Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~, such that:

The payload of the Issuer-signed JWT:
{
  "iss": "https://issuer.example",
  "iat": 1760295600,
  "exp": 1760299200,
  "cnf": {
    "jwk": {"kty": "EC", "crv": "P-256", "x": "...", "y": "..."}
  },
  "_sd": ["6phoD5MBMETkuf5BYia7JD01dubqJSwGuz-o_9M206E", "..."],
  "_sd_alg": "sha-256"
}

And its disclosures, before base64url encoding:

["6403945e2c0a34a4", "name", "Alice Doe"]
["f3e080eb88b5f735", "email", "alice@example.com"]

Note: Because the issuer names its disclosures with the same field names that RPs request, the user agent doesn’t need to map between taxonomies: it discloses a claim if its name is one of the requested fields.

How are nested claims and array elements handled? How is the taxonomy extended beyond the list of recognized fields?

How does a verifier find the issuer’s public keys? Options include the jku header, the jwks_uri of the issuer’s metadata, or a well-known location keyed by iss.

Claims that the issuer puts in the clear, such as a stable sub, are revealed to every RP. See § 5 Privacy considerations.

3. Verifiers

3.1. Requesting a delegated credential

An RP requests a delegated credential by passing the config URL of a delegated config. It doesn’t pass a new parameter to choose the credential’s format.

partial dictionary IdentityProviderRequestOptions {
  USVString nonce;
};
nonce, of type USVString

A challenge from the RP, which the holder puts in the key binding JWT so that the RP can check that the presentation is fresh. It is required when the configURL points to a delegated config, and is never sent to the IDP.

Note: In Federated Credential Management API, an RP passes its nonce in params, which is sent to the identity assertion endpoint. For a delegated config, params is never sent to the IDP, because it could identify the RP. So the nonce needs its own member, which only the holder reads.

In Federated Credential Management API, remove the required keyword from the clientId member of IdentityProviderConfig. Every algorithm in Federated Credential Management API that reads clientId MUST first check that the member exists, and treat its absence as failure, unless the provider’s config file is a delegated config.

Note: An RP doesn’t need to register with an issuer to get a delegated credential, because the issuer never learns who the RP is. A clientId that the RP passes is never sent to the issuer.

3.2. Verifying a delegated credential

This section is non-normative.

The RP receives the delegated credential in token, as a string in the compact form <Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure M>~<KB-JWT> Selective Disclosure for JWTs (SD-JWT). To verify it, the RP:

  1. Verifies the signature of the Issuer-signed JWT with the issuer’s public key, checks that iss is the issuer it expects (for example, by comparing it with configURL), and checks exp.

  2. For each disclosure, checks that its digest is in the Issuer-signed JWT’s _sd.

  3. Verifies the signature of the KB-JWT with the key in the Issuer-signed JWT’s cnf.jwk.

  4. Checks that the KB-JWT’s aud is its own origin, that its nonce is the nonce it passed, that its iat is recent, and that its sd_hash matches the rest of the presentation.

  5. Accepts the disclosed claims.

4. The user agent as a holder

This section defines how the user agent acts as the holder: it generates a holder key, fetches a delegated credential from the VC issuance endpoint, and presents it to the RP.

4.1. Holder keys

A holder key is an ECDSA key pair on the P-256 curve, used with the ES256 algorithm, that the user agent generates for a single sign-in. The user agent MUST generate a new holder key for each request to a VC issuance endpoint, MUST NOT reveal its private key to anyone, and MUST discard its private key as soon as it has signed the key binding JWT.

Note: Because each holder key is new, the public key that the issuer sees can’t be used to link sign-ins together, or to the RP that receives the presentation.

Should user agents support other algorithms? Should the issuer be able to say which ones it accepts?

4.2. Fetching a delegated credential

To fetch a delegated credential given a string accountId, an IdentityProviderRequestOptions provider, an IdentityProviderAPIConfig config, and a globalObject, run the following steps. This returns a tuple of (a string token, a string account ID), or failure.
  1. If provider["nonce"] doesn’t exist, return failure.

  2. Let issuanceUrl be the result of computing the manifest URL given provider, config["vc_issuance_endpoint"], and globalObject. If issuanceUrl is failure, return failure.

  3. Let holderKey be a new holder key.

  4. Let list be « ("account_id", accountId), ("holder_key", the JSON Web Key JSON Web Key (JWK) serialization of holderKey’s public key) ».

  5. Let requestBody be the result of running the urlencoded serializer with list.

  6. Let request be a new request as follows:

    URL

    issuanceUrl

    method

    POST

    body

    the UTF-8 encoding of requestBody

    redirect mode

    "error"

    client

    null

    service-workers mode

    "none"

    destination

    "webidentity"

    origin

    a new opaque origin

    header list

    a list containing a header named Accept with value application/json, and a header named Content-Type with value application/x-www-form-urlencoded

    referrer policy

    "no-referrer"

    credentials mode

    "include"

    mode

    "no-cors"

  7. Let sdJwt be null.

  8. Fetch request with request and globalObject, and with processResponseConsumeBody set to the following steps given a response response and responseBody:

    1. If responseBody is null or failure, or response’s status is not an ok status, set sdJwt to failure and return.

    2. Let json be the result of extract the JSON fetch response from response and responseBody.

    3. Convert json to an IdentityAssertionResponse, result.

    4. If one of the previous two steps threw an exception, or result["error"] exists, set sdJwt to failure and return.

    5. If result["token"] exists, set sdJwt to it and return.

    6. If result["continue_on"] exists, run these steps in parallel:

      1. Let continueOnUrl be the result of running parse url with result["continue_on"] and globalObject.

      2. If continueOnUrl is failure, or it is not same origin with issuanceUrl, set sdJwt to failure and return.

      3. Let tokenPair be the result of show a continuation dialog with continueOnUrl.

      4. If tokenPair is failure, set sdJwt to failure and return.

      5. Set sdJwt to the first entry of tokenPair. If the second entry of tokenPair is not null, set accountId to it.

    7. Otherwise, set sdJwt to failure.

  9. Wait for sdJwt to be set.

  10. If sdJwt is failure or is not a string, return failure.

  11. Let token be the result of present a delegated credential with sdJwt, holderKey, provider, and globalObject.

  12. Discard holderKey’s private key.

  13. If token is failure, return failure.

  14. Return (token, accountId).

The continuation dialog is opened from the RP’s page. Check that nothing in it (for example, the opener or the referrer) reveals the RP to the issuer.

4.3. Presenting a delegated credential

To present a delegated credential given a string sdJwt, a holder key holderKey, an IdentityProviderRequestOptions provider, and a globalObject, run the following steps. This returns a string or failure.
  1. Let parts be the result of strictly splitting sdJwt on "~".

  2. If parts has fewer than 2 items, or its last item isn’t the empty string, return failure.

    Note: The last item being empty means sdJwt doesn’t already have a key binding JWT.

  3. Let issuerJwt be parts[0], and disclosures be the remaining items of parts except the last.

  4. Let payload be the result of decoding the payload of issuerJwt as a JWT JSON Web Token (JWT). If that fails, return failure.

  5. If payload["cnf"]["jwk"] doesn’t exist, or isn’t the same key as holderKey’s public key (for example, by comparing their JWK thumbprints JSON Web Key (JWK) Thumbprint), return failure.

  6. Let hashAlgorithm be payload["_sd_alg"] if it exists, and "sha-256" otherwise. If the user agent doesn’t support hashAlgorithm, return failure.

  7. Let fields be provider["fields"] if it exists, and « "name", "email", "picture" » otherwise.

  8. Let allowedNames be the items of fields that are in the list of recognized fields.

  9. Let selected be an empty list.

  10. For each disclosure of disclosures:

    1. Let decoded be the result of base64url-decoding disclosure and parsing it as JSON. If that fails, or decoded isn’t an array of 3 elements whose second element is a string, continue.

    2. If allowedNames doesn’t contain decoded[1], continue.

    3. If the base64url encoding of the hashAlgorithm digest of disclosure isn’t in payload["_sd"], continue.

    4. Append disclosure to selected.

  11. Let presented be issuerJwt, followed by "~", followed by the items of selected each followed by "~".

  12. Let kbJwt be a JWT JSON Web Token (JWT), signed with holderKey’s private key using ES256 JSON Web Signature (JWS), whose header is {"typ": "kb+jwt", "alg": "ES256"} and whose payload has the following claims:

    iat

    The current time, in seconds since the epoch.

    aud

    The serialization of globalObject’s associated Document’s origin.

    nonce

    provider["nonce"].

    sd_hash

    The base64url encoding of the hashAlgorithm digest of presented.

  13. Return presented followed by kbJwt.

Note: Fetch an identity assertion is only reached after the user agreed to share the RP’s fields, or after an earlier sign-in that connected the account to the RP. The user agent never discloses more than the fields the RP requested.

When the user agent doesn’t ask for permission (for example, for a returning user), should the user agent disclose the fields that the user agreed to share last time, rather than the fields that the RP requests now?

Should the user agent reject a delegated credential that has the user’s claims in the clear, since it can’t keep them from the RP?

4.4. Changes to FedCM

In fetch an identity assertion, insert the following steps at the beginning:
  1. If config is a delegated config:

    1. Let result be the result of fetch a delegated credential with accountId, provider, config, and globalObject.

    2. If result is failure, queue a global task on the DOM manipulation task source given globalObject to let error be the result of create an IdentityCredentialError given {} and globalObject. Wait for error to be set, and return it.

    3. Set tokenValue to result’s first item and accountId to its second item, and continue with the step "If isAutoSelected is false, create a connection between the RP and the IdP account", skipping the steps in between.

Note: The connection is stored only in the user agent’s connected accounts set. The issuer doesn’t learn about it.

In create an IdentityCredential, in the step that sets clientMetadataMap[configURL] to the result of fetch the client metadata, add the condition "and provider’s config file is not a delegated config".

Note: Fetching the client metadata sends client_id to the IDP, which would reveal the RP. Without it, request permission to sign-up doesn’t show the RP’s privacy policy or terms of service. The user agent can still tell whether the RP is third-party to the top-level frame by itself, without the client_is_third_party_to_top_frame_origin value from the IDP.

Where should a verifier’s privacy policy and terms of service come from, if not from the IDP? For example, the RP could pass them itself.

In attempt to disconnect, after the step that fetches the config file into config, insert the following step:
  1. If config is a delegated config, remove all connections given the embedder origin, the RP origin, and the IDP origin, as computed later in this algorithm, resolve promise, and return.

Note: The disconnect endpoint is never fetched for a delegated config, because send a disconnect request sends client_id to the IDP, and the IDP has nothing to disconnect: it never learned about the connection.

5. Privacy considerations

Request Credentials Reveals the RP?
Config file and well-known file No No
Accounts endpoint Yes No
Client metadata endpoint Not fetched for a delegated config
VC issuance endpoint Yes No
Disconnect endpoint Not fetched for a delegated config

How should an RP get a stable, but pairwise, identifier for the user, if the issuer doesn’t know who the RP is?

6. Security considerations

7. Acknowledgements

Thanks to the Federated Identity Community Group for their feedback on this proposal.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. Key words for use in RFCs to Indicate Requirement Levels

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ENCODING]
Anne van Kesteren. Encoding Standard. Living Standard. URL: https://encoding.spec.whatwg.org/
[FEDCM]
Nicolas Pena Moreno. Federated Credential Management API. URL: https://w3c-fedid.github.io/FedCM/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Non-Normative References

[IDP-REGISTRATION]
Sam Goto. IdP Registration API. CG-DRAFT. URL: https://w3c-fedid.github.io/idp-registration/
[RFC7515]
M. Jones; J. Bradley; N. Sakimura. JSON Web Signature (JWS). May 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7515/
[RFC7517]
M. Jones. JSON Web Key (JWK). May 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7517/
[RFC7519]
M. Jones; J. Bradley; N. Sakimura. JSON Web Token (JWT). May 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7519/
[RFC7638]
M. Jones; N. Sakimura. JSON Web Key (JWK) Thumbprint. September 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7638/
[RFC7800]
M. Jones; J. Bradley; H. Tschofenig. Proof-of-Possession Key Semantics for JSON Web Tokens (JWTs). April 2016. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7800/
[SD-JWT]
Daniel Fett; Kristina Yasuda; Brian Campbell. Selective Disclosure for JWTs (SD-JWT). Internet-Draft. URL: https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/

IDL Index

partial dictionary IdentityProviderAPIConfig {
  USVString vc_issuance_endpoint;
};

partial dictionary IdentityProviderRequestOptions {
  USVString nonce;
};

Issues Index

Should issuers be able to issue delegated credentials ahead of time (the "push model"), so that the user agent can present them without contacting the issuer during sign-in? ↵
How are nested claims and array elements handled? How is the taxonomy extended beyond the list of recognized fields? ↵
How does a verifier find the issuer’s public keys? Options include the jku header, the jwks_uri of the issuer’s metadata, or a well-known location keyed by iss. ↵
Claims that the issuer puts in the clear, such as a stable sub, are revealed to every RP. See § 5 Privacy considerations. ↵
Should user agents support other algorithms? Should the issuer be able to say which ones it accepts? ↵
The continuation dialog is opened from the RP’s page. Check that nothing in it (for example, the opener or the referrer) reveals the RP to the issuer. ↵
When the user agent doesn’t ask for permission (for example, for a returning user), should the user agent disclose the fields that the user agreed to share last time, rather than the fields that the RP requests now? ↵
Should the user agent reject a delegated credential that has the user’s claims in the clear, since it can’t keep them from the RP? ↵
Where should a verifier’s privacy policy and terms of service come from, if not from the IDP? For example, the RP could pass them itself. ↵
How should an RP get a stable, but pairwise, identifier for the user, if the issuer doesn’t know who the RP is? ↵