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:
- Create the server
- Create required routes
- Register routes in server
- 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.
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 -
- we sign the token with our own private key
- the received token should be decodable using the corresponding public key for the private key
- 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
// 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.
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"] || "" });
}
);
};
Top comments (0)