Cookie Store API

Draft Community Group Report,

This version:
https://wicg.github.io/cookie-store/
Test Suite:
https://github.com/web-platform-tests/wpt/tree/master/cookie-store
Issue Tracking:
GitHub
Inline In Spec
Editors:
(Google Inc.)
(Google Inc.)
Former Editors:
(Google Inc.)
(Google Inc.)

Abstract

An asynchronous Javascript cookies API for documents and service workers

Status of this document

This specification was published by the Web Platform Incubator 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.

This is a proposal to bring an asynchronous cookie API to scripts running in HTML documents and service workers.

HTTP cookies have, since their origins at Netscape (documentation preserved by archive.org), provided a valuable state-management mechanism for the web.

The synchronous single-threaded script-level document.cookie interface to cookies has been a source of complexity and performance woes further exacerbated by the move in many browsers from:

... to the modern web which strives for smoothly responsive high performance:

On the modern web a cookie operation in one part of a web application cannot block:

Newer parts of the web built in service workers need access to cookies too but cannot use the synchronous, blocking document.cookie interface at all as they both have no document and also cannot block the event loop as that would interfere with handling of unrelated events.

1.1. A Taste of the Proposed Change

Although it is tempting to rethink cookies entirely, web sites today continue to rely heavily on them, and the script APIs for using them are largely unchanged over their first decades of usage.

Today writing a cookie means blocking your event loop while waiting for the browser to synchronously update the cookie jar with a carefully-crafted cookie string in Set-Cookie format:

document.cookie =
  '__Secure-COOKIENAME=cookie-value' +
  '; Path=/' +
  '; expires=Fri, 12 Aug 2016 23:05:17 GMT' +
  '; Secure' +
  '; Domain=example.org';
// now we could assume the write succeeded, but since
// failure is silent it is difficult to tell, so we
// read to see whether the write succeeded
var successRegExp =
  /(^|; ?)__Secure-COOKIENAME=cookie-value(;|$)/;
if (String(document.cookie).match(successRegExp)) {
  console.log('It worked!');
} else {
  console.error('It did not work, and we do not know why');
}

What if you could instead write:

const one_day_ms = 24 * 60 * 60 * 1000;
cookieStore.set(
  {
    name: '__Secure-COOKIENAME',
    value: 'cookie-value',
    expires: Date.now() + one_day_ms,
    domain: 'example.org'
  }).then(function() {
    console.log('It worked!');
  }, function(reason) {
    console.error(
      'It did not work, and this is why:',
      reason);
  });
// Meanwhile we can do other things while waiting for
// the cookie store to process the write...

This also has the advantage of not relying on document and not blocking, which together make it usable from Service Workers, which otherwise do not have cookie access from script.

This proposal also includes a power-efficient monitoring API to replace setTimeout-based polling cookie monitors with cookie change observers.

1.2. Summary

This proposal outlines an asynchronous API using Promises/async functions for the following cookie operations:

1.3. Querying Cookies

Both documents and service workers access the same query API, via the cookieStore property on the global object.

The get() and getAll() methods on CookieStore are used to query cookies. Both methods return Promises. Both methods take the same arguments, which can be either:

The get() method is essentially a form of getAll() that only returns the first result.

Reading a cookie:
try {
  const cookie = await cookieStore.get('session_id');
  if (cookie) {
    console.log(`Found ${cookie.name} cookie: ${cookie.value}`);
  } else {
    console.log('Cookie not found');
  }
} catch (e) {
  console.error(`Cookie store error: ${e}`);
}
Reading multiple cookies:
try {
  const cookies = await cookieStore.getAll('session_id'});
  for (const cookie of cookies)
    console.log(`Result: ${cookie.name} = ${cookie.value}`);
} catch (e) {
  console.error(`Cookie store error: ${e}`);
}

Service workers can obtain the list of cookies that would be sent by a fetch to any URL under their scope.

Read the cookies for a specific URL (in a service worker):
await cookieStore.getAll({url: '/admin'});

Documents can only obtain the cookies at their current URL. In other words, the only valid url value in Document contexts is the document’s URL.

The objects returned by get() and getAll() contain all the relevant information in the cookie store, not just the name and the value as in the older document.cookie API.

Accessing all the cookie data:
await cookie = cookieStore.get('session_id');
console.log(`Cookie scope - Domain: ${cookie.domain} Path: ${cookie.path}`);
if (cookie.expires === null) {
  console.log('Cookie expires at the end of the session');
} else {
  console.log(`Cookie expires at: ${cookie.expires}`);
}
if (cookie.secure)
  console.log('The cookie is restricted to secure origins');

