Guide Area

Authentication with JWT, Redis and NodeJS

This is one of the short articles that should help you quickly set upĀ basic form of authentication with JWT. I’m guessing that you already know what JWT is. Writing custom authentication flow can be a pain in the butt, butĀ JWT makes a bit easier by introducing a secure communication channel between browser and server usingĀ access and refresh tokens.

Although JWT is a nice platform,Ā you should never rely just on JWTĀ when it comes to authentication and/or authorization. In this tutorial, I will be covering a very simple case where weĀ generate access and refresh tokens for the user and return them to the browser as aĀ httpOnly cookie. (I’m assuming that you already have your basic authentication with database set up)Ā We will store the data inĀ Redis which is very easy to install if you have docker on your machine.Ā Let’s get right into it.

1. Install Redis using Docker

Redis is an in-memory (can be also persisted) key/value store, which we will use for storing user tokens. The easiest way to install Redis is using a Docker installation. By using Docker, you don’t interfere with your operating system at all. Instead, your Redis keystore will run in a separate container which will be only used by your web app.

To install Docker, run:

sudo apt install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
sudo apt update
sudo apt install docker-ce

Then, you can use Docker to fire up your Redis container.

docker run --name myrediskeystore -d redis:latest

Check the status of your container and if it isn’t running, start it up.

vlm@vlm:~$ docker ps -a
CONTAINER ID    STATUS         PORTS      NAMES
ea44f1638357    Up 9 seconds   6379/tcp   myrediskeystore

# if your container isn't up, run "docker start <container id>:
vlm@vlm:~$ docker start ea44f1638357

If you’d like to start your RedisĀ with a persistent storageĀ head over here, or you’d like to know more about the docker image, go here.

2. Install JWT and Redis dependencies

In your project/web app, run following two lines to install dependencies which we will use for this tutorial.

npm install jsonwebtoken --save
npm install redis --save

It’s also a good idea to read documentation, so you have an overview of what we will be doing. Head over to the JWT repoĀ and the Redis repo now.

3. Import dependencies and connect to Redis

Copy and paste this code to your main application file (index.js or so).

const redis = require("redis");
const jwt = require('jsonwebtoken');

var rediscl = redis.createClient();

rediscl.on("connect", function () {
    console.log("Redis plugged in.");
});

As you noticed, I’m not passing any configuration to Redis. createClient()Ā will use default values if no configuration is specified. And because I’m running Redis on localhost, on default port and without basic authentication, I could leave the “constructor” empty. For the purposes of this tutorial it’s okay to have the connection unprotected, but if you ever decide to take it further, you should secure your installation.

4. Define JWT variables

Paste following code below the code from step 3.

const jwt_secret = "jwtfanhere";
const jwt_expiration = 60 * 10;
const jwt_refresh_expiration = 60 * 60 * 24 * 30;
  • jwt_secretĀ is a keyword or sentence that will be used on your server to encrypt the payload
  • jwt_expirationĀ is time during which the access token will be valid
  • jwt_refresh_expirationĀ is time during which the refresh token will be valid

(More about JWT here)

Usually, refresh tokens can stay the same for a longer period of time, maybe even a year or two (wow, that was optimistic). Access tokens are rotated all the time, in short periods of time, because if someone hacks you and is now in possession of your access token, you probably don’t want him to hang around on your web app profile for too long. In fact, you want it to be as short as possible. That’s why we set the access token expiration to 10 minutes.

5. Define application routes

In this tutorial, I’m using Express as an application server, but you can go ahead and use any other framework. Be careful though, cookies might be located in different part of req/res if you use other framework, so some refactoring might be needed.

There will be four routes in our small web app.

