Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions spec/index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ spec: login-status; urlPrefix: https://w3c-fedid.github.io/login-status
text: unknown; url: unknown
text: get the login status; url: get-the-login-status
text: set the login status; url: set-the-login-status
spec: service-workers; urlPrefix: https://w3c.github.io/ServiceWorker/
type: dfn
text: register; url: register
text: unregister; url: unregister
text: soft update; url: soft-update
text: create job; url: create-job
text: schedule job; url: schedule-job
text: registration map; url: dfn-scope-to-registration-map
type: dfn; for: job
text: update via cache mode; url: dfn-job-update-via-cache-mode
text: referrer; url: job-referrer
text: worker type; url: dfn-job-worker-type
</pre>

<pre class=link-defaults>
Expand All @@ -69,6 +81,13 @@ spec:fetch; type:dfn; for:/; text:response
spec:css-color-5; type:type; text:<color>
spec:html; type:dfn; text:allowed to use
spec:html; type:dfn; for:/; text:same site
spec:service-workers; type:dfn; for:/; text:service worker
spec:service-workers; type:dfn; for:/; text:service workers
spec:service-workers; type:dfn; for:/; text:service worker registration
spec:storage; type:dfn; text:storage key
spec:html; type:dfn; text:parallel queue
spec:html; type:dfn; text:start a new parallel queue
spec:html; type:dfn; text:enqueue steps
</pre>

<style>
Expand Down Expand Up @@ -1296,6 +1315,28 @@ or failure.
1. Let |allowed_config_url| be the result of [=computing the manifest URL=] with |provider|,
|wellKnown|.{{IdentityProviderWellKnown/provider_urls}}[0], and |globalObject|.
1. If |allowed_config_url| is not [=url/equal=] to |configUrl|, return failure.
1. Let |idpOrigin| be |configUrl|'s [=url/origin=].
1. If |wellKnown|'s {{IdentityProviderWellKnown/identity_handler}} is present, then
Comment thread
Brandr0id marked this conversation as resolved.
[=enqueue steps=] to |idpOrigin|'s [=identity handler queue=] to
[=register the identity handler=] given |configUrl| and |wellKnown|'s
{{IdentityProviderWellKnown/identity_handler}}.
1. Otherwise, [=enqueue steps=] to |idpOrigin|'s [=identity handler queue=] to

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this a common pattern, eg having a queue per origin? Is it needed here, or is having a single queue for this type of task enough?

[=unregister the identity handler=] given |idpOrigin|.

Note: The identity handler is keyed to |configUrl|'s [=/origin=], not to the [=/origin=] the
[=well-known file=] was fetched from. Every credentialed endpoint is [=same origin=] with
|configUrl| (see [=computing the manifest URL=]), so keying the handler this way guarantees
its registration scope can cover the endpoints it intercepts, regardless of where the
[=well-known file=] is hosted.

Note: These steps are only reached when the [=well-known file=] was fetched successfully, so
a network error or an unparsable [=well-known file=] leaves an existing registration
unchanged. An [=IDP=] disables interception by serving a [=well-known file=] without an
{{IdentityProviderWellKnown/identity_handler}} member, not by making the file unavailable.

Note: These steps do not wait for the registration to complete. A concurrent FedCM request
for the same [=IDP=] runs on the same [=identity handler queue=], so it observes a
consistent registration state rather than racing this one.
1. Return |config|.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IdentityProviderWellKnown.provider_urls is a list and can hold URLs on distinct [=/origins=] under the same eTLD+1. Because registration is keyed to |configUrl|'s origin, each origin gets its own [=FedCM identity-handler storage key=] and its own independent registration — all sourced from the same well-known identity_handler.service_worker value.

Note: If a [=well-known file=] lists multiple [=/origins=] in {{IdentityProviderWellKnown/provider_urls}}, each [=/origin=] has its own [=FedCM identity-handler storage key=] and its own registration, even though they share a single {{IdentityProviderWellKnown/identity_handler}} declaration.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Would fetching the well-known file end up in failure if provider_urls are > 1 currently due to :

  1. If one of the previous two steps threw an exception, or if the
    [=list/size=] of |wellKnown|["{{IdentityProviderWellKnown/provider_urls}}"] is
    greater than 1, set |wellKnown| to failure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're right, I missed the size > 1 failure at L1249–1251. So this is moot on today's spec text.


