63-Nodejs Course 2023: Auth: Access Token Database Collection
Hasan Zohdy
Posted on November 12, 2022
Previously, we saw how to implement JWT in our application, now we need to store the access token in our database, so we need to create it.
Why would we need to store the access token in the database?
We need to store the access token in the database to be able to revoke (Remove) it, so if the user logged in from a device and he/she wants to logout from that device only, we can revoke the access token of that device only.
Also, let's assume that the user wants to change his/her password, in that case we should revoke all the access tokens of the user, so he/she can't use the old password to login.
Create Access Token Collection
Now, as the auth system grows up, let's create a new folder called auth
inside the src/core
folder, and inside it let's create a new directory called models
then create a new file called access-token.ts
.
// src/core/auth/models/access-token.ts
import { Model } from "core/database";
export default class AccessToken extends Model {
/**
* {@inheritDoc}
*/
public static collectionName = "accessTokens";
}
A simple regular model that has only the collection name.
Now let's create a new file called index.ts
inside the src/core/auth
folder.
We'll export from it a function called registerAuthRoutes
which will manage all the auth routes.
We'll talk later about service providers concept.
Let's first update request
class to change the property name of Fastify request to be baseRequest
instead of request
.
import events from "@mongez/events";
import { get, only } from "@mongez/reinforcements";
import { Route } from "core/router/types";
import { validateAll } from "core/validator";
import { FastifyReply, FastifyRequest } from "fastify";
import response, { Response } from "./response";
import { RequestEvent } from "./types";
import UploadedFile from "./UploadedFile";
export class Request {
/**
* Fastify Request object
*/
public baseRequest!: FastifyRequest;
/**
* Response Object
*/
protected response: Response = response;
/**
* Route Object
*/
private route!: Route;
/**
* Parsed Request Payload
*/
protected payload: any = {};
/**
* Set request handler
*/
public setRequest(request: FastifyRequest) {
this.baseRequest = request;
this.parsePayload();
return this;
}
/**
* Parse the payload and merge it from the request body, params and query string
*/
protected parsePayload() {
this.payload.body = this.parseBody();
this.payload.query = this.baseRequest.query;
this.payload.params = this.baseRequest.params;
this.payload.all = {
...this.payload.body,
...this.payload.query,
...this.payload.params,
};
}
/**
* Parse body payload
*/
private parseBody() {
const body: any = {};
const requestBody = this.baseRequest.body as Record<string, any>;
for (const key in requestBody) {
const keyData = requestBody[key];
if (Array.isArray(keyData)) {
body[key] = keyData.map(this.parseInputValue.bind(this));
} else {
body[key] = this.parseInputValue(keyData);
}
}
return body;
}
/**
* Set Fastify response
*/
public setResponse(response: FastifyReply) {
this.response.setResponse(response);
return this;
}
/**
* Set route handler
*/
public setRoute(route: Route) {
this.route = route;
// pass the route to the response object
this.response.setRoute(route);
return this;
}
/**
* Trigger an http event
*/
protected trigger(eventName: RequestEvent, ...args: any[]) {
return events.trigger(`request.${eventName}`, ...args, this);
}
/**
* Listen to the given event
*/
public on(eventName: RequestEvent, callback: any) {
return this.trigger(eventName, callback);
}
/**
* Execute the request
*/
public async execute() {
// check for middleware first
const middlewareOutput = await this.executeMiddleware();
if (middlewareOutput !== undefined) {
return middlewareOutput;
}
const handler = this.route.handler;
// 👇🏻 check for validation using validateAll helper function
const validationOutput = await validateAll(
handler.validation,
this,
this.response,
);
if (validationOutput !== undefined) {
return validationOutput;
}
// call executingAction event
this.trigger("executingAction", this.route);
const output = await handler(this, this.response);
// call executedAction event
this.trigger("executedAction", this.route);
return output;
}
/**
* Execute middleware list of current route
*/
protected async executeMiddleware() {
if (!this.route.middleware || this.route.middleware.length === 0) return;
// trigger the executingMiddleware event
this.trigger("executingMiddleware", this.route.middleware, this.route);
for (const middleware of this.route.middleware) {
const output = await middleware(this, this.response);
if (output !== undefined) {
this.trigger("executedMiddleware");
return output;
}
}
// trigger the executedMiddleware event
this.trigger("executedMiddleware", this.route.middleware, this.route);
}
/**
* Get request input value from query string, params or body
*/
public input(key: string, defaultValue: any = null) {
return get(this.payload.all, key, defaultValue);
}
/**
* Get request body
*/
public get body() {
return this.payload.body;
}
/**
* Parse the given data
*/
private parseInputValue(data: any) {
// data.value appears only in the multipart form data
// if it json, then just return the data
if (data.file) return data;
if (data.value !== undefined) return data.value;
return data;
}
/**
* Get request file in UploadedFile instance
*/
public file(key: string): UploadedFile | null {
const file = this.input(key);
return file ? new UploadedFile(file) : null;
}
/**
* Get request params
*/
public get params() {
return this.payload.params;
}
/**
* Get request query
*/
public get query() {
return this.payload.query;
}
/**
* Get all inputs
*/
public all() {
return this.payload.all;
}
/**
* Get only the given keys from the request data
*/
public only(keys: string[]) {
return only(this.all(), keys);
}
/**
* Get boolean input value
*/
public bool(key: string, defaultValue = false) {
const value = this.input(key, defaultValue);
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
return Boolean(value);
}
/**
* Get integer input value
*/
public int(key: string, defaultValue = 0) {
const value = this.input(key, defaultValue);
return parseInt(value);
}
/**
* Get float input value
*/
public float(key: string, defaultValue = 0) {
const value = this.input(key, defaultValue);
return parseFloat(value);
}
/**
* Get number input value
*/
public number(key: string, defaultValue = 0) {
const value = Number(this.input(key, defaultValue));
return isNaN(value) ? defaultValue : value;
}
}
const request = new Request();
export default request;
I just replaced the property request
to be baseRequest
.
Now let's move our guest request to the auth index.
// src/core/auth/index.ts
export { default as registerAuthRoutes } from "./registerAuthRoutes";
Now let's create registerAuthRoutes
function.
// src/core/auth/registerAuthRoutes.ts
import { Request } from "core/http/request";
import { Response } from "core/http/response";
import router from "core/router";
export default function registerAuthRoutes() {
// now let's add a guests route in our routes to generate a guest token to our guests.
router.post("/guests", (request: Request, response: Response) => {
const token = request.baseRequest.jwt.sign({ userType: "guest" });
return response.send({
accessToken: token,
userType: "guest",
});
});
}
We just moved the code from the connectToServer
function and moved it here.
Now let's move the hook as well.
// src/core/auth/registerAuthRoutes.ts
import { Response } from "core/http/response";
import { getServer } from "core/http/server";
import router from "core/router";
export default function registerAuthRoutes() {
// get server instance
const server = getServer();
// now let's add a guests route in our routes to generate a guest token to our guests.
router.post("/guests", (_request, response: Response) => {
const token = server.jwt.sign({ userType: "guest" });
return response.send({
accessToken: token,
userType: "guest",
});
});
// now let's add an event to validate the request token
server.addHook("onRequest", async (request, reply) => {
if (request.url === "/guests") return;
try {
await request.jwtVerify();
} catch (err) {
reply.status(401).send({
error: "Unauthorized: Invalid Access Token",
});
}
});
}
Now we moved the onRequest
event to be in the registerAuthRoutes
function so we keep our connectToServer
function clean.
Now the final look of the connectToServer
function.
import config from "@mongez/config";
import router from "core/router";
import registerHttpPlugins from "./plugins";
import response from "./response";
import { getServer } from "./server";
export default async function connectToServer() {
const server = getServer();
registerHttpPlugins();
// call reset method on response object to response its state
server.addHook("onResponse", response.reset.bind(response));
router.scan(server);
try {
// 👇🏻 We can use the url of the server
const address = await server.listen({
port: config.get("app.port"),
host: config.get("app.baseUrl"),
});
console.log(`Start browsing using ${address}`);
} catch (err) {
console.log(err);
server.log.error(err);
process.exit(1); // stop the process, exit with error
}
}
Registering auth routes
If you can recall, and i doubt you would, we have a centralized routes file to register all of our app routes inside it , which is located under src/app/routes.ts
, there where we'll register our auth routes.
// src/app/routes.ts
// users module
import "app/users/routes";
import { registerAuthRoutes } from "core/auth";
// import auth routes
registerAuthRoutes();
Saving Access Token In Database
Now we've created our token, we need to register it in our database, luckily we don't really want to wait until the database saves the token, so we can just call the create
method directly without waiting for the promise to resolve.
// src/core/auth/registerAuthRoutes.ts
import { Response } from "core/http/response";
import { getServer } from "core/http/server";
import router from "core/router";
// 👇🏻 import the AccessToken model
import AccessToken from "./models/access-token";
export default function registerAuthRoutes() {
// get server instance
const server = getServer();
// now let's add a guests route in our routes to generate a guest token to our guests.
router.post("/guests", (_request, response: Response) => {
const token = server.jwt.sign({ userType: "guest" });
// 👇🏻 create the access token document in the database
AccessToken.create({
token,
userType: "guest",
});
return response.send({
accessToken: token,
userType: "guest",
});
});
// now let's add an event to validate the request token
server.addHook("onRequest", async (request, reply) => {
if (request.url === "/guests") return;
try {
await request.jwtVerify();
} catch (err) {
reply.status(401).send({
error: "Unauthorized: Invalid Access Token",
});
}
});
}
And that's it!, now we can capture the access token and save it in the database.
🎨 Conclusion
We created a database collection for access tokens and learnt the reason behind that behavior, we also also cleaned up our connectToServer
function and moved the auth routes to a separate file.
🚀 Project Repository
You can find the latest updates of this project on Github
😍 Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
🎞️ Video Course (Arabic Voice)
If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.
📚 Bonus Content 📚
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Posted on November 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.