A basic Node authentication server (without packages)

Emma O'Donnell
10 min readJan 10, 2021

Auth has always been a bit intimidating to me. When working as a .Net developer I relied on ASP.NET Identity which obfuscated a lot of what was happening. It meant that setup was simple (follow a guide) but that configuration changes were uncomfortable. If I was asked to estimate a task involving auth I would suck air in through my teeth like a dodgy mechanic and shrug “could be a day, could be a week — who knows with auth?”. I certainly didn’t know.

I’m currently learning Node and when it came time to add auth to a project I realised I should probably peek inside the black box a little instead of immediately relying an abstraction. There’s are packages out there that seem like they would fill the Identity niche, but it’d be nice to not be in the dark about their internals. So today I’m going to build an auth server from scratch. I won’t be using any auth-specific packages but I will use a few for stuff adjacent to the core task (Express, data access etc) and for encryption (happy for that box to stay opaque).

This isn’t going to be something production grade — I’ve probably missed some stuff that’s important from a security POV— so for any deployed personal projects I’ll be continuing to rely on third party packages. But hopefully doing this from scratch can give me some idea as to what these packages are doing.

Starting simple

This article is going to cover the creation of a really simple auth server, minimalistic in a way that makes the American Psycho guy seem garish. I’ve got some API somewhere that needs to have authenticated endpoints — call this the Client API. I decide I’m going to push my auth logic off to a dedicated server, the Auth Server. What is the bare minimum that I need this Auth Server to do?

In the most basic case we need to support two actions: registration and login.

When a user registers with their email address and password we’ll encrypt the password and stick the details into a database.

When a user logs in the auth server will check that their email address and password are correct. If they are then the server will return an access token. The Client API will use this access token to authenticate future requests.

Registration

Registration then is pretty easy. All I need is an endpoint that:

A) Ensures email address is valid
B) Makes sure password meets requirements
C) Ensures user hasn’t already signed up
D) Adds user to db if all conditions are met

I’m using mongo db for this (probably not the best choice but I’m practicing with it at the moment).

The RegisterUserService has two methods: getValidationRequirements() and registerUser().

getValidationRequirements() returns a ValidationChain array that can be used by express-validator. That array includes a step for ensuring the email is properly formatted and one for making sure the password meets all requirements.

registerUser() makes sure that the user isn’t already in the db. If they are we return a Conflict status code which will be (eventually) surfaced to the caller by my controller. If not then we copy the request into a new variable (user) and then overwrite the password with an encrypted version. Stick that user into the db and let the caller know everything went well. That’s it; registration service complete.

Login

At a high level this remains pretty simple — a user submits an email address and password. If we can’t find the email in the db or if the passwords don’t match (compare with bcrypt.compare() to check the plaintext password submitted by the user against the encrypted one in the db) then return an unauthorised response. If the user got through though then we return an access (bearer) token that can be used for subsequent requests.

The slightly more complex part here is the access token. What does this.generateAccessToken() give back?

JWT Construction

It gives back this thing:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0QXV0aFNlcnZlciIsInN1YiI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MTAyMTIxMjgsIm5iZiI6MTYxMDIxMjEyOCwiZXhwIjoxNjEwMjk4NTI4LCJhdWQiOiJjbGllbnQgYXBpIiwiY3VzdG9tQ2xhaW0iOiJ2YWx1ZSJ9.ZtzGfM6v8JIFwQHACo9rWYN2M9F9sc3i1OJ-0IVZUYk

This is a JWT — a JSON Web Token. It’s a way of transmitting information securely between parties. It’s formed of three parts separated from one another by .

These three parts are the header, the payload and a signature.

header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload: eyJpc3MiOiJ0ZXN0QXV0aFNlcnZlciIsInN1YiI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MTAyMTIxMjgsIm5iZiI6MTYxMDIxMjEyOCwiZXhwIjoxNjEwMjk4NTI4LCJhdWQiOiJjbGllbnQgYXBpIiwiY3VzdG9tQ2xhaW0iOiJ2YWx1ZSJ9
signature: ZtzGfM6v8JIFwQHACo9rWYN2M9F9sc3i1OJ-0IVZUYk

