Implementing a JWT auth system with TypeScript and Node

If you've hung out on the web development subreddits or Hacker News, you've probably heard of JSON Web Tokens. Whether you're using them in production right now, or you've only seen the words "JWT" and haven't had a chance to explore it further, this newfangled authentication system is skyrocketing in popularity thanks to its simplicity and ease of use.

JSON Web Tokens, commonly abbreviated JWT, are a method for storing a user's session data in a hashed string and using it for authentication. The token is signed with a specific algorithm and a secret key that you control, so you're always able to verify that the token a client has sent is indeed one that your application issued. The only way it can be spoofed is for the attacker to get a hold of your secret signing key.

These tokens are nifty for several reasons, but for me personally, I think the niftiest thing about them is that they can be easily parsed and ready by anybody without the signing key. You read that correctly, anybody can copy a JWT and parse it without the signing key -- the values are not encrypted, just hashed.

You can try this for yourself! Just enter the following JSON Web Token string at https://jwt.io and set the algorithm to HS512:

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJoZWxsbyI6IndvcmxkIiwibWVzc2FnZSI6IlRoYW5rcyBmb3IgdmlzaXRpbmcgbm96emxlZ2Vhci5jb20hIiwiaXNzdWVkIjoxNTU3MjU4ODc3NTI2fQ.NXd7lC3rFLiNHXwefUu3OQ-R203pGfB87-dIrk2S-vqfaygIWFwZKzmGHr6pzYkl2a0HkY0fdwa38yLWu8Zdhg

You'll see the little message I left for you, but you'll also see a big red "Invalid Signature" warning in the bottom left.

Parsed JWT with invalid signature

The token was parsed and the payload is plainly there to see, but you still don't know the signature that I used to sign the token. If I have a web server set up to read these JSON Web Tokens, I'm going to be checking that the signature can be verified before I trust the token. So while you may be able to create your own JWT and send it to my server, my server isn't going to trust it.

You can see it in action using that same token string at https://jwt.io, but this time enter foo in the signature field at the bottom right. That's the signature I used to sign the token.

Parsed JWT with valid signature

So why do I think it's nifty that you can parse a JSON Web Token without knowing the signature? Because this means that your client application (e.g. the frontend in your web app) can parse and use the session data in the token without making a specific API call to get that data. It's not mindblowing, but in my experience it's a "nice to have" that you don't get with many (any) other authentication schemes.

That's not the only thing that has made JWTs so popular though. The other big reason is that they can massively cut down on the number of database requests your application needs to do just to authenticate a user. If you have this token that contains identifying information about the user (such as their user id, or maybe username), and you only need to check the signature to validate that it's a real token and hasn't been tampered with, you suddenly don't need to make a call to your database to check things like usernames, passwords, session cookies and so on.

Assuming the client sent the token along with the request in a header, cookie or even the body, you've got all the information you need right there without contacting the database at all.

Because a JWT can be easily parsed without the secret signing key, it's super important that sensitive information is not stored in the token. In my own applications, I make sure that that only basic user information gets placed in the token -- user id or username, the user's Shopify shop name or domain (if I'm building a Shopify web app), and two timestamps: one indicating when the token was issued, and another indicating when it should expire.

If I were to store a user's Shopify access token in the JWT, any attacker (or anyone curious enough to parse the token themselves) would see that extremely sensitive token. Therefore, if any route needs to access some sensitive piece of user data, it still gets pulled directly from the database. I can easily look it up by pulling the user id or username from the token, but you simply can't place sensitive data in the JWT without risking it being stolen.

Implementing a simple and easy JWT authentication system with TypeScript and Node

Because of those advantages, almost every authentication system that I implement these days uses JSON Web Tokens. I personally feel like the benefits that come with it easily outweight any disadvantages as long as you're aware of the pitfalls (which I'll cover). Throughout the rest of this article, I'll walk you through my own personal implementation of a JWT auth system for Node servers/endpoints, including some "nice to haves" like expiring tokens and automatic renewal "grace" periods.

