How authentication system works in web apps? | PART 1

By Prajwal Haniya

Techletter #44 | September 19, 2023

In this series, let’s dive into the complete engineering mechanism of how an authentication system works. It is one of the fundamental building blocks of most of the software applications. Here, let’s implement it with nodejs and passportjs .

We will understand two strategies session-based authentication and JSON Web Tokens.

HTTP Headers & Cookies

HTTP headers are metadata or key-value pairs included in the HTTP request and response messages exchanged between a client (typically a web browser) and a server (where a website or web application is hosted). These headers provide additional information about the request or response and help both the client and server understand and process the data being transmitted.

HTTP headers can be categorized into two main groups: request headers and response headers.

  1. Request Headers: These headers are sent by the client (e.g., a web browser) to the server when initiating an HTTP request. Some common request headers include:

    • User-Agent: Provides information about the client making the request, such as the browser type and version.

    • Host: Specifies the domain or IP address of the server the client wants to communicate with.

    • Accept: Informs the server about the types of content (MIME types) the client can accept the response.

    • Cookie: Contains any cookies previously set by the server for this domain.

    • Authorization: Used for authentication purposes, often in the form of a username and password.

    • Referer (or Referer): Indicates the URL of the web page that led the client to the current request.

  2. Response Headers: These headers are sent by the server in response to an HTTP request. They provide information about the server’s response and instructions to the client. Some common response headers include:

    • HTTP Status Code: Not technically a header, but it’s an essential part of the response indicating the status of the request (e.g., 200 OK, 404 Not Found, 500 Internal Server Error).

    • Content-Type: Specifies the format of the data in the response (e.g., text/html, application/json, image/jpeg).

    • Content-Length: Indicates the size of the response content in bytes.

    • Location: Used in redirections to specify a new URL to which the client should navigate.

    • Cache-Control: Provides caching instructions for how the response can be stored and used by the client or intermediary caching systems.

    • Server: Identifies the server software and version being used.

What do you mean by a cookie? How it works?

A cookie is a small piece of data that a web server sends to a user’s web browser. The browser then stores this data locally on the user’s device. Cookies are used to store information that can be retrieved and sent back to the server with subsequent requests. They play a crucial role in enabling stateful interactions between web clients (such as browsers) and web servers. Here’s how cookies work:

  1. Creation and Sending: When a user visits a website, the web server can include one or more Set-Cookie headers in its HTTP response. These headers contain instructions for the browser to create and store cookies. Each Set-Cookie header typically includes a name, a value, and various attributes that control how the cookie behaves. For example:

    Set-Cookie: cookieName=cookieValue; expires=Thu, 31 Dec 2023 23:59:59 GMT; path=/; domain=.example.com
    

    In this example:

    • cookieName is the name of the cookie.

    • cookieValue is the value associated with the cookie.

    • expires specifies the expiration date and time. In this case, it’s set to December 31, 2023, at 23:59:59 GMT.

    The date and time format used in the expires attribute must follow the RFC 1123 standard, which includes the day of the week, day of the month, month, year, and time in GMT (Greenwich Mean Time) format.

    Here’s a breakdown of the parts of the expires attribute:

    • Thu: Day of the week (in abbreviated form, e.g., “Thu” for Thursday).

    • 31: Day of the month.

    • Dec: Month (in abbreviated form).

    • 2023: Year.

    • 23:59:59 GMT: Time in hours, minutes, seconds, and GMT time zone.

    When the user’s browser encounters a cookie with an expires attribute, it will automatically delete the cookie once the specified date and time have passed. If the expires attribute is not set, or if it’s set to a date in the past, the cookie will be treated as a session cookie, which is stored only for the duration of the user’s session and is deleted when the browser is closed.

  2. Storage: The browser receives the Set-Cookie header and stores the cookie locally on the user’s device. Cookies are typically stored in a file on the user’s hard drive or in memory, depending on the browser and its settings.

  3. Sending with Requests: When the user navigates to other pages on the same website or makes subsequent requests to the server (e.g., by clicking links or submitting forms), the browser automatically includes the stored cookies in the HTTP request headers for that website’s domain.

  4. Server-Side Processing: On the server side, the web application can access the cookies sent by the browser and use the data stored in them to make decisions and maintain user sessions.

  5. Expiration and Deletion: Cookies can have expiration dates, which means they are automatically deleted by the browser after a certain period. If a cookie doesn’t have an expiration date (a session cookie), it is deleted when the user closes their browser. Additionally, a server can instruct the browser to delete a cookie by sending a new Set-Cookie header with an expiration date in the past. You can see the above example for how the expiration is set.

