DEV Community

Lekshmi Chandra
Lekshmi Chandra

Posted on

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

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() 

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
//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();
});

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
})

Enter fullscreen mode Exit fullscreen mode

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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
         ]
    )

Enter fullscreen mode Exit fullscreen mode

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'
      }),
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
alanaxp profile image
AlanAxp

Why do you need to use fastify-plugin?

Collapse
 
woss profile image
woss

it has to do with the scope creation fastify.io/docs/latest/Guides/Plug...