For this article I'm going to assume you're using Express as your server framework, but everything within should be applicable to any other Node framework.

To get started, we'll need to install a package for serializing/deserializing JSON Web Tokens. I've been using the jwt-simple package for a long time, so it's usually my go-to for this scenario. It doesn't have some of the fancier stuff that other JWT packages come with, but it does everything I need, and it's usable in both Node and the browser.

You can install it with NPM or Yarn, and since I'm using TypeScript, I also install the typings:

npm i --save jwt-simple && npm i --save-dev @types/jwt-simple
# Or use yarn
yarn add jwt-simple && yarn add --dev @types/jwt-simple

Next, let's set up some TypeScript interfaces and types to represent the following things:

  1. A User object, which contains some sensitive information -- in this case, a password.
  2. A Session object, which is a stripped down version of the User object sans sensitive information. It will also have two timestamps, one indicating when the token was issued and one indicating when it expired. This Session object is what gets serialized into a JSON Web Token.
  3. A PartialSession object, which is identical to the Session object but drops the issued and expiration timestamps. This is solely used when encoding a session, since the encode function will manage those timestamps itself.
  4. An interface to represent the result of encoding a session to a JWT.
  5. A union type to represent the result of parsing a JWT, which can fail due to tampering or mismatched algorithms.
  6. A union type to represent a token's expiration status: valid, expired, or "grace" where it can be automatically renewed by the server.
export interface User {
    id: number;
    dateCreated: number;
    username: string;
    password: string;
}

export interface Session {
    id: number;
    dateCreated: number;
    username: string;
    /**
     * Timestamp indicating when the session was created, in Unix milliseconds.
     */
    issued: number;
    /**
     * Timestamp indicating when the session should expire, in Unix milliseconds.
     */ 
    expires: number;
}

/**
 * Identical to the Session type, but without the `issued` and `expires` properties.
 */
export type PartialSession = Omit<Session, "issued" | "expires">;

export interface EncodeResult {
    token: string,
    expires: number,
    issued: number
}

export type DecodeResult =
    | {
          type: "valid";
          session: Session;
      }
    | {
          type: "integrity-error";
      }
    | {
          type: "invalid-token";
      };

export type ExpirationStatus = "expired" | "active" | "grace";

Creating a JSON Web Token

Probably the most important function for implementing JSON Web Tokens is the function to actually create the token! You'll need to decide how long you want your tokens to last before they expire. In my own applications, I typically use a short 15 minute expiration time, with a three hour automatic renewal period (which we'll get into below).

When we encode the session we're going to use one specific algorithm for signing the token, and that algorithm should never be changed. This is going to become very important when deserializing the token.

import { encode, TAlgorithm } from "jwt-simple";

export function encodeSession(secretKey: string, partialSession: PartialSession): EncodeResult {
    // Always use HS512 to sign the token
    const algorithm: TAlgorithm = "HS512";
    // Determine when the token should expire
    const issued = Date.now();
    const fifteenMinutesInMs = 15 * 60 * 1000;
    const expires = issued + fifteenMinutesInMs;
    const session: Session = {
        ...partialSession,
        issued: issued,
        expires: expires
    };

    return {
        token: encode(session, secretKey, algorithm),
        issued: issued,
        expires: expires
    };
}

One thing that I should mention: if you're the kind of person who likes to read a spec document and you've read the spec for JSON Web Tokens, you'll note that the spec already includes two specific properties for tracking the issued and expiration dates: iat and exp. There are two reasons I'm not following the spec to the letter here:

  1. The iat and exp timestamps should be in Unix seconds, whereas they're being stored in Unix milliseconds here (which lets us easily construct a new Date instance in JavaScript). This isn't that big of a deal, to work around it all we would need to do is multiply the timestamp by 1000 to get milliseconds.
  2. The jwt-simple package will try to look for the exp value when deserializing, and it will throw an error if the token is expired. Since I prefer to have a short automatic renewal period, I'd rather have the JWT package just decode the token and let me check for expiration myself.