</div>
Expand All @@ -1316,11 +1357,172 @@ to keep their actual config files on an arbitary path while allowing the user ag
path manipulation to fingerprint (for instance, by including the RP in the path). See
[[#manifest-fingerprinting]].

<!-- ============================================================ -->
### Register the identity handler ### {#register-identity-handler}
<!-- ============================================================ -->

An [=IDP=] MAY declare a FedCM-specific [=/service worker=] in its [=well-known file=] using the
{{IdentityProviderWellKnown/identity_handler}} member. When it is present, the [=user agent=]
registers and manages this [=/service worker=] itself and dispatches credentialed FedCM requests to
it. The [=IDP=] page does not register the [=/service worker=]: because credentialed FedCM requests
can happen long after the [=IDP=] page was last visited, the [=user agent=] is responsible for
ensuring the declared [=/service worker=] is registered and has an [=active worker=] when it is
needed.

Note: There is no separate "enabled" switch. An [=IDP=] disables interception by removing the
{{IdentityProviderWellKnown/identity_handler}} member from its [=well-known file=], which causes the
[=user agent=] to [=unregister the identity handler=] and use the direct-network path.

Each [=/origin=] has an associated <dfn>FedCM identity-handler storage key</dfn>: a
[=storage key=] the [=user agent=] constructs for that [=/origin=]'s FedCM identity handler.
It MUST be distinct from the [=/origin=]'s first-party [=storage key=], such that:

1. The two do not [=storage key/equals|compare as equal=], so that a [=registration map=] lookup
given one of them never returns a [=service worker registration=] stored under the other;
1. No Web API surface accepts this [=storage key=] or otherwise allows page script to cause a
[=service worker registration=] to be stored under it; and
1. It is derivable from the [=/origin=] alone by the [=user agent=], so that a later FedCM flow can
recompute it to look up or [=unregister=] the [=/service worker=] without any persisted state.

Note: This isolation ensures FedCM registrations neither collide with the [=IDP=]'s ordinary
first-party [=/service workers=] nor are reachable or removable by unrelated code sharing the
[=IDP=]'s [=/origin=].

Note: When the [=user agent=] later dispatches a credentialed request to the identity handler, it
uses this explicit, [=user agent=]-only association between the [=IDP=] and the registration it
created (keyed by the [=FedCM identity-handler storage key=]), rather than the implicit service
worker match path used for client-controlled fetches. Because a credentialed FedCM request has no
controlling client, this ensures the request reaches only a [=/service worker=] the [=user agent=]
registered for that specific [=IDP=].

Each [=/origin=] has an associated <dfn>identity handler queue</dfn>, which is a
[=parallel queue=] the [=user agent=] [=starts a new parallel queue|starts=] the first time it is
needed. [=Register the identity handler=] and [=unregister the identity handler=] run on it, so at
most one of them runs at a time for a given [=/origin=] and their effects are observable in the
order they were enqueued.

Note: Concurrent FedCM requests for the same [=IDP=] can otherwise observe different
[=well-known files=] from the HTTP cache and race to opposite conclusions about whether an identity
handler is declared.

<div algorithm>
To <dfn>register the identity handler</dfn> given a [=/URL=] |configUrl| and an

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we avoid calling this configUrl? Perhaps serviceWorkerUrl or swUrl?

{{IdentityHandler}} |handler|, run these steps:

1. Assert: These steps are running on |configUrl|'s [=url/origin=]'s
[=identity handler queue=].
1. Let |idpOrigin| be |configUrl|'s [=url/origin=].
1. Let |scriptURL| be the result of running the [=URL parser=] on |handler|'s
{{IdentityHandler/service_worker}} with |configUrl| as the base URL.
1. If |scriptURL| is failure, return.
1. If |scriptURL|'s [=url/origin=] is not [=same origin=] with |idpOrigin|, return.

Note: A cross-origin declaration is ignored, and credentialed requests fall back to the
network.
1. Let |scope| be the result of running the [=URL parser=] on "`./`" with |scriptURL| as the
Comment thread
Brandr0id marked this conversation as resolved.
base URL.

Note: This matches the [=/scope=] that [[!SERVICE-WORKERS]] derives for a page-initiated
registration that does not pass one explicitly.
1. Let |storageKey| be the [=FedCM identity-handler storage key=] for |idpOrigin|.
1. Let |registration| be the result of running [=match service worker registration=] given
|storageKey| and |scriptURL| [[!SERVICE-WORKERS]].

Note: [=Match service worker registration=] returns the registration whose [=/scope=] is
the longest prefix of |scriptURL|. The [=user agent=] keeps at most one
[=service worker registration=] under |storageKey| (see the teardown step below), so that
is the identity handler's registration if one exists. It is used here in preference to the
[=registration map=] lookups it wraps, because it is the entry point [[!SERVICE-WORKERS]]
exports for this purpose.
1. If |registration| is not null, |registration|'s [=active worker=] is not null, and
|registration|'s [=active worker=]'s [=service worker/script url=] [=url/equals=]
|scriptURL|, then:
1. [=Soft update=] |registration|.
1. Return.

Note: The [=user agent=] proceeds with the [=active worker=] that is already registered and
checks for a newer version in parallel, so update discovery never delays a FedCM request.
Because [=job/update via cache mode=] is "`all`", how often that check reaches the network
is governed by the script's `Cache-Control` and `ETag` headers, which the [=IDP=] controls.
Running it on each invocation, rather than relying on the [=/service worker=]'s own
staleness threshold, is what lets an [=IDP=] ship a fix at an unchanged script URL without
waiting a day for it to be picked up.
1. [=Unregister the identity handler=] given |idpOrigin|.
Comment thread
Brandr0id marked this conversation as resolved.

Note: This step matters most when |registration| is null. If the [=IDP=] moves the declared
script to a path outside the previous [=/scope=], the lookup misses while the earlier
registration is still stored under |storageKey|. Tearing down first also clears a
registration whose script never activated, and keeps at most one
[=service worker registration=] under |storageKey|.
1. Let |job| be the result of running [=create job=] with *register*, |storageKey|, |scope|,
|scriptURL|, null, and null.
1. Set |job|'s [=job/worker type=] to "`classic`".

Note: [=Create job=] does not initialize [=job/worker type=], and [=register=] compares it
against the newest worker's type. {{IdentityHandler}} has no member for selecting a module
worker, so the [=user agent=] always registers a classic one.
1. Set |job|'s [=job/update via cache mode=] to "`all`".
Comment thread
Brandr0id marked this conversation as resolved.

Note: Setting update-via-cache to "`all`" lets the [=IDP=] control update discovery through
the script's `Cache-Control`/`ETag` headers.
1. Set |job|'s [=job/referrer=] to |scriptURL|.

Note: [=Register=] compares |job|'s script URL and [=/scope=] against |job|'s
[=job/referrer=]'s [=/origin=], which [=create job=] otherwise derives from a
[=/service worker client=]. A FedCM registration has no client, so the [=user agent=] sets
the [=job/referrer=] to the declared script URL, which is [=same origin=] with both by
construction.
1. Invoke [=schedule job=] with |job|.

Note: The job has no [=/service worker client=] and no promise, so a script that fails to
fetch or install is not reported to the [=IDP=]. Credentialed requests fall back to the
network until a later FedCM flow retries the registration.
</div>

<div algorithm>
To <dfn>unregister the identity handler</dfn> given an [=/origin=] |idpOrigin|, run these steps:

1. Assert: These steps are running on |idpOrigin|'s [=identity handler queue=].
1. Let |storageKey| be the [=FedCM identity-handler storage key=] for |idpOrigin|.
1. Let |scopes| be an empty [=list=].
1. [=list/For each=] (|entryStorageKey|, |entryScope|) of the [=registration map=]'s
Comment thread
Brandr0id marked this conversation as resolved.
[=map/keys=]:
1. If |entryStorageKey| is |storageKey|, [=list/append=] |entryScope| to |scopes|.
1. [=list/For each=] |scope| of |scopes|:
1. Let |job| be the result of running [=create job=] with *unregister*, |storageKey|,
|scope|, null, null, and null.
1. Invoke [=schedule job=] with |job|.

Note: The [=user agent=] collects the matching [=/scopes=] before scheduling any job, rather
than scheduling while iterating the [=registration map=]. It iterates at all, rather than
[=unregister=]ing one known [=/scope=], because the
{{IdentityProviderWellKnown/identity_handler}} declaration, and with it the declared
[=/scope=], may already be gone by the time these steps run. [=Register the identity handler=]
keeps at most one entry under |storageKey|, so |scopes| is not expected to hold more than one
[=/scope=].
</div>

Note: Because the [=well-known file=] is fetched subject to ordinary HTTP caching, its freshness
lifetime bounds how quickly adding or removing the {{IdentityProviderWellKnown/identity_handler}}
member takes effect. [=IDPs=] are encouraged to serve it with a short `Cache-Control` `max-age` so
that enabling or disabling the identity handler propagates promptly.

The [=/origin=]'s storage, for the purposes of clearing storage [[!STORAGE]] and of any
[[CLEAR-SITE-DATA]] operation targeting that [=/origin=], includes any
[=service worker registration|registrations=] stored under the
[=FedCM identity-handler storage key=] for that [=/origin=], so no FedCM-specific clearing
mechanism is required.

<xmp class="idl">
dictionary IdentityProviderWellKnown {
sequence<USVString> provider_urls;
USVString accounts_endpoint;
USVString login_url;
IdentityHandler identity_handler;
};

dictionary IdentityHandler {
required USVString service_worker;
};

dictionary IdentityProviderIcon {
Expand Down Expand Up @@ -2134,6 +2336,21 @@ The {{IdentityProviderWellKnown}} JSON object has the following semantics:
:: A URL that points to the same location as the {{IdentityProviderAPIConfig/accounts_endpoint}} in [[#idp-api-config-file]]s.
: <dfn>login_url</dfn>
:: A URL that points to the same location as the {{IdentityProviderAPIConfig/login_url}} in [[#idp-api-config-file]]s.
: <dfn>identity_handler</dfn>
:: An optional {{IdentityHandler}} declaring a FedCM-specific [=/service worker=] that the
[=user agent=] registers and manages in order to intercept credentialed FedCM requests.
See [[#register-identity-handler]].
</dl>

The {{IdentityHandler}} JSON object has the following semantics:

<dl dfn-type="dict-member" dfn-for="IdentityHandler">
: <dfn>service_worker</dfn>
:: The [=/service worker=] script URL, which is [=same origin=] with the [=config file=], so
that a party who controls a different subdomain of the [=IDP=] cannot declare an identity
handler for this [=/origin=]. The [=user agent=] derives the registration [=/scope=] from
this URL, and ignores the declaration when it is cross-origin. See
[=register the identity handler=].
</dl>

Either {{IdentityProviderWellKnown/provider_urls}} or both
Expand Down
Loading