1.4. Modifying Cookies

Both documents and service workers access the same modification API, via the cookieStore property on the global object.

Cookies are created or modified (written) using the set() method.

Write a cookie:
try {
  await cookieStore.set('opted_out', '1');
} catch (e) {
  console.error(`Failed to set cookie: ${e}`);
}

The set() call above is shorthand for using an options dictionary, as follows:

await cookieStore.set({
  name: 'opted_out',
  value: '1',
  expires: null,  // session cookie

  // By default, domain is set to null which means the scope is locked at the current domain.
  domain: null,
  path: '/'
});

Cookies are deleted (expired) using the delete() method.

Delete a cookie:
try {
  await cookieStore.delete('session_id');
} catch (e) {
  console.error(`Failed to delete cookie: ${e}`);
}

Under the hood, deleting a cookie is done by changing the cookie’s expiration date to the past, which still works.

Deleting a cookie by changing the expiry date:
try {
  const one_day_ms = 24 * 60 * 60 * 1000;
  await cookieStore.set({
    name: 'session_id',
    value: 'value will be ignored',
    expires: Date.now() - one_day_ms });
} catch (e) {
  console.error(`Failed to delete cookie: ${e}`);
}

1.5. Monitoring Cookies

To avoid polling, it is possible to observe changes to cookies.

In documents, change events are fired for all relevant cookie changes.

Register for change events in documents:
cookieStore.addEventListener('change', event => {
  console.log(`${event.changed.length} changed cookies`);
  for (const cookie in event.changed)
    console.log(`Cookie ${cookie.name} changed to ${cookie.value}`);

  console.log(`${event.deleted.length} deleted cookies`);
  for (const cookie in event.deleted)
    console.log(`Cookie ${cookie.name} deleted`);
});

In service workers, cookiechange events are fired against the global scope, but an explicit subscription is required, associated with the service worker’s registration.

Register for cookiechange events in a service worker:
self.addEventListener('activate', (event) => {
  event.waitUntil(async () => {
    // Snapshot current state of subscriptions.
    const subscriptions = await self.registration.cookies.getSubscriptions();

    // Clear any existing subscriptions.
    await self.registration.cookies.unsubscribe(subscriptions);

    await self.registration.cookies.subscribe([
      {
        name: 'session_id',  // Get change events for cookies named session_id.
      }
    ]);
  });
});

self.addEventListener('cookiechange', event => {
  // The event has |changed| and |deleted| properties with
  // the same semantics as the Document events.
  console.log(`${event.changed.length} changed cookies`);
  console.log(`${event.deleted.length} deleted cookies`);
});

Calls to subscribe() are cumulative, so that independently maintained modules or libraries can set up their own subscriptions. As expected, a service worker's subscriptions are persisted for with the service worker registration.

Subscriptions can use the same options as get() and getAll(). The complexity of fine-grained subscriptions is justified by the cost of dispatching an irrelevant cookie change event to a service worker, which is is much higher than the cost of dispatching an equivalent event to a Window. Specifically, dispatching an event to a service worker might require waking up the worker, which has a significant impact on battery life.

The getSubscriptions() allows a service worker to introspect the subscriptions that have been made.

Checking change subscriptions:
const subscriptions = await self.registration.cookies.getSubscriptions();
for (const sub of subscriptions) {
  console.log(sub.name, sub.url);
}

2. Concepts

A cookie is normatively defined for user agents by Cookies: HTTP State Management Mechanism § 5 User Agent Requirements.

Per Cookies: HTTP State Management Mechanism § 5.6 Storage Model, a cookie has the following fields: name, value, expiry-time, domain, path, creation-time, last-access-time, persistent-flag, host-only-flag, secure-only-flag, http-only-flag, same-site-flag.

A cookie is script-visible when it is in-scope and does not have the HttpOnly cookie flag. This is more formally enforced in the processing model, which consults Cookies: HTTP State Management Mechanism § 5.7 Retrieval Model at appropriate points.

A cookie is also subject to certain size limits. Per Cookies: HTTP State Management Mechanism § 5.6 Storage Model:

NOTE: Cookie attribute-values are stored as byte sequences, not strings.

A cookie store is normatively defined for user agents by Cookies: HTTP State Management Mechanism §User Agent Requirements.

When any of the following conditions occur for a cookie store, perform the steps to process cookie changes.

2.3. Extensions to Service Worker

[Service-Workers] defines service worker registration, which this specification extends.

