DEV Community

Lekshmi Chandra
Lekshmi Chandra

Posted on

Authentication Strategy - Fastify + Typescript + JWT

In this post we can check how to create an application using fastify, that exposes REST endpoints that will store/retrieve user information and authenticate user as needed.

If you are familiar with Express, Fastify is only a change of flavor for you.

Let's split the work into the following steps:

  1. Create the server
  2. Create required routes
  3. Register routes in server
  4. Add authentication using JWT and HTTP cookies

Step 1: Creating the app

Let's import the Fastify package installed and create a Fastify app
passing some configurations. Then we can make server to listen on the
required port.

import * as fastify from "fastify";

let fastifyConfig: FastifyConfig = {
  trustProxy: true,
  logger: {
    useLevelLabels: true,
    level: "warn"
  }
};

const fastifyApp = fastify(fastifyConfig);

fastifyApp.listen(PORT, "0.0.0.0");
Enter fullscreen mode Exit fullscreen mode

2. Create required routes

For better organization of the code, I am going to keep
my routes in a folder called routes/ and register them
later to the fastifyApp that is created earlier.

We are using fastify-plugin to attach the route configurations
to the fastify instance.
For that, lets install the package first

    yarn add fastify-plugin
Enter fullscreen mode Exit fullscreen mode

Time to write some routes

In routes/user.ts

import import * as fastifyPlugin from "fastify-plugin";

export default fastifyPlugin( async (fastify, opts, callback ) => {
  fastify.get("/", options, (_, rpl) => {
    rpl.code(200).send("all ok here");
  }); 

  callback();
}) 
Enter fullscreen mode Exit fullscreen mode

Another way of writing the routes is


import ajv from "../lib/ajv";

  fastify.route({
    method: "POST",
    url: "/auth",
    schema: {
      body: ajv.getSchema("urn:schema:request:user").schema,
      headers: ajv.getSchema("urn:schema:request:UserAccessToken").schema    
    },
    handler: (req, rpl) => {
        //some logic you wanted
    });

Enter fullscreen mode Exit fullscreen mode

Simple as it is. Now what is schema? I am using a schema validator package ajv and passed a schema validation to validate the request headers and body here.

And what is in ajv.ts?

import * as Ajv from "ajv";

const ajv = new Ajv({
  removeAdditional: true
});

ajv.addSchema({
  $id: "urn:schema:request:user",
  type: "object",
  required: ["email"],
  properties: {
    name: { type: "string" },
    loggedInUsing: { type: "string", enum: ["facebook", "google"] },
    ip: { type: "string", maxLength: 15, minLength: 7 },
    email : { type: "string" }
  }
});

ajv.addSchema({
  $id: "urn:schema:request:UserAccessToken",
  type: "object",
  required: ["X-access-token"],
  properties: {
    UserToken: { type: "string" }
  }
});

Enter fullscreen mode Exit fullscreen mode

urn:schema:request:user specifies which all properties are required and if present what should be the data be like. Additionally, email is specified as mandatory using required: ["email"].

3. Register the routes

In index.ts

import * as fastify from "fastify";
import userRoute from 'routes/user'

let fastifyConfig: FastifyConfig = {
  trustProxy: true,
  logger: {
    useLevelLabels: true,
    level: "warn"
  }
};

const fastifyApp = fastify(fastifyConfig)
.register(userRoute)
.register(fastifyCookie) //to manipulate cookies in the routes
.setSchemaCompiler(schema => { //to use schema validator
    return ajv.compile(schema);
  });

fastifyApp.listen(7000, "0.0.0.0");
Enter fullscreen mode Exit fullscreen mode

Now if you GET on http://localhost:7000, you should
get 200 ok with all ok message.

4. Add authentication using JWT and HTTP cookies

When a new user is registered, we set a HTTP cookie in the response. A JWT token with an expiry time and some unique data to identify the user is set to the cookie.

The idea is, we need not check whether the user is an authentic without going through the whole procedure of going to the db. Instead, for a fixed period of time, the user who brings a valid token is identified as this user. And, how is security ensured -

  1. we sign the token with our own private key
  2. the received token should be decodable using the corresponding public key for the private key
  3. we refresh the expiry time frequently

Lets attach a method setAuthCookie to fastify.
Read more about setting decorators here - Custom Authentication Strategy in Fastify using decorators, lifecycle hooks and fastify-auth

Fastify - decorators

// in decorators.ts

fastify.decorate("setAuthCookie", function(rpl: any, userId: string) {
    const token = createToken(userId); //create a token with custom data
    rpl.setCookie(COOKIE_NAME, token, {
      httpOnly: true,
      secure: true
    });
  });

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

httpOnly - JS cannot read the cookie. It is passed on in further requests to the same host.
secure - the cookie is set over a https only connection

A sample createToken would be

const privateKey = fs.readFileSync(
  path.join(__dirname, "../../keys/my-key.key"),
  "utf8"
);

export const createToken = (id: string) => {
  const tok = jwt.sign(
    {
      id
    },
    privateKey,
    { algorithm: "RS256", expiresIn: "2h" }
  );
  return tok;
};

Enter fullscreen mode Exit fullscreen mode

And when the user is authenticated first, we call the setAuthCookie decorator to set the cookie.

  fastify.route({
    method: "POST",
    url: "/authenticate",
    schema: {
      body: ajv.getSchema("urn:schema:request:user").schema,
      headers: ajv.getSchema("urn:schema:request:UserAccessToken").schema    
    },
    handler: (req, rpl) => {
        //if everything goes around well
        fastify.setAuthCookie()
    });

Enter fullscreen mode Exit fullscreen mode

Next time, when a request comes in with the cookie, we can retrieve the cookie and get the JWT, decode the JWT with the public key and get the info that is set in it.

Fastify - preValidation

Here, let's verify the token in the preValidation hook of each route

fastify.route({
    method: "GET",
    url: "/something",
    preValidation: fastify.auth([fastify.verifyJWT]),
    handler: (req, rpl) => { //do what is needed here }
  });
Enter fullscreen mode Exit fullscreen mode

And the verifyJWT method is

  fastify.decorate("verifyJWT", (req: any, res: any, done: any) => {
      const cookie = req.cookies[AUTH_COOKIE_NAME];
      const callback = ({ userId, err }: TokenDecoded) => {
        if(userId) done(); //passes control to handler
        if(err){ done('some err msg') // sends 401 status code and this msg to client}
      };
      verifyToken(cookie, callback);
    });

Enter fullscreen mode Exit fullscreen mode

The verifyToken method would be

const publicKey = fs.readFileSync(
  path.join(__dirname, "../../keys/my-key.key.pub"),
  "utf8"
);

export const verifyToken = (
  token: string,
  callback: ({ err, userId }: TokenDecoded) => void
) => {
  jwt.verify(
    token,
    publicKey,
    { algorithms: ["RS256"] },
    (err, decoded: any) => {
      if (err) {
        return callback({ err: "unauthorized" });
      }
      return callback({ userId: decoded["id"] || "" });
    }
  );
};

Enter fullscreen mode Exit fullscreen mode

Top comments (0)