# Security

# Authentication

ExpressWebJs strives to give you the tools you need to implement authentication quickly, securely, and easily. Since ExpressWebJs does not support session state, incoming requests that you wish to authenticate will be authenticated via a stateless mechanism such as API tokens.

# Password Hashing

ExpressWebJs uses the Hash module to verify passwords.

Always hash your passwords before saving them to the database.

# Getting Started

Run the Maker command to setup your auth routes in Routes/authRoute/index.js file

  ts-node maker make-auth
"use strict";
import Route from "Elucidate/Route/manager";

/*
|--------------------------------------------------------------------------
| Authentication Route File   
|--------------------------------------------------------------------------
|
| This route handles both login and registration.
| 
*/

Route.post("/register", "Auth/RegisterController@register");

Route.post("/login", "Auth/LoginController@login");

export default Route.exec;

Next, uncomment the auth middleware inside the App/Http/kernel.js file routeMiddleware section:

   /*
  |--------------------------------------------------------------------------
  | Route Middleware
  |--------------------------------------------------------------------------
  |
  | Route middleware is a key/value object to conditionally add middleware on
  | specific routes or assigned to group of routes.
  |
  | // define
  | {
  |   auth: 'App/Http/Middleware/Auth'
  | }
  |
  | // in your route add ["auth"]
  |
  */
  routeMiddleware: {
    auth: "App/Http/Middleware/Auth",
  },

# Config

Authentication configuration is saved inside the App/Config/auth.js file

module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Authenticator
  |--------------------------------------------------------------------------
  |
  | ExpressWebJs does not support session state, incoming requests that 
  | you wish to authenticate must be authenticated via a stateless mechanism such as API tokens.
  |
  */
  authenticator: "jwt",

  /*
  |--------------------------------------------------------------------------
  | Jwt
  |--------------------------------------------------------------------------
  |
  | The jwt authenticator works by passing a jwt token on each HTTP request
  | via HTTP `Authorization` header.
  |
  */
  jwt: {
    model: "User_model",
    driver: "jwt",
    uid: "email",
    password: "password",
    secret: process.env.APP_KEY,
    options: {
      expiresIn: 86400, //default is 86400 (24 hrs)
    },
  },
};
Key Value Description
uid Database field name Database field used as the unique identifier for a given user.
password Database field name Field used to verify the user password.
model Model name Model used to query the database
secret APP_KEY Application key. This is located in your .env file
expiresIn Valid time in seconds or ms string When to expire tokens. (This is in the options section)
algorithm HS256, HS384, RS256 Algorithm used to generate tokens. (This is in the options section)

You can add more JWT options in the options sections.

By default, uid and password values are set to email and password. If you want to change it to any other column name, you can do that like so:

Note: make sure the column names exists in your table.

Lets change uid from email to username, that means we want to authenticate with username and password instead of email and password.

jwt: {
    model: "User_model",
    driver: "jwt",
    uid: "username",
    password: "password",
    secret: process.env.APP_KEY,
    options: {
      expiresIn: 86400, //default is 86400 (24 hrs)
    },
  },
};

Then update the validator values in LoginController

LoginController in App/Http/Controller/Auth/LoginController.js

/**
   * Get a validator for an incoming login request.
   * @param {Array} record
   * @return Validator
   */
  [validator](record) {
    return FormRequest.make(record, {
      username: "required|string|max:255",
      password: "required|string|min:8",
    });
  }

# Social Authentication

Along with the standard JWT authentication, ExpressWebJs also helps you implement social authentication with OAuth providers like Facebook, Google, Twitter, LinkedIn, and Microsoft out of the box.

# Configuration

To get started with social authentication, uncomment SocialServiceProvider in Config/app.ts providers section.

  providers: [
    /*
     * ExpressWebJS Framework Service Providers...
     */
    "Elucidate/Route/RouteConfigServiceProvider::class",
    "Elucidate/Database/DatabaseServiceProvider::class",
    //"Elucidate/Social/SocialServiceProvider::class",  <--- uncomment SocialServiceProvider
  ],

Once that is done, we now move over to social authentication configuration which is located at Config/socials.ts file to configure our drivers.

"use strict";
import env from "expresswebcorets/lib/ENV";

