Authentication Strategy  - Fastify  + Typescript + JWT
Tags: fastify, authentication, jwt, typescript
Published: 2020-01-11 23:36:49


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

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

```
// 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,

```

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

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

```

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](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#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 }
  });
```

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

```

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

```