Cookie-based Authentication

Authentication

Authentication is the process of confirming the identity of a user, based on some form of credentials.

Some examples of credentials:

  • Email and password
  • Username and password
  • PIN
  • Fingerprint
  • Retina scanning

The job of an authentication mechanism is to accept some credentials and assert that the user is who they say they are.

Authentication vs Authorization

It's not to be confused with Authorization, which is the process of establishing the permissions and roles of an authenticated user. Authentication is about "who you are", while authorization is about "what you can do".

Basic forms of authentication on the web can be implemented very easily, with the help of cookies.


Cookie-based Authentication is the simplest form of authentication on the web, and it's also the most robust since it's been used for many years. A lot of big companies use this technique with great success, and even those who use other mechanisms (eg. OAuth 2.0) still use Cookies under the hoods to maintain a session.

For this technique to work, both the front-end and the back-end of an application must be on the same domain (or sub-domains). Before the advent of Single Page Applications, this was always the case because the HTML code was directly generated by some server on each navigation (think of a PHP website, like WordPress). Now, although you may have front-end and back-end on different servers, you have to make sure they're under the same domain if you want this to work.

You could always use a reverse proxy to forward a domain to another.

The flow

1. Entering the credentials

The user navigates to the site and sees a login screen: they enter their credentials.

2. The server verifies the credentials

A request is made to the server (eg. a POST request to /login), which verifies the credentials and, if they're correct, sends a response containing a cookie. The cookie usually contains an ID which refers to the user.

Remember, if you're in charge of the server: the cookie must use the attributes Secure, HttpOnly, SameSite (Lax or Strict), and optionally either Expires or Max-Age to keep the user logged in.

3. The front-end asks for the user information

From now on, the user is logged in…but the front-end doesn't know about it, because it cannot read the cookie (remember the HttpOnly flag)! So, it has to perform a request to an endpoint containing the user's information (eg. /me).

4. The server returns the user information

Since the Cookie is set, the request (and all subsequent requests) automatically includes it, the server verifies it and returns the user's info.

The front-end therefore determines that the user is logged in and shows some information.

Recap
1. `/login` POST request with credentials in body
2. Server responds and sets a Cookie
3. `/me` GET request to get the user's info
4. Server responds with the info

In order to logout, the front-end will call an endpoint (eg. /logout) which must remove the cookie.

That's it! That's the whole flow. As you can see, from the front-end point of view, there's no complexity at all. It's almost as if there was no authentication mechanism. It only has to take care of:

  • A login form which performs a request
  • A guard of some sort which checks if the user is authenticated before showing a page

And of course, the Cookie may always expire. So, make sure to redirect the user to the login screen when the server sends the HTTP error code 401. This is usually done with some global interceptor mechanism.

Variant: separate login page

If the front-end app is served by an actual server (such as NodeJS), you may also decide to handle the login page separately from the rest of the app. In this variant, the server detects the Cookie on the first navigation and:

  • If it's valid, it shows the app
  • If it's invalid, it shows the login screen

Then, when the app detects an expired cookie (eg. HTTP error 401) all it has to do is to refresh the current page. The server will see that the cookie is expired, and show the login screen.

This variant basically keeps the app free of all the authentication-related code (apart from the guard which has to refresh the page). It's just a matter of personal preferences: it may be useful in order to share a single login page with many apps under the same domain.

Back-end for Front-end

There's also a pattern called "Back-end for Front-end" (BFF), which is often implemented when you really can't have front-end and back-end on the same domain (eg. you don't own the server). It consists of creating a dedicated back-end on the same domain of the front-end, in order to assist it. Some recent frameworks have adopted this kind of architecture, think about NextJS and Remix which can handle both front-end and back-end code in the same app (even the same files!). But remember that you could also decide to split them into different projects, it doesn't affect the BFF pattern.

The flow could then be like this:

  • The front-end authenticates with the dedicated back-end
  • The dedicated back-end maintains a session with the front-end via cookies, and performs the actual authentication on another server with some other mechanism (eg. OAuth 2.0, which we'll see later)

Even with this pattern, the front-end complexity remains the same.