DEV Community

Lekshmi Chandra Sheela
Lekshmi Chandra Sheela

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



##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
```



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





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

```



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

```



`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



Enter fullscreen mode Exit fullscreen mode

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



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


<a name="authentication"></a>
##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] (https://dev.to/lek890/custom-authentication-strategy-in-fastify-using-decorators-lifecycle-hooks-and-fastify-auth-2fo7)

[Fastify - decorators](https://github.com/fastify/fastify/blob/master/docs/Decorators.md)



Enter fullscreen mode Exit fullscreen mode

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




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,



Enter fullscreen mode Exit fullscreen mode

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



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)