DEV Community

Cover image for Web Authentication By The Numbers (Part 1)
Jay F. Grissom
Jay F. Grissom

Posted on • Edited on

Web Authentication By The Numbers (Part 1)

How authentication layers are built up to create an authentication system for your website.

Audience

This article is intended for intermediate level website developers and software engineers. I've tried to make it approachable for beginners but it's really not for absolute beginners.

Video

Problem

Website authentication can be a very confusing topic. There are a lot of considerations when thinking about an authentication system for your web projects. It's overwhelming because authentication can be extremely simple or it can be a layer cake of individual systems that each build on top of each other.

Approach

In this series we're going to start with no authentication and then you'll add a very basic authentication system. Then you'll progressively add and remove layers on top of it to make it a full blown Authentication System for your website using PassportJS.

Once that is done you'll learn how to go one step further and implement Xumm (a crypto currency wallet) SignIn as a stand-in for a traditional user:password based authentication mechanism. We'll do this using a new PassportJS strategy I've created to authenticate your users with Xumm.

For the grand finale you'll learn how to implement all of this in BlitzJS by executing a single line of code using a BlitzJS recipe.

Assumptions

The examples here use localhost without Transport Layer Security. I'll assume you understand that all this is not secure in a real world production environment without TLS.

For early portions of this series I'll assume

  1. You're familiar with Typescript.
  2. You know how to setup NodeJS and ExpressJS.
  3. You're familiar with the concept of middleware for ExpressJS.
  4. You know how to use Postman to make calls to your application as if you're a client.
  5. You're familiar with PassportJS but may not have implemented it previously.

For late portions of this series I'll assume

  1. You understand that Xumm is a wallet for the XRP Ledger (an open source crypto currency project).
  2. You're familiar with BlitzJS.

GitHub Repo

If you want to follow along with examples there is a branch for each type of authentication system we're building here over on my corresponding Web Authentication By The Numbers Github Repo.

GitHub logo jfgrissom / web-authentication-by-the-numbers

This repository goes with the article by the same name on dev.to.

web-authentication-by-the-numbers

This repository goes with the article by the same name on dev.to.




Starting With No Authentication (Step 0)

Initially we'll start the application on the master branch where there is no authentication. See the index.ts file on the master branch for this.

import express, { Application, Request, Response } from 'express'

const app: Application = express()
const port = 3000

app.get('/', async (req: Request, res: Response): Promise<Response> => {
  return res.status(200).send({
    message: "Hello World! I'm not authenticated."
  })
})

app.listen(port)
Enter fullscreen mode Exit fullscreen mode

Making a call to this using Postman will return this.

{
    "message": "Hello World! I'm not authenticated."
}
Enter fullscreen mode Exit fullscreen mode

Primitive Authentication System (Step 1)

Probably the most primitive authentication system we can build with express contains a simple set of hard coded credentials. Using this basic auth example we can setup some thing like this.

NOTE: This authentication system is horrible for many reasons. Don't use this in your app (the user and password will be checked into Github). This example is just to help you understand what is going on here.

import express, { Application, Request, Response, NextFunction } from 'express'
import auth from 'basic-auth'

const app: Application = express()
const port = 3000

app.use((req: Request, res: Response, next: NextFunction) => {
  let user = auth(req)

  if (
    user === undefined ||
    user['name'] !== 'admin' ||
    user['pass'] !== 'supersecret'
  ) {
    res.statusCode = 401
    res.setHeader('WWW-Authenticate', 'Basic realm="Node"')
    res.end('Unauthorized')
  } else {
    next()
  }
})

app.get('/', async (req: Request, res: Response): Promise<Response> => {
  return res.status(200).send({
    message: "Hello World! I'm authenticated."
  })
})

app.listen(port)
Enter fullscreen mode Exit fullscreen mode

Once you get basicAuth added to your application you can try to make a call to the service using Postman but you'll just get an empty response with a status code of 401 Unauthorized.

To get an authenticated response you'll need to setup credentials in the "Authorization" tab of your Postman request. The Username is "admin" and the Password is "supersecret".

Make the request again with these credentials and you'll get this for a response.

{
    "message": "Hello World! I'm authenticated."
}
Enter fullscreen mode Exit fullscreen mode

At this point you've got a password database and you can accept "Basic Authentication Headers" from any client.

The user database can be much more complicated than this. It could be in a database or provided by an external authentication provider (like AWS Cognito). For now we'll leave it simple and just keep using basicAuth.

Session Support (Step 2)

So providing credentials every time someone requests something from your site is OK if the client is an API consumer (like another web service). However this isn't typically how you would handle authentication for users who show up to your site using a web browser.

So what resources will you need to create to provide this functionality?

  1. At this point you'll need to provide some webpage features that allow a user to login, use authorized resources, and logout.
  2. You'll also need something that won't require them to login every time they click on something within the page.

Let's begin by adding session support to the project.

To see the code for this take a look at the session support branch of the repo.

NOTE: This branch intentionally doesn't have authentication in it.

import express, { Application, Request, Response } from 'express'
import session from 'express-session'

const app: Application = express()
const port = 3000

const sessionOptions = {
  secret: 'session secret that is not secret'
}

