Creating an Identity Service with Node.js Part 1

Creating an Identity Service with Node.js Part 1

September 13, 2020

A common pattern that is required when writing applications is to provide a way for users to authenticate, sometimes we can utilise an OAuth2 provider like Auth0 or Okta, but this isn't always the most suitable solution given contraints like a short budget or wanting to have authentication as part of your core service.

This article is going to cover a simple setup for identity, for enterprise grade identity where the implementation has been heavily tested & verified, use a service like Auth0 or Okta is possible.The code in this article was written with the best intentions in mind, and we will identify as part of this article points that need to be considered before implementing this code, or any authentication mechanisms, within your project.

For a minimum viable product we need to be able to exchange credentials for a bearer token, and to be able to validate those bearer tokens at a later time. There are many other features we could implement in our identity service, for example WebAuthn support, however we will cover additions to the service we're creating in future articles.

For this article I am assuming you have both Node.js and the npm cli installed, and are familiar with the command line.

I am running these examples in the directory ~/src/identity-service on macOS 10.14 with Node.js 11.13.0 and npm cli version 6.7.0

We're going to first create our package, and install express which we will use to route requests to handlers within our service, we're also going to utilise esm to support the esm syntax while its in the experimental phase in Node.js

npm init # Create your package.json file
npm install --save express
npm install --save-dev esm

We're going to house all our service files in a service folder, within our service folder we're also going to need a few files that we're going to use for our setup & handlers

mkdir service
touch service/index.js
mkdir service/handlers
touch service/handlers/create-credentials.js
touch service/handlers/exchange-credentials.js
mkdir service/middleware
touch service/middleware/verify-bearer.js

We're going to set up a bear bones express instance that we will later add our routes to, this will be in our service/index.js file:

import express from "express";

const app  express();

// IIFE so we don't need to define `port` as `let` ¯\_(ツ)_/¯
const port = (() => {
  if (/^\d+$/.test(process.env.PORT)) {
      return +process.env.PORT;  
  }
  // Maybe you have other defaults you want to check here to decide on the port
  return 8080;
})();

app.listen(port, () => console.log(`Listening on port ${port}`));

We can run our service now using node -r esm service/index.js, you should see Listening on port 8080 in your console.

While developing we won't want to restart our service every time we make a change, so instead we're going to utilise the npm module nodemon, this will restart our service each time a file is changed in the service directory, we can install this by running:

npm install --save-dev nodemon

And then in our package.json file we're going to add a start key to our scripts object with the value nodemon --watch service -- -r esm service/index.js`:

{
  "scripts": {
      "start": "nodemon --watch service -- -r esm service/index.js"
  }
}

Now instead of running our full node command, we can run:

npm start

In your console you should now see something similar to this:

> identity-service@1.0.0 start ~/src/identity-service
> nodemon --watch service -- -r esm service/index.js

[nodemon] 1.18.11
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: ~/src/identity-service/service/**/*
[nodemon] starting `node -r esm service/index.js`
Listening on 8080

Now we have a service we can write code in we're going to create a handle to create new credentials, but first we're going to need a database to store said credentials in, for this we're going to utilise the npm module level, this will give us a database with no setup, ideal for development:

npm install --save level

We're going to import these modules and then create an instance that will be stored on our express instance using locals, this can be later accessed in our route handlers using request.app.locals:

// These will be at the top of your index.js file:
import level from "level"; 

// After `const app = express();` in our index.js file:
app.locals.store = level("./store");

Now we're going to create a handler to accept new credentials, we're going to expect a JSON request where the body is an object containing the key type with the value username-password, if the type doesn't match we will return the status code 400 for "Bad Request", if the type matches we're also going to expect a string value for username, and a string for password. For our username we're going to require a length of at least 1 (no reason we can't have a single character username), and for our password we're going to require a length of at least 8 characters (this seems to be the general number used for minimum length)