This information is encoded but it is not secret. Each component is base64url encoded and can be decoded without any special information. You can decode the whole token now using a site like https://jwt.io/

Signature not pictured

Those 3 components have different functions. Let’s explore them.

JWT components

Header
This just contains metadata about the token. In this case we specify it’s type (JWT) and the algorithm used to encrypt it (HS256). We’ll see why it matters in a bit.

{
"alg": "HS256",
"typ": "JWT"
}

Payload
The payload has the information that the client cares about. It can see who issued the token (testAuthServer), who the token was issued for (test@test.com). It has time values for when the token was issued, is valid from and when it expires. We also specify the intended audience for the token (client api).

Each of these properties are referred to as “claims” and the ones mentioned so far are reserved. We can add claims beyond the reserved claims though that give additional detail about the user or token. So far we’ve got their email address but we might want to include further identifying information (like name) or even information relating to the user’s access level (like any roles they have or groups they’re part of). In the example jwt we’ve added the claim “customClaim” with a value of “value” because my imagination is a bad imagination.

{
"iss": "testAuthServer",
"sub": "test@test.com",
"iat": 1610212128,
"nbf": 1610212128,
"exp": 1610298528,
"aud": "client api",
"customClaim": "value"
}

One thing you may wonder is “hey, couldn’t I just edit these values to give myself access to groups I’m not part of or to increase expiration time?”. That’s sneaky thinkin’. The thing that stops that from being possible is the signature.

Signature
The signature is formed by following these steps.

  1. Take the header and base64url encode it
  2. Take the payload, and base64url encode that.
  3. Make the following string: `${encodedHeader}.${encodedPayload}`
  4. Encrypt that string
  5. base64url encode the result

The code to do that looks like this:

You can see there that we encrypt with HMAS (Keyed Hashing for Message Authentication — a terrible acronym) which takes in a hashing algorithm, sha256, and a sharedSecret. That shared secret is known only to the Client API and the Auth Server and it’s what ensures a malicious agent can’t just change the details of their jwt.

If you modify your payload details your signature will no longer match. If you try to generate a new signature you won’t be able to without the shared secret. You can actually play with this on jwt.io with the example token. If you have the shared secret (testSecret) and pass it through the site should tell you that your signature is valid. If you then change the payload or header sections of the encoded token (on the left) that message will change as your token no longer matches.

Implementation

In the actual implementation I’m splitting token generation across two classes. First there’s AccessToken. This is passed a header and payload on construction which will remain unchanged throughout its lifespan. The client can subsequently add custom claims to the AccessToken and then, when they’re ready, call on encode to convert it into a ready-to-use JWT string.

Constructing these with the correct header and default payload values can be awkward so I also added an AccessTokenGenerator to push that responsibility onto a dedicated class:

This could be improved with a more robust request object that permits the client to set certain values while pulling others from config. But lazy.

Client API

Token Validation

So let’s assume that we’ve set up the rest of the auth server now. We have two endpoints — /register and /authorize which are hooked up to the services we’ve defined above.

For simplicity let’s say that the frontend knows about the separate auth server and calls it directly. So frontend is able to register a user and get an access token for that user without interacting with the Client API.

The Client API now needs to have some mechanism for verifying that an access token issued by the Auth Server is legitimate. It’ll check this before allowing access to any protected resources.

First up then I’m going to create a new class: ReadOnlyAccessToken. This will be parsed from the encoded string and expose two methods to clients.

getPayload()

Simple:

public getPayload() : any {   return {...this.payload};}

valid()

Bit more complicated. First up we need to check various values on the token to ensure that we’re an appropriate audience, that the issuer is one we recognise, that the token is within a valid timeframe and that the encryption format is one we can handle.