Neither of these are huge issues, and you can certainly follow the spec by using iat and exp rather than issued and expires. This is simply a matter of my own preference.

Deserializing a JSON Web Token

If we can serialize a Session object into a JSON Web Token, then it follows that we'll obviously need to deserialize a token into a Session object. The deserializing function will take the token string -- which should be sent along with any authenticated request a client is trying to make -- and then wrap the jwt-simple package's decode function to catch certain errors. We'll turn these errors into one of the DecodeResult types based off of the error's message (which you can find in the package's source code on GitHub).

Remember, it is extremely important that you pin the algorithm when encoding and decoding. One of the weakest points of the JWT spec is that tokens can try to dictate which algorithm should be used to decode them, and many JWT packages will blindly allow that. You should never, ever trust what the token says -- hardcode the algorithm you're using and don't deviate from it.

import { Decode, TAlgorithm } from "jwt-simple";

export function decodeSession(secretKey: string, tokenString: string): DecodeResult {
    // Always use HS512 to decode the token
    const algorithm: TAlgorithm = "HS512";

    let result: Session;

    try {
        result = decode(sessionToken, secretKey, false, algorithm);
    } catch (_e) {
        const e: Error = _e;

        // These error strings can be found here:
        // https://github.com/hokaccha/node-jwt-simple/blob/c58bfe5e5bb049015fcd55be5fc1b2d5c652dbcd/lib/jwt.js
        if (e.message === "No token supplied" || e.message === "Not enough or too many segments") {
            return {
                type: "invalid-token"
            };
        }

        if (e.message === "Signature verification failed" || e.message === "Algorithm not supported") {
            return {
                type: "integrity-error"
            };
        }

        // Handle json parse errors, thrown when the payload is nonsense
        if (e.message.indexOf("Unexpected token") === 0) {
            return {
                type: "invalid-token"
            };
        }

        throw e;
    }

    return {
        type: "valid",
        session: result
    }
}

Notably, this function is not checking the expiration value at all. Its sole responsibility is to deserialize the token to an object, while also verifying that it hasn't been tampered with. Checking the expiration will be the responsibility of the next function.

Checking the expiration status and handling with the automatic renewal period

While an automatic renewal period is not something you need to implement in every JWT system, I do consider it a "nice to have" sort of thing. In this scenario, the server looks at a JSON Web Token's expiration date and says "if you've got a valid token and it's no older than X minutes/hours/days, I'll trust you and automatically renew it". This lets the client continue its request without interruption, as long as the token isn't older than an arbitrary length of time.

Again, this isn't a requirement by any means, it's perfectly valid to just deny an expired token and force the client to make a separate request to manually renew it. The client will need to do this anyway if the token is too old.

Let's write a function for checking the expiration status of a token. It's going to receive a deserialized session object, look at its expires property, and determine if the token is active, expired, or in the grace renewal period. For this example, we'll say the renewal period is three hours; if has been expired for less than three hours, it can be automatically renewed.

export function checkExpirationStatus(token: Session): ExpirationStatus {
    const now = Date.now();
    
    if (token.expires > now) return "active";

    // Find the timestamp for the end of the token's grace period
    const threeHoursInMs = 3 * 60 * 60 * 1000;
    const threeHoursAfterExpiration = token.expires + threeHoursInMs;

    if (threeHoursAfterExpiration > now) return "grace";

    return "expired";
}

This function will be used by the custom auth middleware we'll write in a moment.

Authorization middleware

Okay, so let's recap quickly: we now have three functions, one to serialize a session object into a JSON Web Token; one to deserialize a JSON Web Token into a session object; and one to check if that deserialized session has expired. Let's put it all together and write an Express middleware function, which will let you guard your express routes and require a valid JWT before the route can be accessed.

We need the middleware to do five things:

  1. It should first check that the request has an X-JWT-Token header. The name of the header is arbitrary, you can set it to whatever you want, as long as the client making the request includes the header.
  2. It should check that the token found in the header is valid.
  3. It should check that the token has not yet expired. If the token is in the automatic renewal period, it should renew it and append it to the response headers as X-Renewed-JWT-Token. Again, the name of the header is arbitrary as long as your client is looking for it in the response.
  4. If any of the above requirements are not met, the middleware should end the request and return a 401 Unauthorized result.
  5. If all of the above requirements are met, the middleware should append the session object to Express' response.locals object, where the authenticated route can access it.
import { Request, Response, NextFunction } from "express";

/**
 * Express middleware, checks for a valid JSON Web Token and returns 401 Unauthorized if one isn't found.
 */
export function requireJwtMiddleware(request: Request, response: Response, next: NextFunction) {
    const unauthorized = (message: string) => response.status(401).json({
        ok: false,
        status: 401,
        message: message
    });

    const requestHeader = "X-JWT-Token";
    const responseHeader = "X-Renewed-JWT-Token";
    const header = request.header(requestHeader);
    
    if (!header) {
        unauthorized(`Required ${requestHeader} header not found.`);
        return;
    }

    const decodedSession: DecodeResult = decodeSession(SECRET_KEY_HERE, header);
    
    if (decodedSession.type === "integrity-error" || decodedSession.type === "invalid-token") {
        unauthorized(`Failed to decode or validate authorization token. Reason: ${decodedSession.type}.`);
        return;
    }

    const expiration: ExpirationStatus = checkExpiration(decodedSession.session);

    if (expiration === "expired") {
        unauthorized(`Authorization token has expired. Please create a new authorization token.`);
        return;
    }

    let session: Session;

    if (expiration === "grace") {
        // Automatically renew the session and send it back with the response
        const { token, expires, issued } = encodeSession(SECRET_KEY_HERE, decodedSession.session);
        session = {
            ...decodedSession.session,
            expires: expires,
            issued: issued
        };

        res.setHeader(responseHeader, token);
    } else {
        session = decodedSession.session;
    }

    // Set the session on response.locals object for routes to access
    response.locals = {
        ...response.locals,
        session: session
    };

    // Request has a valid or renewed session. Call next to continue to the authenticated route handler
    next();
}

Adding the custom JWT middleware to your Express application

Once you've got the authMiddleware function written, you can add it to your Express app (or use any framework that's compatible with Express), to guard your routes. Below is a basic example showing how you can set up two routes: one at /sessions to create a JWT token (for logging in), and one at /protected which is guarded by the middleware we wrote above:

import Express from "express";
import { Request, Response } from "express";
import { authMiddleware } from "./path/to/middleware";
import { encodeSession } from "./path/to/encodeSession";

const app = Express();

// Set up middleware to protect the /protected route. This must come before routes.
app.use("/protected", authMiddleware);
// If you want to protect _all_ routes instead of just /protected, uncomment the next line
// app.use(authMiddleware);

// Set up an HTTP Post listener at /sessions, which will "log in" the caller and return a JWT 
app.post("/sessions", (req: Request, res: Response) => {
    // This route is unprotected, anybody can call it
    // TODO: Validate username/password
    const session = encodeSession(SECRET_KEY_HERE, {
        id: userId,
        username: "some user",
        dateCreated: timestamp        
    });
    
    res.status(201).json(session);
});

// Set up an HTTP Get listener at /protected. The request can only access it if they have a valid JWT token
app.get("/protected", (req: Request, res: Response) => {
    // The auth middleware protects this route and sets res.locals.session which can be accessed here
    const session: Session = res.locals.session;

    res.status(200).json({ message: `Your username is ${session.username}` });
});

And that's all it takes to protect your Express routes with custom JWT middleware. If you've got any questions about this guide, don't hesitate to shoot me an email!


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.