Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookies #501

Merged
merged 53 commits into from
Jan 10, 2024
Merged

Cookies #501

merged 53 commits into from
Jan 10, 2024

Conversation

lolaodelola
Copy link
Contributor

@lolaodelola lolaodelola commented Jul 19, 2023

WIP

Issue #287


Preview | Diff

index.bs Outdated
@@ -2152,6 +2157,25 @@ browsingContext.ReadinessState = "none" / "interactive" / "complete"
The <code>browsingContext.ReadinessState</code> type represents the stage of
document loading at which a navigation command will return.

#### The browsingContext.Cookie Type #### {#type-browsingContext-Cookie}
Copy link
Contributor

Choose a reason for hiding this comment

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

we probably want cookies outside of the browsing context module (except for the command to get a partition key or other commands that require the context as input)? I am thinking a new domain called storage would be good? cc @jgraham

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds fine to me. Currently I haven't included a command to get a cookie's partition key, are you thinking something like broswsingContext.getCookiePartitionKey for that?

Copy link
Member

Choose a reason for hiding this comment

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

Yes to a storage domain.

For the partition key, I think one question is whether it's sufficient to be able to get the key only for already loaded contexts. That is roughly equivalent to not exposing the key at all, but only being able to set cookies for a currently loaded page, because you always need to load a page, get the key, and then use that to set cookies.

I slightly wondered if we should define some partition key fields and have something in the capabilities response that returns the list of keys recognised by the current browser. So we might define sourceOrigin as a key. Then a browser with only SOP would just return an empty key set like:

storagePartitionKeys: {}

A browser that required keying on sourceOrigin and couldn't provide any defaults would return something like:

storagePartitionKeys: {sourceOrigin: {required: true}}

And then something like current gecko with containers could provide a custom key like moz:userContext like so:

storagePartitionKeys: {sourceOrigin: {required: true}, "moz:userContext": {default: null}}

So then clients/users could know that to set storage properties you'd have to provide a value for the sourceOrigin key and that moz:userContext is another part of the key, but if you don't provide it you'll get some default value that hopefully does the right thing for most cases. This doesn't totally solve the forwards compatibility problem, but at least makes it possible for clients to implement a good UX (by telling you exactly what you need to change if there's a change, and by allowing users to provide a set of properties that's a superset of what the client expects).

Copy link
Contributor Author

@lolaodelola lolaodelola Jul 24, 2023

Choose a reason for hiding this comment

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

I just want to make a clarification here, when we're talking about partition key for cookies, are we talking about partitioned cookies as defined in CHIPS or are we talking about cookies defined within Mozilla's containers for different user contexts?

Copy link
Member

Choose a reason for hiding this comment

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

Both. Or rather CHIPS is a Chromium API that allows sites to opt in to source-partitioned cookies. Other browsers have source partitioned cookies everywhere. Containers are another part of the partition key in Gecko today but it could be adopted by other browsers (I think maybe WebKit has something similar? I'm not sure). It also seems conceivable that browsers will add additional partitioning in the future if there are additional UX/privacy benefits. So I don't think we want to start from the position that there's a known set of attributes that partition storage state.

@jugglinmike
Copy link
Contributor

I am thinking a new domain called storage would be good?

I believe "domain" is a term from Chrome DevTools Protocol which roughly corresponds to the BiDi draft's "module." Please let me know if I'm mistaken!

It's my understanding that there's no interest in extending the original WebDriver specification with implementation-defined cookie partitioning. That's why I've wrapped WebDriver's "process capabilities" with a new algorithm which adds partition information. To reduce the risk of confusion, I've referenced the original algorithm with the word "classic" and used "BiDi" in the name of the new algorithm (though either one of those alone would be sufficient to disambiguate).

@jgraham's recent comment describes a scenario involving multiple partition keys, so I've changed the type in storage.Cookie to an optional extensible object (rather than string|undefined).

The draft currently defines a type named network.Cookie. Is there a need for that type to differ from storage.Cookie? I believe the organization of types is not observable, so would it be possible to extend network.Cookie and use it from the new storage module?

@jgraham
Copy link
Member

jgraham commented Sep 19, 2023

Yes, reusing network.Cookie is the right thing to do here.

On the more general design here, I think we agreed at TPAC to drop the capabilities part for now.

Instead, in the meantime, we can have something like

storage.PartitionKey = {
  ?container: text,
  ?sourceOrigin: text,  
}

Then when you set a cookie you have something like:

storage.SetCookieParameters = {
  cookies: [ +network.Cookie ],
  ? partitionKey: storage.PartitionKey,
}

and storage.SetCookieResult which will look like

storage.SetCookieResult = {
   partitionKey: partitionKey
}

In the return value all partition keys that were used must be set to the value that was used, and any that were not used must be unset. For sourceOrigin the default must be the same as the Cookie's destination origin (there might be some detail here because cookies are not precisely keyed on origin). For container the default must be the default container (which we might call null or empty string, I'm not sure).