export default {
  /*
    |--------------------------------------------------------------------------
    | Social Connection Drivers
    |--------------------------------------------------------------------------
    |
    | ExpressWebJs social API supports so many authenication services,
    | giving you the flexibility to use single sign-on using an OAuth provider such as
    | Facebook,Twitter,LinkedIn,Slack,Google, Microsoft, Github, and GitLab.
    | 
    | You can set up social connection drivers like so:
    | social_connections: ["facebook","twitter","linkedIn"]
    |
    */

  social_connections: ["facebook"], // ["facebook","twitter","linkedIn"]

  /*
    |--------------------------------------------------------------------------
    | Social connections
    |--------------------------------------------------------------------------
    |
    | Here you can configure your social connection information 
    | for any of the drivers you choose.
    |
    */

  connections: {
    facebook: {
      driver: "facebook",
      clientID: env("FACEBOOK_CLIENT_ID"),
      clientSecret: env("FACEBOOK_CLIENT_SECRET"),
      callbackURL: "http://localhost:5000/api/facebook/secrets",
    },
    google: {
      driver: "google",
      clientID: env("GOOGLE_CLIENT_ID"),
      clientSecret: env("GOOGLE_CLIENT_SECRET"),
      callbackURL: "http://localhost:5000/api/auth/google/callback",
    },
    twitter: {
      driver: "twitter",
      consumerKey: env("TWITTER_CONSUMER_KEY"),
      consumerSecret: env("TWITTER_CONSUMER_SECRET"),
      callbackURL: "http://localhost:5000/api/auth/twitter/callback",
    },
    linkedin: {
      driver: "linkedin",
      clientID: env("LINKEDIN_CLIENT_ID"),
      clientSecret: env("LINKEDIN_CLIENT_SECRET"),
      callbackURL: "http://localhost:5000/api/auth/linkedin/callback",
      scope: ["r_emailaddress", "r_liteprofile"],
    },
    microsoft: {
      driver: "microsoft",
      clientID: env("MICROSOFT_CLIENT_ID"),
      clientSecret: env("MICROSOFT_CLIENT_SECRET"),
      callbackURL: "http://localhost:5000/api/auth/microsoft/callback",
      scope: ["user.read"],
    },
  },
};

# driver

Name of the driver to use. It must always be one of the following available drivers.

  • facebook
  • twitter
  • google
  • linkedin
  • microsoft

# clientID

The OAuth provider's client id. This must be securely kept inside the environment variables.

# clientSecret

The OAuth provider's client secret. This must be securely kept inside the environment variables.

# callbackURL

The callback URL to handle the post redirect response from the OAuth provider. You must register the same URL with the OAuth provider as well.

# Authenticate requests

After the setup process, you can now access your Social object inside your route handlers using the Social.use() method and specifying your driver e.g: facebook, twitter etc which redirects the user to the OAuth provider website.

import Route from "Elucidate/Route/manager";
import { Request, Response, NextFunction } from "Elucidate/HttpContext";
import Social from "Elucidate/Social";

Route.get("/auth/facebook", Social.use("facebook"));

# Handling the callback request

Once the user decides to approve or rejects the login request, the OAuth provider will redirect the user back to the callbackUrl where you can now access the user with Social.authenticate() method.

import Route from "Elucidate/Route/manager";
import { Request, Response, NextFunction } from "Elucidate/HttpContext";
import Social from "Elucidate/Social";

Route.get("/auth/facebook", Social.use("facebook"));

Route.get("/facebook/callback", function(req: Request, res: Response, next: NextFunction) {
  Social.authenticate("facebook", req, res, next)
    .then((facebook: { profile: object; info: object }) => {
      let user = facebook.profile;
      // Process user data
      console.log(user);
    })
    .catch((err: any) => {
      //Handle error is any
      console.log(err);
    });

  res.redirect("/api/");
});

You can also decide to handle it in a controller.

import Route from "Elucidate/Route/manager";
import { Request, Response, NextFunction } from "Elucidate/HttpContext";
import Social from "Elucidate/Social";

Route.get("/auth/facebook", Social.use("facebook"));

// Handle callback in FacebookController
Route.get("/facebook/callback", "FacebookController@facebookCallback");

Our FacebookController

import Social from "Elucidate/Social";

class FacebookController {
  facebookCallback = async (req: Request, res: Response, next: NextFunction) => {
    try {
      await Social.authenticate("facebook", req, res, next)
        .then((facebook: { profile: object; info: object }) => {
          console.log(facebook);
        })
        .catch((err: any) => {
          console.log(err);
        });

      res.redirect("/api/");
    } catch (error) {
      return next(error);
    }
  };
}

export default FacebookController;

# Authorization

In addition to enabling you build a secured application, ExpressWebJs also provides a simple way to authorize user actions against a given resource. For example, even though a user is authenticated, they may not be authorized to update or delete certain records in the database. ExpressWebJs authorization features provide an easy, organized way of managing these types of authorization checks.

# Prerequisites

Before we continue with ExpressWebJS Identity Manager, you should make sure the following models and schemas does not exist in the system.

  • Permissions_model.ts
  • Role_permissions_model.ts
  • Roles_model.ts
  • User_roles_model.ts