Express Middlewares

As we are using nodejs & express, we need to understand how the express middleware works.

const express = require('express');
const app = express();

app.get('/', (req, res, next) => {
    res.send('Hello world');
})
app.listen(3000);

The above function can also be written along with the middleware as follows

const express = require('express');
const app = express();

function middlewareFn(req, res, next) {
    console.log('middleware');
    // Call next() -> it moves to the next middleware standardCallback
    next();
}

function standardCallback(req, res, next) {
    res.send('Hello world');
}

app.get('/', middlewareFn, standardCallback)
app.listen(3000);

Instead of calling the middleware inside of an API, we can make use of app.use(). This is helpful because, you can make use of many middlewares, and don’t have to pass it from each API that you are going to write.

const express = require('express');
const app = express();

app.use(middlewareFn);

function middlewareFn(req, res, next) {
    console.log('middleware');
    // Call next() -> it moves to the next middleware standardCallback
    next();
}

function standardCallback(req, res, next) {
    res.send('Hello world');
}

app.get('/', standardCallback)
app.listen(3000);

How you write app.use() affects how the function is called. For example, if you write app.use(middleware1) & next app.use(middleware2) they are going to be called accordingly. middleware1 first & then middleware2 . This means the order of how you call middlewares matter.

Error Handling

Error handling is one of the important functionality of your application. So, here, we will write another middleware that can handle errors more gracefully & improve both developer & user experience.

const express = require('express');
const app = express();

app.use(middlewareFn);
app.use(errorHandler);

function middlewareFn(req, res, next) {
    console.log('middleware');
    if (YOUR_ERROR_CONDITION) {
        const errorObj = new Error('Error occured in middleware');
        // this will call the errorHandler
        next(errorObj);
    } else {
        next();
    }
    
    
}

function errorHandler(err, req, res, next) {
    if (err) {
        res.send('Error occurred');
    }
}

function standardCallback(req, res, next) {
    res.send('Hello world');
}

app.get('/', standardCallback)
app.listen(3000);

Session

A “session” refers to a period of interaction and data exchange between a user and a web application or website. Sessions help in maintaining the state and managing user interactions. They are typically used to preserve user-specific data and activities across multiple HTTP requests and responses, allowing web applications to recognize and remember users as they navigate through a site.

But, why do we need a session when we already have a cookie?

Cookies and sessions are closely related but serve different purposes. While both can be used to maintain state in web applications, they have distinct characteristics and use cases. Here’s why we need sessions in addition to cookies:

Now, to create a session and set the cookie with the respective sessionId we will make use of express-session & express-mysql-session .

const express = require('express');
const app = express();
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);

const options = {
	host: 'localhost',
	port: 3306,
	user: 'session_test',
	password: 'password',
	database: 'session_test'
};

const sessionStore = new MySQLStore(options);

app.use(session({
	key: 'session_cookie_name',
	secret: 'session_cookie_secret',
	store: sessionStore,
	resave: false,
	saveUninitialized: false
}));

app.get('/', (req, res) => {
    res.send('Hello world');
});

app.listen(3000);

Sessions have a lot of application. For example keeping the website/article visit count, keeping track of products in the shopping cart, etc.

That’s it for this part, will be continuing with passport and JSON Web Tokens in the upcoming articles.