@jugglinmike
Copy link
Contributor

Hi folks, I've completed a first draft; I'll share a few specific thoughts as in-line comments.

index.bs Outdated
Comment on lines 8720 to 8727
storage.CookieFilter = {
? ~ network.Cookie
}

storage.GetCookiesParameters = {
cookie: storage.CookieFilter,
? partitionKey: storage.PartitionKey,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These commands can't use BiDi's network.Cookie type directly because in these contexts, many of the entries are optional. In the interest of limiting duplication, I've attempted to specify the storage.CookieFilter and storage.PartialCookie types in terms of network.Cookie, but I'm not certain the CDDL syntax I've applied is appropriate. (You can find more detail in the message I sent to the CBOR mailing list.)

Copy link
Member

Choose a reason for hiding this comment

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

I'm quite in favour of just duplicating the definition with explicit markers for optionality, rather than trying to use clever CDDL which readers are unlikely to figure out the meaning of without being CDDL experts :)

index.bs Outdated
Comment on lines 8695 to 8701
To <dfn>get matching cookies</dfn> given |cookie store| and |filter|:

1. Let |cookies| be a new list.
1. For each |stored cookie| in |cookie store|:
1. If [=match cookie=] with |stored cookie| and |filter| is true:
1. Append the result of [=serialize cookie=] given |stored cookie| to |cookies|.
1. Return |cookies|.
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be expressed without an algorithm like "match cookie", but doing so would involve labeling a loop in order to continue from within a nested loop. Factoring the operation into two simpler algorithms seems preferable.

index.bs Show resolved Hide resolved
index.bs Outdated
Comment on lines 8599 to 8602
<p>The following <dfn>table for cookie conversion</dfn>
defines the cookie concepts relevant to WebDriver BiDi,
how these are referred to in [[COOKIES]],
as well as what keys they map to in a [=serialized cookie=].
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to re-use the corresponding table in WebDriver Classic, we'll have to special-case the "Cookie expiry time" concept because WebDriver classic uses "expiry" as the JSON key, but BiDi's network.Cookie uses "expires".

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe it's not too late for changing the BiDi's network.Cookie "expires" to "expiry" to align with WebDriver Classic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, that's great! Here's a patch for that.

@lolaodelola lolaodelola marked this pull request as ready for review November 1, 2023 11:48
Copy link
Member

@jgraham jgraham left a comment

Choose a reason for hiding this comment

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

At a high level this is looking pretty good to me.

One formatting comment: in the rest of the spec we leave a blank line between algorithm steps in order to make it easier to read.

index.bs Outdated

<pre class="cddl local-cddl remote-cddl">
storage.PartitionKey {
? container: text,
Copy link
Member

Choose a reason for hiding this comment

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

I think in #570 we are likely to rename "container" to "userContext". Since we seem to be heading in the direction of just making user contexts a mandatory feature, we could even consider making this a top-level property outside the partition key, whilst still keeping the partition key construct in case we want to key storage on more than just souceOrigin in the future (however that's a point for discussion).

Copy link
Contributor

Choose a reason for hiding this comment

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

I've renamed the key, but I'll hold off on restructuring until there's a stronger signal about whether it's mandatory.

index.bs Outdated
Comment on lines 8720 to 8727
storage.CookieFilter = {
? ~ network.Cookie
}

storage.GetCookiesParameters = {
cookie: storage.CookieFilter,
? partitionKey: storage.PartitionKey,
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm quite in favour of just duplicating the definition with explicit markers for optionality, rather than trying to use clever CDDL which readers are unlikely to figure out the meaning of without being CDDL experts :)

index.bs Outdated
<dd>
<pre class="cddl local-cddl">
storage.GetCookiesResult = {
cookies: [*network.Cookie]
Copy link
Member

Choose a reason for hiding this comment

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

We should also return the value of the partition key that we finally used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good.

index.bs Outdated
The [=remote end steps=] with <var ignore>session</var> and |command parameters| are:

1. Let |cookie spec| be the value of the <code>cookie</code> field of |command parameters|.
1. If |cookie spec|["<code>size</code>"] exists:
Copy link
Member

Choose a reason for hiding this comment

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

We should definitely change the CDDL instead of having this, but also in general we don't raise errors for additional fields.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I've made Storage.CookieFilter and Storage.PartialCookie extensible.

index.bs Show resolved Hide resolved
index.bs Outdated
<dt>Result Type</dt>
<dd>
<pre class="cddl">
EmptyResult
Copy link
Member

Choose a reason for hiding this comment

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

Again we should return the expanded partition key.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it. This makes Storage.SetCookieResult and Storage.DeleteCookiesResult identical, but I've defined them as distinct types, anyway.

index.bs Outdated

A [=remote end=] has a [=/set=] of <dfn>storage partition key attributes</dfn> which contains zero or more implementation-defined string values.

A [=remote end=] has a [=/map=] of <dfn>default values for storage partition key attributes</dfn> which contains zero or more implementation-defined entries. Each key must be a member of the [=storage partition key attributes=] which the [=remote end=] does not require as input to [=storage.getCookies=], [=storage.setCookie=], and [=storage.deleteCookies=]; the corresponding value is a the value which the [=remote end=] uses when one is not provided.
Copy link
Member

Choose a reason for hiding this comment

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

I don't think a fixed default works. For sourceOrigin in particular the right default is to set it as a first-party cookie i.e. with the source origin matching the destination.

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 that we need to require the domain key in storage.getCookies and storage.deleteCookies (just as we already do for storage.setCookie)? Or maybe require at least one, e.g. either:

getCookies({domain: 'zombo.com'})

or

getCookies({}, {partitionKey: {sourceOrigin: 'zombo.com'}})

?

Or could a wildcard value actually work for "get" and "delete" operations? Unlike "set" (where implementations may not be able to create cookies for all domains a piori), it seems like "get" and "delete" can describe meaningful operations in the absence of any particular domain, e.g. "retrieve matching cookies from all source origins."

Copy link
Member

Choose a reason for hiding this comment

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

I think it's theoretically possible to get all cookies in a partition (at least in gecko, c.f. https://searchfox.org/mozilla-central/source/netwerk/cookie/nsICookieManager.idl#232-245; note that "origin attributes" here is basically the same as "partition").

But I don't think it's possible to get all cookies from all partitions at once.

So I think you either need to exactly specify which partition you want or you need to specify the domain so that we can compute a default partition (or specify a navigable / context which is implicitly associated with a specific partition).

index.bs Outdated

storage.SetCookieParameters = {
cookie: storage.PartialCookie,
? partitionKey: storage.PartitionKey,
Copy link
Member

Choose a reason for hiding this comment

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

Should we also allow passing in a context to use as the default for the partition key? e.g if you already have example.org loaded in some context, then the default values of the partition key will be set as if the cookie was being set by example.org loaded in that specific context.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you clarify what you mean by "context" in this case? I believe the existing "browsing context" and the proposed "user context" can both have associated domains, so it seems like either could fit in your example.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe both could be applicable here. But I guess we could start with the browsing context since the user contexts are not fully defined yet.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, browsing contexts (navigables).

(I don't think a user context has an associated domain? It's "just" a storage partition key. One can imagine having different domains load in different user contexts by default or something, and certainly each user context might at any one time have multiple navigables, all with documents that have origins covering multiple domains, but I don't see how there would be a useful 1:1 mapping).

@lolaodelola lolaodelola changed the title Cookies -wip Cookies Nov 1, 2023
Copy link
Member

@jgraham jgraham left a comment

Choose a reason for hiding this comment

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

This feels really close to landing; I think we should just clarify how "required" and "default" partition keys relate to each other, but at that point we're good to go.


1. For each |name| in the remote end's [=required partition key attributes=]:

1. If |partition key|[|name|] does not [=map/exist=]:
1. If |partition spec|[|name|] [=map/exists=]:
Copy link
Member

Choose a reason for hiding this comment

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

So this means that you're treating "required" and "default" as mutually exclusive? i.e. the full key will be all the required keys plus all the keys with a default value? I was imagining that "required" would be everything and "default" would be the subset for which a known default exists. Either way works, but I found this quite surprising, so we at least need to be clearer in the definitions what's expected.

@jugglinmike
Copy link
Contributor

Thanks, @jgraham! I've personally been considering this "requirement" from the perspective of the local end, where the attributes with default values are implicitly not required. I believe your expectation comes from the remote end's perspective, where the requirement stands regardless of whether each property was explicitly specified.

It seems like your recommendation to have guidance applies in either case--that is, whether the list SHOULD or SHOULD NOT include property values from the map. I've added some text which supports the "mutually exclusive"/"local end" framing since that seems like the lowest-friction solution at this late stage of the design process.

This has me wondering, though: do we really need two data structures here? If we only allowed implementers to specify which attributes are optional (and treat all other attributes are required), then would that limit them in any meaningful way?

If not, then we could avoid confusion (and simplify the spec generally) by eliminating this avenue of implementer choice. If, instead of "required partition key attributes", we had a definition like:

A remote end has a set of <dfn>recognized storage partition key attributes</dfn>
which is the union of the attributes in the [=table of standard storage
partition key attributes=] and the [=remote end=]'s [=extension storage
partition key attributes=].

Note: The [=local end=] must specify any partition key attribute that a
[=remote end=] [=recognized storage partition key attributes|recognizes=] but
does not define a [default values for storage partition key
attributes|default value=].

I believe that would be sufficient to express the desired semantics of the "expand" algorithm.

@jgraham
Copy link
Member

jgraham commented Dec 21, 2023

Writing the spec from the perspective of the local end isn't going to work out well; in practice the spec is specifying what happens in remote ends, and local ends are essentially free to do whatever they like to achieve their desired result. Of course by constraining the behaviour of remote ends the spec also de-facto constrains the things that local ends can do to achieve their goals. But note that, for example, we don't have a local end testsuite, only a remote end one. That reflects the fact that the normative criteria in the spec are all on the remote end.

I've made some suggestions for small improvements that I think clarify things for now. With those I'm basically happy for this to land, although as with all other commands I expect we'll need some later cleanup as we implement and as the interaction between this feature and other features in the spec is worked out.

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
jugglinmike and others added 5 commits December 21, 2023 16:58
Co-authored-by: jgraham <james@hoppipolla.co.uk>
Co-authored-by: jgraham <james@hoppipolla.co.uk>
Co-authored-by: jgraham <james@hoppipolla.co.uk>
@jugglinmike
Copy link
Contributor

Thanks, @jgraham. I'm still interested in learning if the simplification I proposed is feasible, but like you, I'm satisfied with where we've landed for now.

Copy link
Contributor

@whimboo whimboo left a comment

Choose a reason for hiding this comment

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

Thanks for all the updates @jugglinmike! I did a overall check only for now given that there are some issues to fix.

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
Comment on lines +9273 to +9281
? name: text,
? value: network.BytesValue,
? domain: text,
? path: text,
? size: js-uint,
? httpOnly: bool,
? secure: bool,
? sameSite: network.SameSite,
? expiry: js-uint,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Lets sort alphabetical, which applies to the following definitions as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

This document doesn't follow a convention concerning if/how optionality impacts sort order (or concerning sorting in general). Would you like to sort with or without regard for optionality? Or would you prefer to introduce the new convention atomically?

Copy link
Contributor

Choose a reason for hiding this comment

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

Generally we apply this sorting in our CDDL definitions. Usually we have the mandatory group first, and then the optional one, whereby within each group we sort alphabetically. I checked quite a few definitions and that is true, only some do not follow. With introducing complete new commands and types we should follow that.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can do the sorting in a follow-up PR. It's not blocking.

index.bs Outdated Show resolved Hide resolved
jugglinmike and others added 6 commits January 3, 2024 21:03
Co-authored-by: Henrik Skupin <mail@hskupin.info>
Co-authored-by: Henrik Skupin <mail@hskupin.info>
Co-authored-by: Henrik Skupin <mail@hskupin.info>
@whimboo whimboo dismissed their stale review January 10, 2024 17:13

My comment shouldn't block. Leaving review to others.

@css-meeting-bot
Copy link
Member

The Browser Testing and Tools Working Group just discussed Cookies.

The full IRC log of that discussion <jgraham> Topic: Cookies
<jgraham> github: https://github.com//pull/501
<MaksimSadym> q+
<jgraham> jgraham: The Cookies PR seems nearly ready to land. Is there more that needs to be done here?
<AutomatedTester> q?
<jgraham> ack next
<jgraham> MaksimSadym: I tried to implement it and found a few possible extensions we'd like to make. That's fine, but we want to add some Chrome-specific data to the cookies.
<jgraham> jgraham: I think adding extra data is fine in general, but you're expected to use a prefix on the key like goog:
<jgraham> jgraham: If it's really Chrome-specific data then that's fine, if it's actually relevant to multiple browsers we should standardise it
<jgraham> MaksimSadym: I think it's Chrome-specific extensions
<jgraham> whimboo: The comment from me hasn't been addressed yet. I don't know when Mike is back, but this doesn't need to block; we can do it as a follow up.
<jgraham> jgraham: Great. Sounds like we should merge the current PR and address any further issues in follow ups.

@whimboo
Copy link
Contributor

whimboo commented Jan 10, 2024

We have full review from Mozilla and Google. So lets get it merged! 🥇

@whimboo whimboo merged commit 6ebacb6 into w3c:main Jan 10, 2024
5 checks passed
github-actions bot added a commit that referenced this pull request Jan 10, 2024
SHA: 6ebacb6
Reason: push, by whimboo

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@lutien
Copy link
Contributor

lutien commented Jan 11, 2024

I've picked up the work on storage.getCookies command in the scope of this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1854580.
@sadym-chromium, let me know if you started with the wpt tests already. If not, I'm going to add them with the implementation.

@sadym-chromium
Copy link
Contributor

I've picked up the work on storage.getCookies command in the scope of this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1854580. @sadym-chromium, let me know if you started with the wpt tests already. If not, I'm going to add them with the implementation.

I started working on implementation, but not on WPT. We can split the work on WPT

@lutien
Copy link
Contributor

lutien commented Jan 11, 2024

I started working on implementation, but not on WPT. We can split the work on WPT

Sounds good to split :)
What do you think what would be the best way to do it? I could only think about that I take tests for getCookies and you for setCookies, but I'm open to suggestions

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.

10 participants