If you are using SQL database, you should also make sure the above mentioned model schemas does not exist in your Database/Migrations directory.

We can now run the following maker command to generate our database models and schemas(for SQL database) needed for authorization operations.

  ts-node maker make-identity

Once that is done, we can now create a ManyToMany relationship with Roles_model and our already existing User_model.

For SQL Model, we will do this:

"use strict";
import { Model } from "Elucidate/Database/Model";
import Roles_mode from "./Roles_model";

class User extends Model {
  // Model attributes
  id!: number;
  first_name!: string;
  last_name!: string;
  email!: string;
  password!: string;

  // Table name
  static tableName = "users";

  static relationMappings = {
    roles: {
      relation: Model.ManyToManyRelation,
      modelClass: Roles_mode,
      join: {
        from: "users.id",
        through: {
          from: "user_roles.user_id",
          to: "user_roles.role_id",
        },
        to: "roles.id",
      },
    },
  };
}

export default User;

You can now run your migration with the following Maker command.

  ts-node maker run-sql-migration

Or with the short hand

  ts-node maker rsqlm

For NOSQL Model, we will do this:

"use strict";
import { mongoose, Schema, Document } from "Elucidate/Database/NoSQLModel";
import Roles from "./Roles_model";
import uniqueValidator from "mongoose-unique-validator";

export interface UserInterface extends Document {
  username: string;
  email: string;
  password: string;
}

const UserSchema: Schema = new Schema({
  username: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  role: [{ type: mongoose.Schema.Types.ObjectId, ref: Roles }],
  password: { type: String, required: true },
});

UserSchema.set("timestamps", true);
UserSchema.plugin(uniqueValidator);

const User = mongoose.model<UserInterface>("User", UserSchema);
export default User;

Next register Identity Manager in the AppServiceProvider in App/Providers/AppServiceProvider.ts register method.

import ServiceProvider from "Elucidate/Support/ServiceProvider";
import IdentityManager from "Elucidate/IdentityManager";

class AppServiceProvicer extends ServiceProvider {
  /**
   * Register any application services.
   * @return void
   */
  public register() {
    this.app.register("IdentityManager", IdentityManager, "class");
  }

  /**
   * Bootstrap any application services.
   * @return void
   */
  public async boot() {
    //
  }
}

export default AppServiceProvicer;

Once that is done, we can now inject it in our Controllers class or Services class or any other class we want to use it with. Let's use it in our UserController class in App/Http/Controller/UserController.ts

"use strict";
import { Request, Response, NextFunction } from "Elucidate/HttpContext";
import HttpResponse from "Elucidate/HttpContext/ResponseType";
import IdentityManager from "Elucidate/IdentityManager"; //this is the IdentityManger type

class UserController {
  protected identityManager: IdentityManager;

  constructor(IdentityManager: IdentityManager) {
    this.identityManager = IdentityManager;
  }

  getSystemRoles = async (req: Request, res: Response, next: NextFunction) => {
    try {
      let roles = await this.identityManager.getRoles();
      return HttpResponse.OK(res, roles);
    } catch (error) {
      return next(error);
    }
  };
  /**
   * Display a listing of the resource.
   */
  checkUserRole = async (req: Request, res: Response, next: NextFunction) => {
    try {
      return await this.identityManager
        .hasRole(req.user["id"], req.params["role"])
        .then((result) => {
          return HttpResponse.OK(res, result);
        })
        .catch((err) => {
          return HttpResponse.EXPECTATION_FAILED(res, err);
        });
    } catch (error) {
      return next(error);
    }
  };

  /**
   * Show the form for creating a new resource.
   *
   * @return Response
   */
  createRole = async (req: Request, res: Response, next: NextFunction) => {
    try {
      let { name, description } = req.body;
      return await this.identityManager
        .createRole(name, description)
        .then((createdRole) => {
          return HttpResponse.OK(res, createdRole);
        })
        .catch((err) => {
          return HttpResponse.EXPECTATION_FAILED(res, err);
        });
    } catch (error) {
      return next(error);
    }
  };

  /**
   * Store a newly created resource in storage.
   * @param  Request
   * @return Response
   */
  addUserToRole = async (req: Request, res: Response, next: NextFunction) => {
    try {
      let { role_id, user_id } = req.body;
      return await this.identityManager
        .assignRoleToUser(role_id, user_id)
        .then((result) => {
          return HttpResponse.OK(res, result);
        })
        .catch((err) => {
          return HttpResponse.EXPECTATION_FAILED(res, err);
        });
    } catch (error) {
      return next(error);
    }
  };
}

export default UserController;

# Basic Usage

//Creating a new role
let roleName = "Admin";
let roleDescription = "System Admin";

