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:
-
a single browser process,
-
a single-threaded event loop model, and
-
no general expectation of responsiveness for scripted event handling while processing cookie operations
... to the modern web which strives for smoothly responsive high performance:
-
in multiple browser processes,
-
with a multithreaded, multiple-event loop model, and
-
with an expectation of responsiveness on human-reflex time scales.
On the modern web a cookie operation in one part of a web application cannot block:
-
the rest of the web application,
-
the rest of the web origin, or
-
the browser as a whole.
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:
-
write (or "set") and delete (or "expire") cookies
-
read (or "get") script-visible cookies
-
... including for specified in-scope request paths in service worker contexts
-
-
monitor script-visible cookies for changes using
CookieChangeEvent
-
... in long-running script contexts (e.g.
document
) -
... for script-supplied in-scope request paths in service worker contexts
-
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
Promise
s.
Both
methods
take
the
same
arguments,
which
can
be
either:
-
a name, or
-
a dictionary of options (optional for
getAll()
)
The
get()
method
is
essentially
a
form
of
getAll()
that
only
returns
the
first
result.
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} ` ); }
try { const cookies= await cookieStore. getAll( 'session_id' }); for ( const cookieof 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 .
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.
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.
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.
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.
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.
change
events
in
documents:
cookieStore. addEventListener( 'change' , event=> { console. log( ` ${ event. changed. length} changed cookies` ); for ( const cookiein event. changed) console. log( `Cookie ${ cookie. name} changed to ${ cookie. value} ` ); console. log( ` ${ event. deleted. length} deleted cookies` ); for ( const cookiein 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.
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.
const subscriptions= await self. registration. cookies. getSubscriptions(); for ( const subof subscriptions) { console. log( sub. name, sub. url); }
2. Concepts
2.1. Cookie
A cookie is normatively defined for user agents by Cookies: HTTP State Management Mechanism § 5 User Agent Requirements .
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 :
-
The combined lengths of the name and value fields must not be greater than 4096 bytes (the maximum name/value pair size ).
-
The length of every field except the name and value fields must not be greater than 1024 bytes (the maximum attribute value size ).
2.2. Cookie Store
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 .
-
A newly-created cookie is inserted into the cookie store .
-
A user agent evicts expired cookies from the cookie store .
-
A user agent removes excess cookies from the cookie store .
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 = "strict"; };
sameSite dictionary {
CookieStoreDeleteOptions required USVString ;
name USVString ?=
domain null ;USVString = "/"; };
path dictionary {
CookieListItem USVString ;
name USVString ;
value USVString ?;
domain USVString ;
path DOMHighResTimeStamp ?;
expires boolean ;
secure CookieSameSite ; };
sameSite typedef sequence <CookieListItem >;
CookieList
3.1.
The
get()
method
-
cookie
=
await
cookieStore
.
get
( name )- cookie = await cookieStore .
get
( options ) - cookie = await cookieStore .
-
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()
ordocument.domain
.
get(
name
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
Let url be settings ’s creation URL .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Return p .
get(
options
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
Let url be settings ’s creation URL .
-
If options is empty, then return a promise rejected with a
TypeError
. -
If options ["
url
"] is present, then run these steps:-
Let parsed be the result of parsing options ["
url
"] with settings ’s API base URL . -
If this 's relevant global object is a
Window
object and parsed does not equal url , then return a promise rejected with aTypeError
. -
If parsed ’s origin and url ’s origin are not the same origin , then return a promise rejected with a
TypeError
. -
Set url to parsed .
-
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Return p .
3.2.
The
getAll()
method
-
cookies
=
await
cookieStore
.
getAll
( name )- cookies = await cookieStore .
getAll
( options ) - cookies = await cookieStore .
-
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()
ordocument.domain
.
getAll(
name
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
Let url be settings ’s creation URL .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Let list be the results of running query cookies with url and name .
-
Otherwise, resolve p with list .
-
-
Return p .
getAll(
options
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
Let url be settings ’s creation URL .
-
If options ["
url
"] is present, then run these steps:-
Let parsed be the result of parsing options ["
url
"] with settings ’s API base URL . -
If this 's relevant global object is a
Window
object and parsed does not equal url , then return a promise rejected with aTypeError
. -
If parsed ’s origin and url ’s origin are not the same origin , then return a promise rejected with a
TypeError
. -
Set url to parsed .
-
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Let list be the results of running query cookies with url and options ["
name
"] (if present). -
Otherwise, resolve p with list .
-
-
Return p .
3.3.
The
set()
method
set(
name
,
value
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
If origin is not a potentially trustworthy origin , or if origin ’s scheme is "
file
", then return a promise rejected with aTypeError
. Let url be settings ’s creation URL .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Let r be the result of running set a cookie with url , name , value .
-
If r is failure, then reject p with a
TypeError
and abort these steps. -
Resolve p with undefined.
-
-
Return p .
set(
options
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
If origin is not a potentially trustworthy origin , or if origin ’s scheme is "
file
", then return a promise rejected with aTypeError
. Let url be settings ’s creation URL .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Return p .
3.4.
The
delete()
method
delete(
name
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
If origin is not a potentially trustworthy origin , or if origin ’s scheme is "
file
", then return a promise rejected with aTypeError
. Let url be settings ’s creation URL .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Let r be the result of running delete a cookie with url , name , null, "
/
", true, and "strict
". -
If r is failure, then reject p with a
TypeError
and abort these steps. -
Resolve p with undefined.
-
-
Return p .
delete(
options
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let origin be settings ’s origin .
-
If origin is an opaque origin , then return a promise rejected with a "
SecurityError
"DOMException
. -
If origin is not a potentially trustworthy origin , or if origin ’s scheme is "
file
", then return a promise rejected with aTypeError
. Let url be settings ’s creation URL .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
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()
andgetAll()
, with optionalname
andurl
properties.Once subscribed, notifications are delivered as "
cookiechange
" events fired against the Service Worker 's global scope:
subscribe(
subscriptions
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let registration be this 's registration .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Let subscription list be registration ’s associated cookie change subscription list .
-
For each entry in subscriptions , run these steps:
-
Let name be entry ["
name
"]. -
Let url be the result of parsing entry ["
url
"] with settings ’s API base URL . -
If url does not start with registration ’s scope url , then reject p with a
TypeError
and abort these steps. -
Let subscription be the cookie change subscription ( name , url ).
-
If subscription list does not already contain subscription , then append subscription to subscription list .
-
-
Resolve p with undefined.
-
-
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.
getSubscriptions()
method
steps
are:
-
Let registration be this 's registration .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
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 fromgetSubscriptions()
.
unsubscribe(
subscriptions
)
method
steps
are:
-
Let settings be this 's relevant settings object .
-
Let registration be this 's registration .
-
Let p be a new promise .
-
Run the following steps in parallel :
-
Let subscription list be registration ’s associated cookie change subscription list .
-
For each entry in subscriptions , run these steps:
-
Let name be entry ["
name
"]. -
Let url be the result of parsing entry ["
url
"] with settings ’s API base URL . -
If url does not start with registration ’s scope url , then reject p with a
TypeError
and abort these steps. -
Let subscription be the cookie change subscription ( name , url ).
-
Remove any item from subscription list equal to subscription .
-
-
Resolve p with undefined.
-
-
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.
self. registration. cookies. subscribe([{ name: 'session-id' }]);
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
Cookie attribute-values are stored as byte sequences , not strings.
To encode a string , run UTF-8 encode on string .
To decode a value , run UTF-8 decode without BOM on value .
Note: This is the same representation used for time values in [ECMAScript] .
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:
-
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.
-
Let list be a new list .
-
For each cookie in cookie-list , run these steps:
-
Assert: cookie ’s http-only-flag is false.
-
If name is given, then run these steps:
-
Let item be the result of running create a CookieListItem from cookie .
-
Append item to list .
-
-
Return list .
To
create
a
CookieListItem
from
cookie
,
run
the
following
steps.
-
Let expires be cookie ’s expiry-time ( as a timestamp ).
-
Let secure be cookie ’s secure-only-flag .
-
Switch on cookie ’s same-site-flag :
-
Return «[ "name" → name , "value" → value , "domain" → domain , "path" → path , "expires" → expires , "secure" → secure , "sameSite" → sameSite ]»
Note: The cookie ’s creation-time , last-access-time , persistent-flag , host-only-flag , and http-only-flag attributes are not exposed to script.
7.2. Set a Cookie
To set a cookie with url , name , value , optional expires , domain , path , and sameSite , run the following steps:
-
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]
-
If name ’s length is 0 and value contains U+003D (
=
), then return failure. -
If name ’s length is 0 and value ’s length is 0, then return failure.
-
Let encodedName be the result of UTF-8 encoding name .
-
Let encodedValue be the result of UTF-8 encoding value .
-
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.
-
Let host be url ’s host
-
Let attributes be a new list .
-
If domain is not null, then run these steps:
-
If domain starts with U+002D (
.
), then return failure. -
If host does not equal domain and host does not end with U+002D (
.
) followed by domain , then return failure. -
Let encodedDomain be the result of UTF-8 encoding domain .
-
If the byte sequence length of encodedDomain is greater than the maximum attribute value size , then return failure.
-
Append `
Domain
`/ encodedDomain to attributes .
-
-
If expires is given, then append `
Expires
`/ expires ( date serialized ) to attributes . -
If path is not null, then run these steps:
-
If path does not start with U+002F (
/
), then return failure. -
If path does not end with U+002F (
/
), then append U+002F (/
) to path . -
Let encodedPath be the result of UTF-8 encoding path .
-
If the byte sequence length of encodedPath is greater than the maximum attribute value size , then return failure.
-
Append `
Path
`/ encodedPath to attributes .
-
-
Append `
Secure
`/`` to attributes . -
Switch on sameSite :
-
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.
-
Return success.
Note: Storing the cookie may still fail due to requirements in [RFC6265bis] but these steps will be considered successful.
7.3. Delete a Cookie
To delete a cookie with url , name , domain and path , run the following steps:
-
If path is not null, then run these steps:
-
If path does not start with U+002F (
/
), then return failure. -
If path does not end with U+002F (
/
), then append U+002F (/
) to path .
-
-
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.
-
Let value be the empty string.
-
Let sameSite be "
strict
".Note: The values for value , and sameSite will not be persisted by this algorithm.
-
Return the results of running set a cookie with url , name , value , expires , domain , path , and sameSite .
7.4. Process Changes
To process cookie changes , run the following steps:
-
For every
Window
window , run the following steps:-
Let url be window ’s relevant settings object 's creation URL .
-
Let changes be the observable changes for url .
-
Queue a global task on the DOM manipulation task source given window to fire a change event named "
change
" with changes at window ’sCookieStore
.
-
-
For every service worker registration registration , run the following steps:
-
Let changes be a new set .
-
For each change in the observable changes for registration ’s scope url , run these steps:
-
Let cookie be change ’s cookie.
-
For each subscription in registration ’s cookie change subscription list , run these steps:
-
-
Let changedList and deletedList be the result of running prepare lists from changes .
-
Fire a functional event named "
cookiechange
" usingExtendableCookieChangeEvent
on registration with these properties:
-
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 ):
-
A cookie which is removed due to an insertion of another cookie with the same name , domain , and path is ignored.
-
A newly-created cookie which is not immediately evicted is considered changed .
-
A newly-created cookie which is immediately evicted is considered deleted .
-
A cookie which is otherwise evicted or removed is considered deleted
To fire a change event named type with changes at target , run the following steps:
-
Let event be the result of creating an Event using
CookieChangeEvent
. -
Set event ’s
type
attribute to type . -
Set event ’s
bubbles
andcancelable
attributes to false. -
Let changedList and deletedList be the result of running prepare lists from changes .
-
Set event ’s
changed
attribute to changedList . -
Set event ’s
deleted
attribute to deletedList . -
Dispatch event at target .
To prepare lists from changes , run the following steps:
-
Let changedList be a new list .
-
Let deletedList be a new list .
-
For each change in changes , run these steps:
-
Let item be the result of running create a CookieListItem from change ’s cookie.
-
If change ’s type is changed , then append item to changedList .
-
Otherwise, run these steps:
-
-
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.
-
unsecured origins can typically overwrite cookies used on secure origins
-
superdomains can typically overwrite cookies seen by subdomains
-
cross-site scripting attacks and other script and header injection attacks can be used to forge cookies too
-
cookie read operations (both from script and on web servers) don’t give any indication of where the cookie came from
-
browsers sometimes truncate, transform or evict cookie data in surprising and counterintuitive ways
-
... due to reaching storage limits
-
... due to character encoding differences
-
... due to differing syntactic and semantic rules for cookies
-
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.