In our create-credentials.js file:

import express from "express";

async function handleCreateCredentialsUsernamePassword(request, response) {
  if (typeof request.body.username !== "string" || request.body.username.length < 1) {
      return response.sendStatus(400); 
  }
  if (typeof request.body.password !== "string" || request.body.password.length < 8) {
      return response.sendStatus(400); 
  }
     // We're going to create our credentials here!
}

function handleCreateCredentialsRoute(request, response, next) {
    if (!request.body) {
    return response.sendStatus(400);
  }
  // In future articles we're going to add new credential types! So lets prepare for that using a switch here
  switch(request.body.type) {
    case "username-password": return handleCreateCredentialsUsernamePassword(request, response).catch(next);
    default: response.sendStatus(400);
  }
}

// express allows a "handler" to be an array, as it will flatten out the 
// list of handlers and invoke them in serial
export default [
  // Parse our body as json
  express.json(),
  handleCreateCredentialsRoute
];

We're going to be receiving a plain text password, meaning it if we were to store the password as is anyone with access to our store will be able to read the password and know its value, so before saving the password in our database we're going to hash the password, hashing provides a way to retreive a representation of a password that can be verified against only if the original value is provided.

To hash our passwords we're going to utilise argon2 (recommended here), we're also going to create an identifier for our identity using pure-uuid:

npm install --save argon2 pure-uuid

In our handleCreateCredentialsUsernamePassword function we will hash our password, and then store our credentials in our store using the key credentials:lowerCaseUsername and the value as our password, we're also going to create an "identity" that can be accessed using the provided credentials.

// At the top of our file
import argon from "argon2";
import UUID from "pure-uuid";

// Within our `handleCreateCredentialsUsernamePassword` function
const hash = await argon.hash(request.body.password);
const username = request.body.username.trim();
const lowerCaseUsername = username.toLowerCase();
const credentialsKey = `credentials:${lowerCaseUsername}`;
const store = request.app.locals.store;
// Ensure it doesn't already exist
// Use catch to return falsy if the key doesn't exist
if (await store.get(credentialsKey).catch(() => undefined)) {
      return response.sendStatus(409); // Conflict 
}
// Create identifier scoped to our host
const uuid = new UUID(4).format();
const identity = {
  id: uuid,
  primaryUsername: username
};
// Store our identity
await store.put(`identity:${uuid}`, JSON.stringify(identity));
// Store our new credentials
await store.put(credentialsKey, JSON.stringify({
    hash,
  identity: uuid
}));
// Return 201, return our new id, it doesn't let the caller 
// do any additional with it, they will next need to invoke exchange-credentials 
// to make use of their new identity
return response.status(201).json({
  id: uuid
});

We also need to hook up this route, so in our index.js file we will add the route POST /create-credentials:

// At the top of our file
import createCredentialsHandler from "./handlers/create-credentials";

// After `app.locals.store = levelup(leveldown("./store"));`
app.post("/create-credentials", createCredentialsHandler);

Now when we start our service, we should be able to run this curl command, which will return our new identifier:

curl -XPOST http://localhost:8080/create-credentials -H "Content-Type: application/json" -d '{"type": "username-password", "username": "John Doe","password":"correcthorsebatterystaple"}'

If we run the same request again, we will receive the status 409 Conflict, meaning we have to select a different username.

Next: Creating an Identity Service with Node.js Part 2

Fabian Cook

Fabian Cook

Software Engineer @ Dovetail

JavaScript Developer.

Read similar articles

How to Make a Discord Bot in Node.js for Beginners

Check out our tutorial
How to Make a Discord Bot in Node.js for BeginnersHow to Make a Discord Bot in Node.js for Beginners

Implementing and Testing a Mongoose Model with CI/CD Integration

Check out our tutorial
Implementing and Testing a Mongoose Model with CI/CD IntegrationImplementing and Testing a Mongoose Model with CI/CD Integration