A service worker registration has an associated cookie change subscription list which is a list; each member is a cookie change subscription. A cookie change subscription is a tuple of name and url.

3. The CookieStore Interface

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options = {});

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options = {});

  Promise<undefined> set(USVString name, USVString value);
  Promise<undefined> set(CookieInit options);

  Promise<undefined> delete(USVString name);
  Promise<undefined> delete(CookieStoreDeleteOptions options);

  [Exposed=Window]
  attribute EventHandler onchange;
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
};

enum CookieSameSite {
  "strict",
  "lax",
  "none"
};

dictionary CookieInit {
  required USVString name;
  required USVString value;
  DOMHighResTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  CookieSameSite sameSite = "strict";
  boolean partitioned = false;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean partitioned = false;
};

dictionary CookieListItem {
  USVString name;
  USVString value;
  USVString? domain;
  USVString path;
  DOMHighResTimeStamp? expires;
  boolean secure;
  CookieSameSite sameSite;
  boolean partitioned;
};

typedef sequence<CookieListItem> CookieList;

3.1. The get() method

cookie = await cookieStore . get(name)
cookie = await cookieStore . get(options)

Returns a promise resolving to the first in-scope script-visible value for a given cookie name (or other options). In a service worker context this defaults to the path of the service worker’s registered scope. In a document it defaults to the path of the current document and does not respect changes from replaceState() or document.domain.

The get(name) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let list be the results of running query cookies with url and name.

    2. If list is failure, then reject p with a TypeError and abort these steps.

    3. If list is empty, then resolve p with null.

    4. Otherwise, resolve p with the first item of list.

  7. Return p.

The get(options) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. If options is empty, then return a promise rejected with a TypeError.

  6. If options["url"] is present, then run these steps:

    1. Let parsed be the result of parsing options["url"] with settings’s API base URL.

    2. If this's relevant global object is a Window object and parsed does not equal url, then return a promise rejected with a TypeError.

    3. If parsed’s origin and url’s origin are not the same origin, then return a promise rejected with a TypeError.

    4. Set url to parsed.

  7. Let p be a new promise.

  8. Run the following steps in parallel:

    1. Let list be the results of running query cookies with url and options["name"] (if present).

    2. If list is failure, then reject p with a TypeError and abort these steps.

    3. If list is empty, then resolve p with null.

    4. Otherwise, resolve p with the first item of list.

  9. Return p.

3.2. The getAll() method

cookies = await cookieStore . getAll(name)
cookies = await cookieStore . getAll(options)

Returns a promise resolving to the all in-scope script-visible value for a given cookie name (or other options). In a service worker context this defaults to the path of the service worker’s registered scope. In a document it defaults to the path of the current document and does not respect changes from replaceState() or document.domain.

The getAll(name) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let list be the results of running query cookies with url and name.

    2. If list is failure, then reject p with a TypeError.

    3. Otherwise, resolve p with list.

  7. Return p.

The getAll(options) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. If options["url"] is present, then run these steps:

    1. Let parsed be the result of parsing options["url"] with settings’s API base URL.

    2. If this's relevant global object is a Window object and parsed does not equal url, then return a promise rejected with a TypeError.

    3. If parsed’s origin and url’s origin are not the same origin, then return a promise rejected with a TypeError.

    4. Set url to parsed.

  6. Let p be a new promise.

  7. Run the following steps in parallel:

    1. Let list be the results of running query cookies with url and options["name"] (if present).

    2. If list is failure, then reject p with a TypeError.

    3. Otherwise, resolve p with list.

  8. Return p.

3.3. The set() method

await cookieStore . set(name, value)
await cookieStore . set(options)

Writes (creates or modifies) a cookie.

The options default to:

  • Path: /

  • Domain: same as the domain of the current document or service worker’s location

  • No expiry date

  • SameSite: strict

The set(name, value) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let r be the result of running set a cookie with url, name, value.

    2. If r is failure, then reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  7. Return p.

The set(options) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let r be the result of running set a cookie with url, options["name"], options["value"], options["expires"], options["domain"], options["path"], options["sameSite"], and options["partitioned"].

    2. If r is failure, then reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  7. Return p.

3.4. The delete() method

await cookieStore . delete(name)
await cookieStore . delete(options)

Deletes (expires) a cookie with the given name or name and optional domain and path.

The delete(name) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let r be the result of running delete a cookie with url, name, null, "/", true, and "strict".

    2. If r is failure, then reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  7. Return p.

