learn hapi — JWT “Immediate” Logout (Part 2/2)

The first tutorial on JWT logout shows you a basic logout when using JSON web token (JWT). The downside of this logout is that users can authenticate requests for the remaining JWT lifetime. Only the refresh token is revoked.

In contrast to the first tutorial, you’ll now implement a JWT logout that invalidates both tokens, the JWT itself and the refresh token, immediately. A user must re-login to interact with API endpoints that require authentication.

hapi Series Overview

Overview

What’s missing in the previous tutorial on JWT logout is a “hard logout”. You’re accepting the trade-off that users keep their valid JWT until its expiration. In contrast, you’ll now implement this “hard logout”.

This tutorial will use blacklists to invalidate JWTs. The linked tutorial walks you through details on how to implement token blacklisting in your application.

Your tasks:

  • implement a “hard logout”: revoke a user’s refresh token and JWT
  • create a new logout handler or update the existing one to invalidate a JWT and refresh token
  • test the immediate logout implementation

“Hard Logout”: Invalidate JWT and Refresh Token

In contrast to just revoking the refresh token, you may want to cut a user’s access to your application immediately. In these cases, you need to revoke the JWT itself and the related refresh token.

This option has the following advantages:
- revoke access to your app immediately

The downsides of this approach:
- additional handling needed to ensure the request includes the refresh token and JWT to revoke both - JWT blacklisting needed - a more complex implementation

Install “hapi-auth-multiple-strategies” Package

When revoking the JWT and refresh token you want to ensure that an incoming request contains both tokens. Your jwt and refresh authentication strategies handle both cases independently. You need to combine them to ensure a request contains the two tokens.

Because hapi doesn’t implement the feature to require multiple authentication strategies natively, you need to implement it yourself. We at Future Studio created a hapi plugin to require multiple authentication strategies.

This plugin comes handy for the “hard logout”. Install the package as a project dependency from NPM:

npm i hapi-auth-multiple-strategies  

Create an Authentication Strategy

Register the hapi-auth-multiple-strategies plugin to your hapi server by adding it to the list of API authentication dependencies.

Then go ahead and create a new authentication strategy that requires the two existing strategies: jwt and refresh. Have a look at the hapi-auth-multiple-strategies documentation if you want more details on how this plugin works. The following code snippet illustrates how to use it:

api/authentication/index.js

async function register (server) {  
  await server.register([
    …
+   {
+     plugin: require('hapi-auth-multiple-strategies')
+   }
  ])

  server.auth.strategy('jwt', 'jwt', { … })
  server.auth.strategy('refresh', 'bearer-token', { … })

+ server.auth.strategy('jwt-and-refresh-token', 'multiple-strategies', {
+   strategies: ['jwt', 'refresh']
+ })
}

That’s it! The new strategy jwt-and-refresh-token is usable in your application and requires requests to authenticate against the jwt and refresh strategies.

Create a “Logout Immediately” Route

Create a new route for the “hard logout”. If you already know that your application needs this functionality instead of the “soft logout”, you may update the existing logout handler.

api/users/routes.js

{
  method: 'GET',
  path: '/logout-immediately',
  config: Handler.logoutImmediately
}

Create the “Logout Immediately” Route Handler

The next step is to implement the related route handler to log out a user. The route handler must blacklist the JWT and invalidate the refresh token.

JWT blacklisting requires the decoded JWT payload to access the JWT identifier. The decoded payload is available through the request.user shortcut. At this point, we store the JWT claims and user data in the same object which makes the JWT claims available in the authenticated credentials:

api/users/handler.js

logoutImmediately: {  
  auth: 'jwt-and-refresh-token',
  handler: async (request, h) => {
    await JWT.setBlacklisted(request.user)

    const { token } = await RefreshToken.findActive(request.query.refreshToken)
    await RefreshToken.invalidate(token)

    return h.response().code(204)
  }
}

Ensure that the route handler revokes the tokens. Especially the JWT blacklisting is important to cut access right in this moment.

Fixing the Issues

All the functionality is in place and it seems like testing is the next step. Please go ahead and start your Futureflix server.

While sending a request to the new /logout-immediately endpoint, you’ll notice a 401 Unauthorized error “Refresh token invalid”. Using the same refresh token to request a new JWT works fine. Huh, why’s that?