app.use(session(sessionOptions))

app.get('/', async (req: Request, res: Response): Promise<Response> => {
  return res.send(`Session ID: ${req.session.id}`)
})

app.listen(port)
Enter fullscreen mode Exit fullscreen mode

Once you've updated this file connect to your site using a web browser at http://localhost:3000/. When you do this you should see a result similar to this on your web page Session ID: Outbyq2G_EYkL5VQzAdKlZIZPYfaANqB.

NOTE: To keep your browser sessions secure in production you would not share this session ID over an unsecured connection. You would use https (TLS).

So what is this session good for exactly? I'm glad you asked! This session is your server's way of keeping track of browser sessions (note it doesn't take care of user sessions - at least not yet anyway). The session solves the problem of requiring a user to login every time they click on something within the page.

So you've got a session and you've got a user database. How exactly do these things tie together?

The session is tied to a specific client (in this case a browser). The way the server and browser share data related to this session is through a cookie. If you look at the cookies in your browser you'll see that it matches the ID that was presented in your web page.

Session Support with User Support (Step 3)

So how to the session and the user tie together?

In this example we'll reintroduce the Basic Authentication feature by merging in the two previous branches we created (feature/basic-auth and feature/session-support).

You should end up with with this after accounting for previously existing sessions. See the code here.

import express, { Application, Request, Response, NextFunction } from 'express'
import session from 'express-session'
import auth from 'basic-auth'

// Add the session data we need that is specific to our application.
declare module 'express-session' {
  interface SessionData {
    userToken?: string
    tokenExpiration?: number
  }
}

const app: Application = express()
const port = 3000

const sessionOptions = {
  secret: 'session secret that is not secret',
  cookie: {
    httpOnly: true // Only let the browser modify this, not JS.
  }
}

app.use(session(sessionOptions))

app.use((req: Request, res: Response, next: NextFunction) => {
  // If we have a previous session with key session data then we are authenticated.
  const currentTime = Date.now() / 1000
  if (
    req.session.userToken &&
    req.session.tokenExpiration &&
    req.session.tokenExpiration > currentTime
  ) {
    next()
    return
  }

  // If no prior session was established and bad credentials were passed.
  const user = auth(req)
  if (
    user === undefined ||
    user['name'] !== 'admin' ||
    user['pass'] !== 'supersecret'
  ) {
    res.statusCode = 401
    res.setHeader('WWW-Authenticate', 'Basic realm="Node"')
    res.end('Unauthorized')
    return
  }

  // Create a new session for the user who has passed good credentials.
  req.session.userToken = user.name
  req.session.tokenExpiration = currentTime + 15 // 15 second session.
  next()
})

app.get('/', async (req: Request, res: Response): Promise<Response> => {
  const currentTime = Date.now() / 1000
  return res.send(`
  Session ID: ${req.session.id} <br/>
  Authenticated Username: ${auth(req)?.name} <br/>
  User Token: ${req.session.userToken} <br/>
  Current Time: ${currentTime} <br/>
  Session Expiration: ${req.session.tokenExpiration}
  `)
})

app.listen(port)
Enter fullscreen mode Exit fullscreen mode

You have session functionality and you have basic authentication functionality.

You can test how the page behaves without credentials by going to the page in a web browser and clicking cancel when prompted for a username and password. You should see a 401 Error in the console and unauthorized on the web page.

You can test how the page behaves with credentials by prepending the username and password in the url so that it looks like this http://admin:supersecret@localhost:3000/.

Session ID: Wc29HPGVTdnx0VqsDr7uaxWPTV3KoIzO
Authenticated Username: admin
User Token: admin
Current Time: 1637179009.834
Session Expiration: 1637179024.829
Enter fullscreen mode Exit fullscreen mode

You can test out the session persistence by refreshing the page. You'll notice that the User Token remains admin but the Authenticated Username becomes undefined.

To test out the session expiring by passing good credentials like this http://admin:supersecret@localhost:3000/. Then you can pass bad bad credentials to the page like this http://bad:credentials@localhost:3000/. Then refresh the page repeatedly until the session expires after 15 seconds. When the tokenExpires then you'll see a prompt show up for the Username and Password (just click cancel). NOTE: This is most easily done in Chrome because it will not automatically cache (and reuse) good credentials after you've passed bad credentials.

With this latest iteration we've answered a few questions.

  1. How do we access the name of the user? You can see the Authenticated username came in through the authenticated request auth(req) and that if we want to use it again we'll need to access it through the session.
  2. How does our system know if the user previously was authenticated? It knows because a prior session was established.
  3. Why can't a browser just manipulate the cookie and add data we are expecting? We are telling browsers that they can't make changes to the cookie using Javascript with the httpOnly directive {cookie: { httpOnly: true }}. Our server knows the state of the cookie and will reject it if the client changes the cookie.

So what if you don't want to use Basic Auth? This is a very reasonable thing. Basic auth is pretty terrible for a lot of reasons.

I've added more to this in the next section of this series. The section is called Web Authentication By the Numbers (Part 2) and it deals directly with setting up PassportJS using the Local Strategy.

Article Image Credit

Photo by Parsoa Khorsand on Unsplash

Top comments (0)