The delete(options) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let r be the result of running delete a cookie with url, options["name"], options["domain"], options["path"], and options["partitioned"].

    2. If r is failure, then reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  7. Return p.

4. The CookieStoreManager Interface

A CookieStoreManager has an associated registration which is a service worker registration.

The CookieStoreManager interface allows Service Workers to subscribe to events for cookie changes. Using the subscribe() method is necessary to indicate that a particular service worker registration is interested in change events.

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStoreManager {
  Promise<undefined> subscribe(sequence<CookieStoreGetOptions> subscriptions);
  Promise<sequence<CookieStoreGetOptions>> getSubscriptions();
  Promise<undefined> unsubscribe(sequence<CookieStoreGetOptions> subscriptions);
};

4.1. The subscribe() method

await registration . cookies . subscribe(subscriptions)

Subscribe to changes to cookies. Subscriptions can use the same options as get() and getAll(), with optional name and url properties.

Once subscribed, notifications are delivered as "cookiechange" events fired against the Service Worker's global scope:

The subscribe(subscriptions) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let registration be this's registration.

  3. Let p be a new promise.

  4. Run the following steps in parallel:

    1. Let subscription list be registration’s associated cookie change subscription list.

    2. For each entry in subscriptions, run these steps:

      1. Let name be entry["name"].

      2. Let url be the result of parsing entry["url"] with settings’s API base URL.

      3. If url does not start with registration’s scope url, then reject p with a TypeError and abort these steps.

      4. Let subscription be the cookie change subscription (name, url).

      5. If subscription list does not already contain subscription, then append subscription to subscription list.

    3. Resolve p with undefined.

  5. Return p.

4.2. The getSubscriptions() method

subscriptions = await registration . cookies . getSubscriptions()

This method returns a promise which resolves to a list of the cookie change subscriptions made for this Service Worker registration.

The getSubscriptions() method steps are:
  1. Let registration be this's registration.

  2. Let p be a new promise.

  3. Run the following steps in parallel:

    1. Let subscriptions be registration’s associated cookie change subscription list.

    2. Let result be a new list.

    3. For each subscription in subscriptions, run these steps:

      1. Append «[ "name" → subscription’s name, "url" → subscription’s url]» to result.

    4. Resolve p with result.

  4. Return p.

4.3. The unsubscribe() method

await registration . cookies . unsubscribe(subscriptions)

Calling this method will stop the registered service worker from receiving previously subscribed events. The subscriptions argument should list subscriptions in the same form passed to subscribe() or returned from getSubscriptions().

The unsubscribe(subscriptions) method steps are:
  1. Let settings be this's relevant settings object.

  2. Let registration be this's registration.

  3. Let p be a new promise.

  4. Run the following steps in parallel:

    1. Let subscription list be registration’s associated cookie change subscription list.

    2. For each entry in subscriptions, run these steps:

      1. Let name be entry["name"].

      2. Let url be the result of parsing entry["url"] with settings’s API base URL.

      3. If url does not start with registration’s scope url, then reject p with a TypeError and abort these steps.

      4. Let subscription be the cookie change subscription (name, url).

      5. Remove any item from subscription list equal to subscription.

    3. Resolve p with undefined.

  5. Return p.

4.4. The ServiceWorkerRegistration interface

The ServiceWorkerRegistration interface is extended to give access to a CookieStoreManager via cookies which provides the interface for subscribing to cookie changes.

[Exposed=(ServiceWorker,Window)]
partial interface ServiceWorkerRegistration {
  [SameObject] readonly attribute CookieStoreManager cookies;
};

Each ServiceWorkerRegistration has an associated CookieStoreManager object. The CookieStoreManager's registration is equal to the ServiceWorkerRegistration's service worker registration.

The cookies getter steps are to return this's associated CookieStoreManager object.

Subscribing to cookie changes from a Service Worker script:
self.registration.cookies.subscribe([{name:'session-id'}]);
Subscribing to cookie changes from a script in a window context:
navigator.serviceWorker.register('sw.js').then(registration => {
  registration.cookies.subscribe([{name:'session-id'}]);
});

5. Event Interfaces

5.1. The CookieChangeEvent interface

A CookieChangeEvent is dispatched against CookieStore objects in Window contexts when any script-visible cookie changes have occurred.

[Exposed=Window,
 SecureContext]
