A note on jwt
JWT, short for JSON Web Tokens, is an authentication mechanism, rising in popularity in recent years.
JWT is well suited for single page and mobile applications, but it presents a new set of challenges. The typical flow for a frontend application wanting to authenticate against an API is the following:
- Frontend sends credentials to the backend
- Backend checks credentials and sends back a token
- Frontend sends the token on each subsequent request
Cookies and authentication
Authentication is one of the most challenging tasks in web development. There seems to be so much confusion around this topic, as token based authentication with JWT seems to supersede “old”, solid patterns like session based authentication.
Let’s see what role cookies play here.
Cookies are scoped by domain: the domain attribute
The value for the Domain attribute of a cookie controls whether the browser should accept it or not and where the cookie goes back.
Let’s see some examples.
NOTE: the following URL are on free Heroku instances. Give it a second to spin up. Open up a browser’s console before opening the links to see the result in the network tab.
Cookies can be kind of secret: the secure attribute
But not so secret after all.
Cookies cannot always travel over ajax requests
Consider a different situation where the backend runs stand-alone, so you have this Flask app running:
Now in a different folder, outside of the Flask app, create an index.html:
Create in the same folder a JavaScript file named index.js with the following code:
In the same folder, from the terminal run:
Cookies in javascript
You can use the document.cookie property to create, read, and delete cookies in JavaScript.
Cookies, document.cookie
Cookies are small strings of data that are stored directly in the browser. They are a part of the HTTP protocol, defined by the RFC 6265 specification.
Cookies are usually set by a web-server using the response Set-Cookie
HTTP-header. Then, the browser automatically adds them to (almost) every request to the same domain using the Cookie
HTTP-header.
One of the most widespread use cases is authentication:
- Upon sign in, the server uses the
Set-Cookie
HTTP-header in the response to set a cookie with a unique “session identifier”. - Next time when the request is sent to the same domain, the browser sends the cookie over the net using the
Cookie
HTTP-header. - So the server knows who made the request.
We can also access cookies from the browser, using document.cookie
property.
There are many tricky things about cookies and their options. In this chapter we’ll cover them in detail.
Does your browser store any cookies from this site? Let’s see:
The value of document.cookie
consists of name=value
pairs, delimited by ;
. Each one is a separate cookie.
To find a particular cookie, we can split document.cookie
by ;
, and then find the right name. We can use either a regular expression or array functions to do that.
We leave it as an exercise for the reader. Also, at the end of the chapter you’ll find helper functions to manipulate cookies.
We can write to document.cookie
. But it’s not a data property, it’s an accessor (getter/setter). An assignment to it is treated specially.
A write operation to document.cookie
updates only cookies mentioned in it, but doesn’t touch other cookies.
For instance, this call sets a cookie with the name user
and value John
:
If you run it, then probably you’ll see multiple cookies. That’s because the document.cookie=
operation does not overwrite all cookies. It only sets the mentioned cookie user
.
Technically, name and value can have any characters. To keep the valid formatting, they should be escaped using a built-in encodeURIComponent
function:
Cookies have several options, many of them are important and should be set.
The options are listed after key=value
, delimited by ;
, like this:
The url path prefix must be absolute. It makes the cookie accessible for pages under that path. By default, it’s the current path.
If a cookie is set with path=/admin
, it’s visible at pages /admin
and /admin/something
, but not at /home
or /adminpage
.
Usually, we should set path
to the root: path=/
to make the cookie accessible from all website pages.
A domain defines where the cookie is accessible. In practice though, there are limitations. We can’t set any domain.
There’s no way to let a cookie be accessible from another 2nd-level domain, so other.com
will never receive a cookie set at site.com
.
It’s a safety restriction, to allow us to store sensitive data in cookies that should be available only on one site.
By default, a cookie is accessible only at the domain that set it.
Please note, by default a cookie is also not shared to a subdomain as well, such as forum.site.com
.
…But this can be changed. If we’d like to allow subdomains like forum.site.com
to get a cookie set at site.com
, that’s possible.
For that to happen, when setting a cookie at site.com
, we should explicitly set the domain
option to the root domain: domain=site.com
. Then all subdomains will see such cookie.
For example:
For historical reasons, domain=.site.com
(with a dot before site.com
) also works the same way, allowing access to the cookie from subdomains. That’s an old notation and should be used if we need to support very old browsers.
To summarize, the domain
option allows to make a cookie accessible at subdomains.
By default, if a cookie doesn’t have one of these options, it disappears when the browser is closed. Such cookies are called “session cookies”
To let cookies survive a browser close, we can set either the expires
or max-age
option.
expires=Tue, 19 Jan 2038 03:14:07 GMT
The cookie expiration date defines the time, when the browser will automatically delete it.
The date must be exactly in this format, in the GMT timezone. We can use date.toUTCString
to get it. For instance, we can set the cookie to expire in 1 day:
If we set expires
to a date in the past, the cookie is deleted.
Is an alternative to expires
and specifies the cookie’s expiration in seconds from the current moment.
If set to zero or a negative value, the cookie is deleted:
The cookie should be transferred only over HTTPS.
By default, if we set a cookie at http://site.com
, then it also appears at https://site.com
and vice versa.
That is, cookies are domain-based, they do not distinguish between the protocols.
With this option, if a cookie is set by https://site.com
, then it doesn’t appear when the same site is accessed by HTTP, as http://site.com
. So if a cookie has sensitive content that should never be sent over unencrypted HTTP, the secure
flag is the right thing.
That’s another security attribute samesite
. It’s designed to protect from so-called XSRF (cross-site request forgery) attacks.
To understand how it works and when it’s useful, let’s take a look at XSRF attacks.
Imagine, you are logged into the site bank.com
. That is: you have an authentication cookie from that site. Your browser sends it to bank.com
with every request, so that it recognizes you and performs all sensitive financial operations.
Now, while browsing the web in another window, you accidentally come to another site evil.com
. That site has JavaScript code that submits a form <form action="https://bank.com/pay">
to bank.com
with fields that initiate a transaction to the hacker’s account.
The browser sends cookies every time you visit the site bank.com
, even if the form was submitted from evil.com
. So the bank recognizes you and actually performs the payment.
That’s a so-called “Cross-Site Request Forgery” (in short, XSRF) attack.
Real banks are protected from it of course. All forms generated by bank.com
have a special field, a so-called “XSRF protection token”, that an evil page can’t generate or extract from a remote page. It can submit a form there, but can’t get the data back. The site bank.com
checks for such token in every form it receives.
Such a protection takes time to implement though. We need to ensure that every form has the required token field, and we must also check all requests.
The cookie samesite
option provides another way to protect from such attacks, that (in theory) should not require “xsrf protection tokens”.
It has two possible values:
samesite=strict
(same assamesite
without value)
A cookie with samesite=strict
is never sent if the user comes from outside the same site.
In other words, whether a user follows a link from their mail or submits a form from evil.com
, or does any operation that originates from another domain, the cookie is not sent.
If authentication cookies have the samesite
option, then a XSRF attack has no chances to succeed, because a submission from evil.com
comes without cookies. So bank.com
will not recognize the user and will not proceed with the payment.
The protection is quite reliable. Only operations that come from bank.com
will send the samesite
cookie, e.g. a form submission from another page at bank.com
.
Although, there’s a small inconvenience.
When a user follows a legitimate link to bank.com
, like from their own notes, they’ll be surprised that bank.com
does not recognize them. Indeed, samesite=strict
cookies are not sent in that case.
We could work around that by using two cookies: one for “general recognition”, only for the purposes of saying: “Hello, John”, and the other one for data-changing operations with samesite=strict
. Then, a person coming from outside of the site will see a welcome, but payments must be initiated from the bank’s website, for the second cookie to be sent.
A more relaxed approach that also protects from XSRF and doesn’t break the user experience.
Lax mode, just like strict
, forbids the browser to send cookies when coming from outside the site, but adds an exception.
A samesite=lax
cookie is sent if both of these conditions are true:
The HTTP method is “safe” (e.g. GET, but not POST).
The full list of safe HTTP methods is in the RFC7231 specification. Basically, these are the methods that should be used for reading, but not writing the data. They must not perform any data-changing operations. Following a link is always GET, the safe method.
The operation performs a top-level navigation (changes URL in the browser address bar).
That’s usually true, but if the navigation is performed in an
<iframe>
, then it’s not top-level. Also, JavaScript methods for network requests do not perform any navigation, hence they don’t fit.
So, what samesite=lax
does, is to basically allow the most common “go to URL” operation to have cookies. E.g. opening a website link from notes that satisfy these conditions.
But anything more complicated, like a network request from another site or a form submission, loses cookies.
If that’s fine for you, then adding samesite=lax
will probably not break the user experience and add protection.
Overall, samesite
is a great option.
There’s a drawback:
samesite
is ignored (not supported) by very old browsers, year 2022 or so.
So if we solely rely on samesite
to provide protection, then old browsers will be vulnerable.
But we surely can use samesite
together with other protection measures, like xsrf tokens, to add an additional layer of defence and then, in the future, when old browsers die out, we’ll probably be able to drop xsrf tokens.
This option has nothing to do with JavaScript, but we have to mention it for completeness.
The web-server uses the Set-Cookie
header to set a cookie. Also, it may set the httpOnly
option.
This option forbids any JavaScript access to the cookie. We can’t see such a cookie or manipulate it using document.cookie
.
That’s used as a precaution measure, to protect from certain attacks when a hacker injects his own JavaScript code into a page and waits for a user to visit that page. That shouldn’t be possible at all, hackers should not be able to inject their code into our site, but there may be bugs that let them do it.
Normally, if such a thing happens, and a user visits a web-page with hacker’s JavaScript code, then that code executes and gains access to document.cookie
with user cookies containing authentication information. That’s bad.
But if a cookie is httpOnly
, then document.cookie
doesn’t see it, so it is protected.
Here’s a small set of functions to work with cookies, more convenient than a manual modification of document.cookie
.
There exist many cookie libraries for that, so these are for demo purposes. Fully working though.
The shortest way to access a cookie is to use a regular expression.
The function getCookie(name)
returns the cookie with the given name
:
Here new RegExp
is generated dynamically, to match ; name=<value>
.
Please note that a cookie value is encoded, so getCookie
uses a built-in decodeURIComponent
function to decode it.
Sets the cookie’s name
to the given value
with path=/
by default (can be modified to add other defaults):
To delete a cookie, we can call it with a negative expiration date:
Together: cookie.js.
A cookie is called “third-party” if it’s placed by a domain other than the page the user is visiting.
For instance:
A page at
site.com
loads a banner from another site:<img src="https://ads.com/banner.png">
.Along with the banner, the remote server at
ads.com
may set theSet-Cookie
header with a cookie likeid=1234
. Such a cookie originates from theads.com
domain, and will only be visible atads.com
:Next time when
ads.com
is accessed, the remote server gets theid
cookie and recognizes the user:What’s even more important is, when the user moves from
site.com
to another siteother.com
, which also has a banner, thenads.com
gets the cookie, as it belongs toads.com
, thus recognizing the visitor and tracking him as he moves between sites:
Third-party cookies are traditionally used for tracking and ads services, due to their nature. They are bound to the originating domain, so ads.com
can track the same user between different sites, if they all access it.
Naturally, some people don’t like being tracked, so browsers allow to disable such cookies.
Also, some modern browsers employ special policies for such cookies:
This topic is not related to JavaScript at all, just something to keep in mind when setting cookies.
There’s a legislation in Europe called GDPR, that enforces a set of rules for websites to respect the users’ privacy. One of these rules is to require an explicit permission for tracking cookies from the user.
Please note, that’s only about tracking/identifying/authorizing cookies.
So, if we set a cookie that just saves some information, but neither tracks nor identifies the user, then we are free to do it.
But if we are going to set a cookie with an authentication session or a tracking id, then a user must allow that.
Websites generally have two variants of following GDPR. You must have seen them both already in the web:
If a website wants to set tracking cookies only for authenticated users.
To do so, the registration form should have a checkbox like “accept the privacy policy” (that describes how cookies are used), the user must check it, and then the website is free to set auth cookies.
If a website wants to set tracking cookies for everyone.
To do so legally, a website shows a modal “splash screen” for newcomers, and requires them to agree to the cookies. Then the website can set them and let people see the content. That can be disturbing for new visitors though. No one likes to see such “must-click” modal splash screens instead of the content. But GDPR requires an explicit agreement.
GDPR is not only about cookies, it’s about other privacy-related issues too, but that’s too much beyond our scope.
document.cookie
provides access to cookies.
- Write operations modify only cookies mentioned in it.
- Name/value must be encoded.
- One cookie may not exceed 4KB in size. The number of cookies allowed on a domain is around 20 (varies by browser).
Cookie options:
Additionally:
- Third-party cookies may be forbidden by the browser, e.g. Safari does that by default.
- When setting a tracking cookie for EU citizens, GDPR requires to ask for permission.
Creating and reading cookie example in browser
import Cookies from 'universal-cookie';const cookies =new Cookies();cookies.set('myName', 'Aman', { path: '/' });console.log(cookies.get('myName')) // Aman
Dealing with cors
CORS, acronym for Cross-Origin Resource Sharing, is a way for servers to control access to resources on a given origin, when JavaScript code running on a different origin requests these resources.
Delete a cookie
Deleting a cookie is very simple. All you need to do is set the expiration date to some time in the past with the same name, path, domain, and secure option:
document.cookie ="name=; expires=Thu, 01 Jan 1970 00:00:00 UTC";
You can also use the setCookie() function to remove the cookie:
setCookie('name','',-1);
Read Next:How to use cookies in Spring Boot
Further resources
Icon in the featured picture by freepik.
Google chrome
В DevTools (Windows: F12, MacOS: ⌘ ⌥ i), вкладка «Application», раздел «Storage» – «Cookies».
Max-age и expires
max-age устанавливает время жизни куки в секундах, а параметр expires задает непосредственно дату окончания в формате RFC-822 или RFC-1123 (Mon, 03 Jul 2021 10:00:00 GMT).
Mozilla firefox
В веб-консоли (Windows: F12, MacOS: ⌘ ⌥ k), вкладка «Хранилище», раздел «Куки».
Read a cookie
To read a cookie in JavaSript, you just access document.cookie:
const cookies = document.cookie;
The document.cookie property returns all cookies set by the server as a series of key-value pairs separated by semi-colons:
_ga=GA1.2.315746813.1624003242; _gid=GA1.2.315746813.1624003242; lesson_completed=false;theme=dark
Since all the values and names are URL-encoded, you have to decode them using the decodeURIComponent() method.
If you are looking for a specific cookie value, you can use the following function:
constgetCookie=(name)=>{const cookies = document.cookie.split(';');for(let i =0; i < cookies.length; i ){let c = cookies[i].trim().split('=');if(c[0]=== name){return c[1];}}return"";}
console.log(getCookie('_ga'));
The getCookie() function uses the JavaScript split() method to split the cookie string by semi-colon. Then it loops through the result array to match the name of the requested cookie with the key-value pairs.
Reading cookie example in server
import Cookies from 'universal-cookie';const cookies =new Cookies(req.headers.cookie);console.log(cookies.get('myName')); // Aman or undefined if not set yet
For detailed function included in the package, Check NPM Package:- Universal-Cookie
Hope this helps 🙂
Secure
The secure flag is used to make a cookie secure.
Update a cookie
You can update a cookie in the same way as you create it with the same name, path, domain, and secure option:
document.cookie ='name=John Doe; expires=Sat, 10 Jul 2021 10:52:32 UTC';
You could also use the above setCookie() function:
setCookie('name','John Doe',10);
When to use session based authentication?
Use it whenever you can. Session based authentication is one of the simplest, secure, and straightforward form of authentication for websites. It’s available by default on all the most popular web frameworks like Django.
But, its stateful nature is also its main drawback, especially when a website is served by a load balancer. In this case, techniques like sticky sessions, or storing sessions on a centralized Redis storage can help.
Working with cookies in reactjs
While working in a Javascript, you can have some simpler ways to work with cookies via npm package universal-cookie
npm install universal-cookie --save
Working with samesite
At the time of writing, third-party cookies causes a warning to pop up in the Chrome console: