Custom Authentication Strategy in Fastify using decorators, lifecycle hooks and fastify-auth
Lekshmi Chandra
Posted on January 11, 2020
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
- check for jwt expiry
- then decode the jwt using my secret key and get the userId saved in it
- if verification passes, continues execution and passes the userId to the handler function.
- 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 January 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 11, 2020