If that all checks out we need to check to ensure that the signature is valid. We do this by creating a new signature in exactly the same we do in the auth server. Take the header, payload and secret. Smush them together to get the expected signature. If the expected signature matches the signature on the token then we can trust that the server that issued it knows our secret key.

The full ReadOnlyAccessToken class is below:

Middleware

With the ReadOnlyAccessToken class in place we need a pretty light middleware that uses it to verify the token:

Here we’re pulling the access token from the headers, making sure the format is correct and then ensuring it’s a valid token.

If it is, then we attach the access token to the response object

res.locals.accessToken = accessToken;

This means that in our controller we’ll be able to get to the access token and pull any claims from its payload. We can see this in action on the following controller:

The protected endpoint includes the auth middleware step. Because we’ve gone through that we can be assured of two things when resolving the protected request:

  1. The user had a valid access token
  2. The access token (and thus its payload) has been attached to the request.

That means on the protected endpoint we’re able to pull the user’s email (without a db hit) from the token and return it in the response.

We can’t get at that information on the unprotected endpoint because there’s no guarantee that the user submitted an access token when hitting it. So when we try to pull the access token there we find it’s undefined. This is true even if the user did submit a valid access token with the request as that token is pulled as part of the auth middleware.

Alternative Options

So that’s a full, albeit simple, auth flow. It’s worth thinking at this point about some things we could have done differently to see where the positive and negatives are for this approach.

No separate auth server

With a single client application I don’t think it would necessarily be a bad idea to keep the auth inside a single app. Doing it this way introduces overhead (in costs if nothing else)… so if you don’t need separated auth then it’s probably good to avoid it.

On the other hand an auth server can be really beneficial if you have multiple applications that need auth. It lets you issue granular permissions to users without requiring credential sharing and prevents apps from duping logic or becoming dependent on one another.

In our sample case though we could have dodged this and kept auth on the Client API.

Store access tokens in the database

That JWT stuff is a lot of work. An alternative approach would have been to:

A) Generate a random string access token
B) Put the string in a database
C) When a user passes through an access token check to see if it’s in the database before allowing the user through

There’s actually some big upsides to this approach:

First it’s simple — if we’d used database access tokens this entire article would be 3 paragraphs long. The access token doesn’t need to hold data, be encrypted or to meet some standard; as long as it’s sufficiently randomised on generation and then stored in the database it’s good. Simplicity is a huge win and as a general rule: if a simple approach is viable for your use case it’s often the best choice even if it isn’t optimal in terms of time or memory complexity.

Second, it gives you more security options — you’re able to easily revoke access tokens with this set up which isn’t true with the simple JWT approach outlined throughout the rest of the article.

Thought experiment: You’re the dev in charge of an application that uses JWT access tokens. A malicious agent, Mr Grumps, manages to get their hands on one of these tokens —now he can access one of your user’s data! How do you revoke the token to block Mr Grump’s access to your API? Well, the API verifies the token by looking at key payload data (issuer, expiration) and then by verifying the signature. Your only two options then are to wait for the token to expire (which in our system would take a day), or to change the secret key so that all currently issued tokens fail validation checks (which would effectively invalidate every users token, not just Mr Grump’s).

“If we’d gone with db access tokens instead then things would have been so much easier!” thought-experiment-You would probably lament. “I’d just need to remove that access token from the database and that devilish Mr Grumps would have been thwarted”.

Our simple JWT-style server probably doesn’t cut the mustard yet for this exact reason. We’ll address that in a future article.

The big downside though to db access tokens is that you need to hit the db for every protected call. That’s potentially some pretty heavy overhead in terms of database impact and response time.. but arguably manageable depending on the scale of your app (particularly if your client API is handling its own auth).

Wrap up

I’m gonna call it there for this article. I’ll do a follow up soon that covers how we might expand our auth server to deal with the revocation issue and to let us use the auth across multiple applications.

Thanks for reading!

References

(plus other pages on that site)

(annnd other pages on that site)

--

--