interface CookieChangeEvent : Event {
  constructor(DOMString type, optional CookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary CookieChangeEventInit : EventInit {
  CookieList changed;
  CookieList deleted;
};

The changed and deleted attributes must return the value they were initialized to.

5.2. The ExtendableCookieChangeEvent interface

An ExtendableCookieChangeEvent is dispatched against ServiceWorkerGlobalScope objects when any script-visible cookie changes have occurred which match the Service Worker's cookie change subscription list.

Note: ExtendableEvent is used as the ancestor interface for all events in Service Workers so that the worker itself can be kept alive while the async operations are performed.

[Exposed=ServiceWorker]
interface ExtendableCookieChangeEvent : ExtendableEvent {
  constructor(DOMString type, optional ExtendableCookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary ExtendableCookieChangeEventInit : ExtendableEventInit {
  CookieList changed;
  CookieList deleted;
};

The changed and deleted attributes must return the value they were initialized to.

6. Global Interfaces

A CookieStore is accessed by script using an attribute in the global scope in a Window or ServiceWorkerGlobalScope context.

6.1. The Window interface

[SecureContext]
partial interface Window {
  [SameObject] readonly attribute CookieStore cookieStore;
};

A Window has an associated CookieStore, which is a CookieStore.

The cookieStore getter steps are to return this's associated CookieStore.

6.2. The ServiceWorkerGlobalScope interface

partial interface ServiceWorkerGlobalScope {
  [SameObject] readonly attribute CookieStore cookieStore;

  attribute EventHandler oncookiechange;
};

A ServiceWorkerGlobalScope has an associated CookieStore, which is a CookieStore.

The cookieStore getter steps are to return this's associated CookieStore.

7. Algorithms

To represent a date and time dateTime as a timestamp, return the number of milliseconds from 00:00:00 UTC, 1 January 1970 to dateTime (assuming that there are exactly 86,400,000 milliseconds per day).

Note: This is the same representation used for time values in [ECMAScript].

To date serialize a DOMHighResTimeStamp millis, let dateTime be the date and time millis milliseconds after 00:00:00 UTC, 1 January 1970 (assuming that there are exactly 86,400,000 milliseconds per day), and return a byte sequence corresponding to the closest cookie-date representation of dateTime according to Cookies: HTTP State Management Mechanism § 5.1.1 Dates.

7.1. Query Cookies

To query cookies with url and optional name, run the following steps:

  1. Perform the steps defined in Cookies: HTTP State Management Mechanism § 5.7 Retrieval Model to compute the "cookie-string from a given cookie store" with url as request-uri. The cookie-string itself is ignored, but the intermediate cookie-list is used in subsequent steps.

    For the purposes of the steps, the cookie-string is being generated for a "non-HTTP" API.

  2. Let list be a new list.

  3. For each cookie in cookie-list, run these steps:

    1. Assert: cookie’s http-only-flag is false.

    2. If name is given, then run these steps:

      1. Let cookieName be the result of running UTF-8 decode without BOM on cookie’s name.

      2. If cookieName does not equal name, then continue.

    3. Let item be the result of running create a CookieListItem from cookie.

    4. Append item to list.

  4. Return list.

To create a CookieListItem from cookie, run the following steps.

  1. Let name be the result of running UTF-8 decode without BOM on cookie’s name.

  2. Let value be the result of running UTF-8 decode without BOM on cookie’s value.

  3. Let domain be the result of running UTF-8 decode without BOM on cookie’s domain.

  4. Let path be the result of running UTF-8 decode without BOM on cookie’s path.

  5. Let expires be cookie’s expiry-time (as a timestamp).

  6. Let secure be cookie’s secure-only-flag.

  7. Switch on cookie’s same-site-flag:

    `None`

    Let sameSite be "none".

    `Strict`

    Let sameSite be "strict".

    `Lax`

    Let sameSite be "lax".

  8. Let partitioned be a boolean indicating that the user agent supports cookie partitioning and that that cookie has a partition key.

  9. Return «[ "name" → name, "value" → value, "domain" → domain, "path" → path, "expires" → expires, "secure" → secure, "sameSite" → sameSite, "partitioned" → partitioned

Note: The cookie’s creation-time, last-access-time, persistent-flag, host-only-flag, and http-only-flag attributes are not exposed to script.

To set a cookie with url, name, value, optional expires, domain, path, sameSite, and partitioned run the following steps:

  1. If name or value contain U+003B (;), any C0 control character except U+0009 (the horizontal tab character), or U+007F, then return failure.

    Note that it’s up for discussion whether these character restrictions should also apply to expires, domain, path, and sameSite as well. [Issue #httpwg/http-extensions#1593]

  2. If name’s length is 0 and value contains U+003D (=), then return failure.

  3. If name’s length is 0 and value’s length is 0, then return failure.

  4. Let encodedName be the result of UTF-8 encoding name.

  5. Let encodedValue be the result of UTF-8 encoding value.

  6. If the byte sequence length of encodedName plus the byte sequence length of encodedValue is greater than the maximum name/value pair size, then return failure.

  7. Let host be url’s host

  8. Let attributes be a new list.

  9. If domain is not null, then run these steps:

    1. If domain starts with U+002D (.), then return failure.

    2. If host does not equal domain and host does not end with U+002D (.) followed by domain, then return failure.

    3. Let encodedDomain be the result of UTF-8 encoding domain.

    4. If the byte sequence length of encodedDomain is greater than the maximum attribute value size, then return failure.

    5. Append `Domain`/encodedDomain to attributes.

  10. If expires is given, then append `Expires`/expires (date serialized) to attributes.

  11. If path is not null, then run these steps:

    1. If path does not start with U+002F (/), then return failure.

    2. If path does not end with U+002F (/), then append U+002F (/) to path.

    3. Let encodedPath be the result of UTF-8 encoding path.

    4. If the byte sequence length of encodedPath is greater than the maximum attribute value size, then return failure.

    5. Append `Path`/encodedPath to attributes.

  12. Append `Secure`/`` to attributes.

  13. Switch on sameSite:

    "none"

    Append `SameSite`/`None` to attributes.

    "strict"

    Append `SameSite`/`Strict` to attributes.

    "lax"

    Append `SameSite`/`Lax` to attributes.

  14. If partitioned is true, Append `Partitioned`/`` to attributes.

  15. Perform the steps defined in Cookies: HTTP State Management Mechanism § 5.6 Storage Model for when the user agent "receives a cookie" with url as request-uri, encodedName as cookie-name, encodedValue as cookie-value, and attributes as cookie-attribute-list.

    For the purposes of the steps, the newly-created cookie was received from a "non-HTTP" API.

  16. Return success.

    Note: Storing the cookie may still fail due to requirements in [RFC6265bis] but these steps will be considered successful.

To delete a cookie with url, name, domain, path, and partitioned run the following steps:

  1. If path is not null, then run these steps:

    1. If path does not start with U+002F (/), then return failure.

    2. If path does not end with U+002F (/), then append U+002F (/) to path.

  2. Let expires be the earliest representable date represented as a timestamp.

    Note: The exact value of expires is not important for the purposes of this algorithm, as long as it is in the past.

  3. Let value be the empty string.

  4. Let sameSite be "strict".

    Note: The values for value, and sameSite will not be persisted by this algorithm.

  5. Return the results of running set a cookie with url, name, value, expires, domain, path, sameSite, and partitioned.

7.4. Process Changes

To process cookie changes, run the following steps:

  1. For every Window window, run the following steps:

    1. Let url be window’s relevant settings object's creation URL.

    2. Let changes be the observable changes for url.

    3. If changes is empty, then continue.

    4. Queue a global task on the DOM manipulation task source given window to fire a change event named "change" with changes at window’s CookieStore.

  2. For every service worker registration registration, run the following steps:

    1. Let changes be a new set.

    2. For each change in the observable changes for registration’s scope url, run these steps:

      1. Let cookie be change’s cookie.

      2. For each subscription in registration’s cookie change subscription list, run these steps:

        1. If change is not in the observable changes for subscription’s url, then continue.

        2. Let cookieName be the result of running UTF-8 decode without BOM on cookie’s name.

        3. If cookieName equals subscription’s name, then append change to changes and break.

    3. If changes is empty, then continue.

    4. Let changedList and deletedList be the result of running prepare lists from changes.

    5. Fire a functional event named "cookiechange" using ExtendableCookieChangeEvent on registration with these properties:

      changed

      changedList

      deleted

      deletedList

The observable changes for url are the set of cookie changes to cookies in a cookie store which meet the requirements in step 1 of Cookies: HTTP State Management Mechanism § 5.7.3 Retrieval Algorithm's steps to compute the "cookie-string from a given cookie store" with url as request-uri, for a "non-HTTP" API.

A cookie change is a cookie and a type (either changed or deleted):

To fire a change event named type with changes at target, run the following steps:

  1. Let event be the result of creating an Event using CookieChangeEvent.

  2. Set event’s type attribute to type.

  3. Set event’s bubbles and cancelable attributes to false.

  4. Let changedList and deletedList be the result of running prepare lists from changes.

  5. Set event’s changed attribute to changedList.

  6. Set event’s deleted attribute to deletedList.

  7. Dispatch event at target.

To prepare lists from changes, run the following steps:

  1. Let changedList be a new list.

  2. Let deletedList be a new list.

  3. For each change in changes, run these steps:

    1. Let item be the result of running create a CookieListItem from change’s cookie.

    2. If change’s type is changed, then append item to changedList.

    3. Otherwise, run these steps:

      1. Set item["value"] to undefined.

      2. Append item to deletedList.

  4. Return changedList and deletedList.

8. Security Considerations

Other than cookie access from service worker contexts, this API is not intended to expose any new capabilities to the web.

8.1. Gotcha!

Although browser cookie implementations are now evolving in the direction of better security and fewer surprising and error-prone defaults, there are at present few guarantees about cookie data security.

For these reasons it is best to use caution when interpreting any cookie’s value, and never execute a cookie’s value as script, HTML, CSS, XML, PDF, or any other executable format.

8.2. Restrict?

This API may have the unintended side-effect of making cookies easier to use and consequently encouraging their further use. If it causes their further use in non-secure contexts this could result in a web less safe for users. For that reason this API has been restricted to secure contexts only.

8.3. Secure cookies

This section is non-normative.

This API only allows writes for Secure cookies to encourage better decisions around security. However the API will still allow reading non-Secure cookies in order to facilitate the migration to Secure cookies. As a side-effect, when fetching and modifying a non-Secure cookie with this API, the non-Secure cookie will automatically be modified to Secure.

8.4. Surprises

Some existing cookie behavior (especially domain-rather-than-origin orientation, non-secure contexts being able to set cookies readable in secure contexts, and script being able to set cookies unreadable from script contexts) may be quite surprising from a web security standpoint.

Other surprises are documented in Cookies: HTTP State Management Mechanism § 1 Introduction - for instance, a cookie may be set for a superdomain (e.g. app.example.com may set a cookie for the whole example.com domain), and a cookie may be readable across all port numbers on a given domain name.

Further complicating this are historical differences in cookie-handling across major browsers, although some of those (e.g. port number handling) are now handled with more consistency than they once were.

8.5. Prefixes

Where feasible the examples use the __Host- and __Secure- name prefixes which causes some current browsers to disallow overwriting from non-secure contexts, disallow overwriting with no Secure flag, and — in the case of __Host- — disallow overwriting with an explicit Domain or non-'/' Path attribute (effectively enforcing same-origin semantics.) These prefixes provide important security benefits in those browsers implementing Secure Cookies and degrade gracefully (i.e. the special semantics may not be enforced in other cookie APIs but the cookies work normally and the async cookies API enforces the secure semantics for write operations) in other browsers. A major goal of this API is interoperation with existing cookies, though, so a few examples have also been provided using cookie names lacking these prefixes.

Prefix rules are also enforced in write operations by this API, but may not be enforced in the same browser for other APIs. For this reason it is inadvisable to rely on their enforcement too heavily until and unless they are more broadly adopted.

8.6. URL scoping

Although a service worker script cannot directly access cookies today, it can already use controlled rendering of in-scope HTML and script resources to inject cookie-monitoring code under the remote control of the service worker script. This means that cookie access inside the scope of the service worker is technically possible already, it’s just not very convenient.

When the service worker is scoped more narrowly than / it may still be able to read path-scoped cookies from outside its scope’s path space by successfully guessing/constructing a 404 page URL which allows IFRAME-ing and then running script inside it the same technique could expand to the whole origin, but a carefully constructed site (one where no out-of-scope pages are IFRAME-able) can actually deny this capability to a path-scoped service worker today and I was reluctant to remove that restriction without further discussion of the implications.

8.7. Cookie aversion

To reduce complexity for developers and eliminate the need for ephemeral test cookies, this async cookies API will explicitly reject attempts to write or delete cookies when the operation would be ignored. Likewise it will explicitly reject attempts to read cookies when that operation would ignore actual cookie data and simulate an empty cookie jar. Attempts to observe cookie changes in these contexts will still "work", but won’t invoke the callback until and unless read access becomes allowed (due e.g. to changed site permissions.)

Today writing to document.cookie in contexts where script-initiated cookie-writing is disallowed typically is a no-op. However, many cookie-writing scripts and frameworks always write a test cookie and then check for its existence to determine whether script-initiated cookie-writing is possible.

Likewise, today reading document.cookie in contexts where script-initiated cookie-reading is disallowed typically returns an empty string. However, a cooperating web server can verify that server-initiated cookie-writing and cookie-reading work and report this to the script (which still sees empty string) and the script can use this information to infer that script-initiated cookie-reading is disallowed.

9. Privacy Considerations

9.1. Clear cookies

This section is non-normative.

When a user clears cookies for an origin, the user agent needs to wipe all storage for that origin; including service workers and DOM-accessible storage for that origin. This is to prevent websites from restoring any user identifiers in persistent storage after a user initiates the action.

10. Acknowledgements

Thanks to Benjamin Sittler, who created the initial proposal for this API.

Many thanks to Adam Barth, Alex Russell, Andrea Marchesini, Andrew Williams, Anne van Kesteren, Ben Kelly, Craig Francis, Daniel Appelquist, Daniel Murphy, Domenic Denicola, Elliott Sprehn, Fagner Brack, Jake Archibald, Joel Weinberger, Kenneth Rohde Christiansen, Lukasz Olejnik, Marijn Kruisselbrink, and Mike West for helping craft this proposal.

Special thanks to Tab Atkins, Jr. for creating and maintaining Bikeshed, the specification authoring tool used to create this document, and for his general authoring advice.

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. [RFC2119]

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/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[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
[RFC6265bis]
L. Chen; et al. Cookies: HTTP State Management Mechanism. Internet-Draft. URL: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis
[Service-Workers]
Jake Archibald; Marijn Kruisselbrink. Service Workers. URL: https://w3c.github.io/ServiceWorker/
[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/

Informative References

[ECMAScript]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/

IDL Index

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options = {});

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options = {});

  Promise<undefined> set(USVString name, USVString value);
  Promise<undefined> set(CookieInit options);

  Promise<undefined> delete(USVString name);
  Promise<undefined> delete(CookieStoreDeleteOptions options);

  [Exposed=Window]
  attribute EventHandler onchange;
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
};

enum CookieSameSite {
  "strict",
  "lax",
  "none"
};

dictionary CookieInit {
  required USVString name;
  required USVString value;
  DOMHighResTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  CookieSameSite sameSite = "strict";
  boolean partitioned = false;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean partitioned = false;
};

dictionary CookieListItem {
  USVString name;
  USVString value;
  USVString? domain;
  USVString path;
  DOMHighResTimeStamp? expires;
  boolean secure;
  CookieSameSite sameSite;
  boolean partitioned;
};

typedef sequence<CookieListItem> CookieList;

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStoreManager {
  Promise<undefined> subscribe(sequence<CookieStoreGetOptions> subscriptions);
  Promise<sequence<CookieStoreGetOptions>> getSubscriptions();
  Promise<undefined> unsubscribe(sequence<CookieStoreGetOptions> subscriptions);
};

[Exposed=(ServiceWorker,Window)]
partial interface ServiceWorkerRegistration {
  [SameObject] readonly attribute CookieStoreManager cookies;
};

[Exposed=Window,
 SecureContext]
interface CookieChangeEvent : Event {
  constructor(DOMString type, optional CookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary CookieChangeEventInit : EventInit {
  CookieList changed;
  CookieList deleted;
};

[Exposed=ServiceWorker]
interface ExtendableCookieChangeEvent : ExtendableEvent {
  constructor(DOMString type, optional ExtendableCookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary ExtendableCookieChangeEventInit : ExtendableEventInit {
  CookieList changed;
  CookieList deleted;
};

[SecureContext]
partial interface Window {
  [SameObject] readonly attribute CookieStore cookieStore;
};

partial interface ServiceWorkerGlobalScope {
  [SameObject] readonly attribute CookieStore cookieStore;

  attribute EventHandler oncookiechange;
};

Issues Index

Note that it’s up for discussion whether these character restrictions should also apply to expires, domain, path, and sameSite as well. [Issue #httpwg/http-extensions#1593]
MDN

CookieChangeEvent/CookieChangeEvent

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieChangeEvent/changed

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieChangeEvent/deleted

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieChangeEvent

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore/change_event

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore/change_event

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore/delete

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore/get

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore/getAll

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore/set

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStore

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStoreManager/getSubscriptions

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStoreManager/subscribe

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStoreManager/unsubscribe

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

CookieStoreManager

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

ExtendableCookieChangeEvent/ExtendableCookieChangeEvent

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

ExtendableCookieChangeEvent/changed

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

ExtendableCookieChangeEvent/deleted

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

ExtendableCookieChangeEvent

In only one current engine.

FirefoxNoneSafariNoneChrome87+
Opera?Edge87+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?