The Problem

This is a “feature” in the used hapi-auth-bearer-token library. It always checks the Authorization header to contain a value and as the second step checks other parameters.

The problem occurs because a request contains the JWT in the Authorization HTTP header and the refresh token as a query parameter. Precisely, the refresh authentication strategy will find the JWT, ignore the search for a refresh token in the query parameters, and then fail because the validation function in the refresh strategy uses the JWT instead of the refresh token.

The Solutions

You can go different ways to fix this problem:

  • create your own authentication scheme for (refresh) token authentication
  • use another library to authenticate requests with a given token

Both ideas have their advantages: creating your own authentication scheme gives you full control over the functionality and what happens when authenticating requests with (refresh) tokens. Using another library will save you implementation time.

This tutorial replaces the underlying library to authenticate refresh tokens. We’ll implement a custom authentication scheme in another tutorial.

Replacing the NPM Dependencies

While searching for solutions to this problem, another hapi plugin came up: @now-ims/hapi-now-auth. The hapi-now-auth plugin is inspired by
hapi-auth-bearer-token and has a slightly different approach to authenticate requests: it checks the request for the defined accessTokenName instead of always checking the Authorized HTTP header.

This solves our problem because the refresh token strategy will now look for the configured values.

Go ahead and install the dependency to your project:

npm i @now-ims/hapi-now-auth  

{{placeholder-npm-shortcuts}}

Refactor the Refresh Token Authentication Strategy

Update the refresh authentication strategy to use the newly installed package. Replace the hapi-auth-bearer-token with the new @now-ims/hapi-now-auth dependency. The other thing to change is the scheme name from hapi-now-auth to hapi-auth-bearer-token. Both schemes provide the same API which makes the migration straightforward. All options stay the same and will work as expected:

api/authentication/index.js

async function register (server) {  
  await server.register([
    …,
    {
+     plugin: require('@now-ims/hapi-now-auth')
    }
  ])

+ server.auth.strategy('refresh', 'hapi-now-auth', {
    allowQueryToken: true,
    accessTokenName: 'refreshToken',
    validate: async (_, token) => { … }
  })
}

That’s it! This change will make your code work and you can start testing!

Result

Test your implementation by starting your Futureflix server and the Postman app. If you want to know more about API testing with Postman, check out these 10 tasty tips on API testing with Postman 🚀

As the first thing to do: log in to fetch a valid JWT and refresh token. If you have a stored refresh token, you can also use it to renew the JWT:

Log in to retrieve a new pair of JWT and refresh token

The next step is sending the logout request to immediately invalidate both, the JWT and refresh token. The expected response has an empty response payload and the 204 HTTP status code:

Send a logout request including the JWT and refresh token

Great, the response to the logout request is the expected one!

Now verify whether the JWT is on the blacklist and invalid. The request to /me which would fetch your personal data should fail with an unauthorized error. The error message should show a blacklisted JWT:

Bild-Beschreibung

Yeah, it works! The blacklisted JWT can’t fetch data from API endpoints that require authentication.

The immediate logout should also revoke the refresh token. Check if you can fetch a new JWT using the refresh token. This request should also fail with an unauthorized error:

Bild-Beschreibung

Awesome, you’re receiving the expected error. The code works as expect!

Your Tasks

Before moving on to the next tutorial, please make sure you can check off the following tasks:

  • [ ✓ ] implement a “hard logout”: revoke a user’s refresh token and JWT
  • [ ✓ ] create a new logout handler or update the existing one to invalidate a JWT and refresh token
  • [ ✓ ] test the immediate logout implementation

Proceed to the next lesson once you’ve completed all the tasks.

Next Lesson

Implementing the functionality from this tutorial adds an immediate JWT logout to your application. Even though you accepted the JWT lifetime in the first place, the added functionality on server-side allows you to invalidate a pair of tokens for a given user.

In a previous tutorial, we added the functionality to invalidate a refresh token in the refresh authentication strategy. In this tutorial, we added the same functionality to the route handler. The route handler also adds the JWT to the blacklist.

In the next tutorial, you’ll refactor the existing code base and move the token revocation to a better place: a lifecycle extension point. This will clean up your code and not spread the revocations across different files.


Mentioned Resources

Explore the Library

Find interesting tutorials and solutions for your problems.