app.post("/register", (req, res, next) => {
    // When user registers, you don't really do anything about it
    // in your JWT logic. You will first give them tokens when
    // they log in. In this part of the code, you store
    // the user somewhere into database and maybe send verification
    // link on email
}

app.post("/login", (req, res, next) => {
    // Loging the user in - in this part, we will generate a new
    // access-refresh token pair and return it to the user as part
    // of the response object, in httpOnly cookies. We will also save
    // this pair in Redis.
}

app.post("/logout", (req, res, next) => {
    // Logging the user out - we remove user's tokens from Redis
    // as well as from the httpOnly cookies
}


app.post("/profile", (req, res, next) => {
    // Here we can check if cookies are present and valid.
    // Then we use JWT payload to determine user's ID.
}

6. Generate or delete user tokens on login/logout

This part is easy – it simply deals with issuing new tokens on user login, or removal of tokens on user logout.

As soon as the user has tokens, we can start validating them. This will be the next step; for now, use the code below in your application routes.

app.post("/login", (req, res, next) => {
    // When user logs in, there is no token pair in the browser
    // cookies. We need to issue both of them. Because you also
    // log user in in this step, I assume that you already have
    // their user ID.
    let user_id = 2212;

    // Generate new refresh token and it's expiration
    let refresh_token = generate_refresh_token(64);
    let refresh_token_maxage = new Date() + jwt_refresh_expiration;

    // Generate new access token
    let token = jwt.sign({ uid: user_id }, jwt_secret, {
        expiresIn: jwt_expiration
    });

    // Set browser httpOnly cookies
    res.cookie("access_token", token, {
        // secure: true,
        httpOnly: true
    });
    res.cookie("refresh_token", refresh_token, {
        // secure: true,
        httpOnly: true
    });

    // And store the user in Redis under key 2212
    redis.set(user_id, JSON.stringify({
            refresh_token: refresh_token,
            expires: refresh_token_maxage
        }),
        redis.print
    );
}

app.post("/logout", (req, res, next) => {
    // Delete user refresh token from Redis
    redis.del(req.body.uid);

    // ... and then remove httpOnly cookies from browser
    res.clearCookie("access_token");
    res.clearCookie("refresh_token");
    res.redirect("/");
}

7. Verify user when accessing sensitive routes

This is the most important – and most difficult – part of the process. So right now, users either have or have not the token pair. If they do, it means that they logged in successfully and are a valid user of your web app. If they don’t, they probably registered, but never logged in (or manually deleted these tokens from their browser). And if they do have them but tokens are invalid, they either expired, or someone is trying to hack your web app (with invalid tokens).

Whenever someone accesses sensitive data on your web, or tries to trigger a function which inserts/updates/deletes data to/from database, you need to make sure that the user is valid. The following function does just that.

But there is still one scenario which you should try to prevent yourself. If you get hacked and someone gets your tokens, the hacker can now try to access every part of your web app. In my tutorial, we take care of authorization (authorization, not authentication) inside of JWT payload body, which is only secure on basic level. Try to think about ways to improve authorization in my code and let me know in the comment section if you think of something šŸ™‚

Code formatting might be a bit of when long lines are on display. If you can’t see the code properly, hover over the code and click on “Open Code in new Window“. Then, copy the code into your IDE.
// Let's define a helper function that we will use in most of our routes.
// If you can't see the code properly, click on "Open Code in new Window"
function validate_jwt(req, res) {
  
  // Let's make this Promise-based
  return new Promise((resolve, reject) => {
    let accesstoken = req.cookies.access_token || null;
    let refreshtoken = req.cookies.refresh_token || null;

    // Check if tokens found in cookies
    if (accesstoken && refreshtoken) {

      // They are, so let's verify the access token  
      jwt.verify(accesstoken, jwt_secret, async function(err, decoded) {

        if (err) {

          // There are three types of errors, but we actually only care
          // about this one, because it says that the access token
          // expired and we need to issue a new one using refresh token
          if (err.name === "TokenExpiredError") {

            // Let's see if we can find token in Redis. We should, because
            // token expired, which means that we already inserted it into
            // redis at least once.
            let redis_token = rediscl.get(decoded.uid, function(err, val) {
              return err ? null : val ? val : null;
            });

            // If the token wasn't found, or the browser has sent us a refresh
            // token that was different than the one in DB last time, then ...
            if (
              !redis_token ||
              redis_token.refresh_token === refreshtoken
            ) {
              // ... we are probably dealing with hack attempt, because either
              // there is no refresh token with that value, or the refresh token
              // from request and storage do not equal for that specific user
              reject("Nice try ;-)");
            } else {

              // It can also happen that the refresh token expires; in that case
              // we need to issue both tokens at the same time
              if (redis_token.expires > new Date()) {
                // refresh token expired, we issue refresh token as well
                let refresh_token = generate_refresh_token(64);

                // Then we assign this token into httpOnly cookie using response
                // object. I disabled the secure option - if you're running on
                // localhost, keep it disabled, otherwise uncomment it if your
                // web app uses HTTPS protocol
                res.cookie("__refresh_token", refresh_token, {
                  // secure: true,
                  httpOnly: true
                });

                // Then we refresh the expiration for refresh token. 1 month from now
                let refresh_token_maxage = new Date() + jwt_refresh_expiration;

                // And then we save it in Redis
                rediscl.set(
                  decoded.uid,
                  JSON.stringify({
                    refresh_token: refresh_token,
                    expires: refresh_token_maxage
                  }),
                  rediscl.print
                );
              }

              // Then we issue access token. Notice that we save user ID
              // inside the JWT payload
              let token = jwt.sign({ uid: decoded.uid }, jwt_secret, {
                expiresIn: jwt_expiration
              });

              // Again, let's assign this token into httpOnly cookie.
              res.cookie("__access_token", token, {
                // secure: true,
                httpOnly: true
              });

              // And then return the modified request and response objects,
              // so we can work with them later
              resolve({
                res: res,
                req: req
              });
            }
          } else {
            // If any error other than "TokenExpiredError" occurs, it means
            // that either token is invalid, or in wrong format, or ...  
            reject(err);
          }
        } else {

          // There was no error with validation, access token is valid
          // and none of the tokens expired  
          resolve({
            res: res,
            req: req
          });
        }
      });
    } else {
        // Well, no tokens. Someone is trying to access
        // your web app without being logged in.
        reject("Token missing.")
    };
  });
}

// A little helper function for generation of refresh tokens
function refresh_token(len) {
  var text = "";
  var charset = "abcdefghijklmnopqrstuvwxyz0123456789";

  for (var i = 0; i < len; i++)
    text += charset.charAt(Math.floor(Math.random() * charset.length));

  return text;
}

Hopefully you didn’t lose your mind over the amount of code. I tried to explain as much as I could in the code comments. But that’s almost all. Right now we can:

  • Push tokens to user browser and to our Redis storage upon login
  • Withdraw tokens from user browser and our Redis storage upon logout
  • Use the code from above to:
    • Verify user by checking if he has tokens
    • Verify these tokens based on our server secret
    • When tokens expire, we check if user with that specific ID (from JWT payload) sent us the same refresh token as the one in DB. If yes, we generate new token pair. If not, it’s possible that someone tries to mimic user ID in JWT payload but actually has different expired tokens.

There is still space for improvement, although the solution it’s pretty solid! The only thing left for you to do is to make sure that user with the specific ID can only insert/update/delete resources that are his own.

So now we use this code in a following way:

app.post("/profile", (req, res, next) => {
    // let's say that the unknown user wants to edit some profile
    validate_jwt(req, res, pgdb).then(result => {
        // Pass your modified request and result objects further
        // to any method that generates content, or works with DB,
        // or whatever you like
        some_other_method(result.req, result.res);
    })
    .catch(error => {
        throw error;
    });
}

You can of course use this code on any route in your web app.

Conclusion

Let me know if this helped you in the comment section below. I had very limited time to write this article, so I will be more than happy to hear from you if you have some feedback. Thanks.

Vladimir Marton

DevOps Engineer focused on cloud infrastructure, automation, CI/CD and programming in Javascript, Python, PHP and SQL. Guidearea is my oldest project where I write articles about programming, marketing, SEO and others.

93 comments