IdentityManager.createRole(roleName, roleDescription);

//Creating a new permission
let permissionName = "edit articles";
let permissionDescription = "This permission can only edit articles";

IdentityManager.createPermission(permissionName, permissionDescription);

A permission or multiple permissions can be synced to a role:

let roleId = "2";
let permissionIds = ["1", "2", "3", "4", "5"];
IdentityManager.giveRolePermission(roleId, permissionIds);

A permission or multiple permissions can be removed from a role:

let roleId = "2";
let permissionIds = ["1", "2", "3", "4", "5"];
IdentityManager.revokeRolePermissions(roleId, permissionIds);

# Encryption

ExpressWebJs encryption services provide a simple, convenient way for encrypting and decrypting text via OpenSSL using AES-256 and AES-128 encryption. All of ExpressWebJs encrypted values are signed using a message authentication code (MAC) so that their underlying value can not be modified or tampered with once encrypted.

# Configuration

Before using ExpressWebJs encrypter, you must set the key configuration option in your APP_KEY environment variable in .env file.

# Encrypting A Value

The encryption module generates a unique iv (opens new window) for every encryption call. Hence encrypting the same value twice will result in a different visual output.

import Encryption from "Elucidate/Encryption";

new Encryption().encrypt("Welcome to ExpressWebJs");

# Also You can encrypt the following data types.

// Object
new Encryption.encrypt({
  firstName: "Alex",
  lastName: "Igbokwe",
});

// Nested Object:
new Encryption.encrypt({
  foo: {
    bar: [1, "baz"],
  },
});

// Array
new Encryption.encrypt([1, 2, 3, 4]);

// Boolean
new Encryption.encrypt(true);

// Number
new Encryption.encrypt(10);

// Date objects are converted to ISO string
new Encryption.encrypt(new Date());

# Decrypt A Value

The Encryption.decrypt method decrypts the encrypted value. Returns null when unable to decrypt the value.

import Encryption from "Elucidate/Encryption";

new Encryption().decrypt(encryptedValue);

# Using a custom secret key

ExpressWebJs Encryption module uses the APP_KEY inside .env environment file as the secret for encrypting values. However, you can create a custom secret key as well.

import Encryption from "Elucidate/Encryption";

let encryption = new Encryption();
encryption.setKey("My_custom_key");

# Hashing

ExpressWebJs Hash module provides secure Bcrypt and Argon2 hashing for storing user passwords. Bcrypt is used for registration and authentication by default. You are free to use Argon2.

Bcrypt is a great choice for hashing passwords because its "work factor" is adjustable, which means that the time it takes to generate a hash can be increased as hardware power increases. When hashing passwords, slow is good. The longer an algorithm takes to hash a password, the longer it takes malicious users to generate "rainbow tables" of all possible string hash values that may be used in brute force attacks against applications.

# Configuration

You can configure the driver of your choice inside the Config/hashing.ts file.

import env from "Elucidate/ENV";
export default {
  /*
    |--------------------------------------------------------------------------
    | Default Hash Driver
    |--------------------------------------------------------------------------
    |
    | This option controls the default hash driver that will be used to hash
    | passwords for your application. By default, the bcrypt algorithm is
    | used; however, you remain free to modify this option if you wish.
    |
    | Supported: "bcrypt", "argon".
    |
    */

  driver: "bcrypt",

  /*
    |--------------------------------------------------------------------------
    | Bcrypt Options
    |--------------------------------------------------------------------------
    | npm install bcrypt
    |--------------------------------------------------------------------------
    | Here you may specify the configuration options that should be used when
    | passwords are hashed using the Bcrypt algorithm. This will allow you
    | to control the amount of time it takes to hash the given password.
    |
    */

  bcrypt: {
    rounds: env("BCRYPT_ROUNDS", 10),
  },

  /*
    |--------------------------------------------------------------------------
    | Argon Options
    |--------------------------------------------------------------------------
    | npm install argon
    |--------------------------------------------------------------------------
    | Here you may specify the configuration options that should be used when
    | passwords are hashed using the Argon algorithm. These will allow you
    | to control the amount of time it takes to hash the given password.
    |
    */

  argon: {
    hashLength: 32,
    timeCost: 3,
    memory: 4096,
    parallelism: 1,
    type: "argon2i",
  },
};

# Hashing values

# make

The Hash.make method accepts a string value.

import Hash from "Elucidate/Hashing/Hash";

const hashedPassword = await Hash.make(user.password);

# verify

The check method provided by the Hash module allows you to verify that a given plain-text string corresponds to a given hash:

import Hash from "Elucidate/Hashing/Hash";

const hashedPassword = await Hash.check("plain-text", hashedPassword);