loading...

Custom Authentication Strategy in Fastify using decorators, lifecycle hooks and fastify-auth

lek890 profile image Lekshmi Chandra ・3 min read

To authenticate every incoming request before processing, Fastify lifecycle events and decorators can be used. If you want to have multiple authentication logics fastify-auth can be used.

For example, I have incoming requests which bring http cookie with a jwt token in it for authentication. Before processing each requests, I need to verify whether the jwt token is valid.

For that, I am going to use a decorator called verifyJWT, which is a custom function which has my logic in it.

My verifyJWT will

  1. check for jwt expiry
  2. then decode the jwt using my secret key and get the userId saved in it
  3. if verification passes, continues execution and passes the userId to the handler function.
  4. if verification failed, return unauthorized status to the client

Motivation is I don't want to write this verification logic in every route handler and prefer this happens as a hook before every request reaches the handler.

For that I am using preValidation lifecycle hook from fastify.

I will abstract away the verification logic as a decorator function in my fastify instance. So that, I can simply call

fastify.verifyJWT() 

Needed package:
fastify-auth - a fastify plugin which supports multiple authentication strategies at the same time

Step 1: Configuration

We can register the fastify-auth plugin and the decorators we wrote to the fastify instance.

//index.ts

import decorators from "./decorators";
import * as fastifyAuth from "fastify-auth";


const fastifyApp = fastify(fastifyConfig)
                   .register(decorators)
                   .register(fastifyAuth);

Step 2: Write some decorators

Writing decorators will help us attach custom functions to the instance and we can call them elegantly like


fastify.verifyJWT() //verifyJWT is the name of the decorator function

//decorators.ts

import * as fastifyPlugin from "fastify-plugin";

export default fastifyPlugin( (fastify,options, next) => {
  fastify.decorate("verifyJWT", function(req: any, rpl: any, done: any) {
    const cookie = req.cookies[COOKIE_NAME];

    const verificationCallback = ({ userId, err }: TokenDecoded) => {
      if (userId) {
        //pass this to the handler function so that it can use it to 
        //identify the user and process his data
        req.params.userId = userId;
        return done();
      }
      done(err);
    };
    //verifyToken gives userId in case of successful decoding
    //gives err msg in case of error 
    verifyToken(cookie, verificationCallback);
  });
  next();
});

Note that we call the callback called done here when our authentication process is complete. If called simply, the execution will be passed to the handler of the route. If passed with some error message, this will automatically throw unauthorized status with the error message passed in.

Also, I need the userId retrieved in the handler function. For that, I am using the following and can be accessed using request.params in the handler.

        req.params.userId = userId;

Step 3: Add the authentication strategy to the route

We are using the preValidation lifecycle method to validate each request using the verifyJWT method.

// routes/wishlist.ts

import * as fastifyPlugin from "fastify-plugin";

export const fastifyPlugin((fastify, options, next) => {
  fastify.route({
    method: "GET",
    url: "/wishlist",
    preValidation: fastify.auth([fastify.verifyJWT]),
    handler: (req, rpl) => {
      //remember we set userId into req params in previous step?
      const { userId } = req.params;
      //get user from the db using this id
    }
  });
  next();
})

Note: Typescript will complain that it doesn't know a method verifyJWT in the fastify instance. To fix that, we need to extend the typings for Fastify.

Lets check the value of typeRoots in tsconfig.json.
Mine has"typeRoots": ["node_modules/@types", "types"].

So in types folder in the root of the project,

// in types/fastify/index.d.ts

import fastify from "fastify";
import { ServerResponse, IncomingMessage, Server } from "http";

declare module "fastify" {
  export interface FastifyInstance<
    HttpServer = Server,
    HttpRequest = IncomingMessage,
    HttpResponse = ServerResponse
  > {
    verifyJWT(): void;
    someOtherDecorator(rpl: any, userId: string) => void
  }
}

The fastify.auth function takes an array of authentication functions. We could pass any number of functions inside that and those functions will be checked on a OR condition. Say,
I pass in

    preValidation:fastify.auth(
         [
          fastify.verifyJWT,
          fastify.verifyUsernameAndPassword
         ]
    )

Now, if any one of these auth function passes, it is considered as a authenticated request. You could add a AND condition also using

preValidation: fastify.auth([
        fastify.verifyIsAdmin,
        fastify.verifyIsSuperAdmin
      ], {
        relation: 'and'
      }),

Posted on by:

Discussion

pic
Editor guide