63-Nodejs Course 2023: Auth: Access Token Database Collection

hassanzohdy

Hasan Zohdy

Posted on November 12, 2022

63-Nodejs Course 2023: Auth: Access Token Database Collection

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";
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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",
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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",
      });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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",
      });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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

Packages & Libraries

React Js Packages

Courses (Articles)

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on November 12, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related