Skip to content

Commit

Permalink
Added authorization middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabrice-Dush committed May 30, 2024
1 parent 5e7953f commit e5bfafe
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 104 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"start": "ts-node-dev src/index.ts",
"dev": "ts-node-dev src/index.ts",
"test": "cross-env NODE_ENV=test npm run deleteAllTables && cross-env NODE_ENV=test npm run createAllTables && cross-env NODE_ENV=test npm run createAllSeeders && nyc cross-env NODE_ENV=test mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit",
"test-local": "cross-env NODE_ENV=development npm run deleteAllTables && cross-env NODE_ENV=development npm run createAllTables && cross-env NODE_ENV=development npm run createAllSeeders && nyc cross-env NODE_ENV=development mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit",
"coveralls": "nyc --reporter=lcov --reporter=text-lcov npm test | coveralls",
"coverage": "cross-env NODE_ENV=test nyc mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit",
"lint": "eslint . --ext .ts",
Expand All @@ -34,8 +35,7 @@
"exclude": [
"src/index.spec.ts",
"src/databases/**/*.*",
"src/modules/**/test/*.spec.ts",
"src/middlewares/index.ts"
"src/modules/**/test/*.spec.ts"
],
"reporter": [
"html",
Expand Down Expand Up @@ -83,6 +83,7 @@
"sequelize-cli": "^6.6.2",
"sequelize-typescript": "^2.1.6",
"sinon": "^18.0.0",
"sinon-chai": "^3.7.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"ts-node": "^10.9.2",
Expand All @@ -108,4 +109,4 @@
"eslint-plugin-import": "^2.29.1",
"lint-staged": "^15.2.2"
}
}
}
1 change: 1 addition & 0 deletions src/databases/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable comma-dangle */
import dotenv from "dotenv";

dotenv.config();
Expand Down
1 change: 1 addition & 0 deletions src/databases/config/db.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable comma-dangle */
import { config } from "dotenv";
import { Sequelize } from "sequelize";

Expand Down
167 changes: 90 additions & 77 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable comma-dangle */
import chai, { expect } from "chai";
import chaiHttp from "chai-http";
import app from "./index";
import sinon from "sinon";
import jwt from "jsonwebtoken";
import chai from "chai";
import chaiHttp from "chai-http";
const { expect } = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
const { userAuthorization } = require("./middlewares/authorization");
const httpStatus = require("http-status");
import * as helpers from "./helpers/index";
import authRepositories from "./modules/auth/repository/authRepositories";
import { protect } from "./middlewares";

chai.use(chaiHttp);
chai.use(sinonChai);
//
const router = () => chai.request(app);

describe("Initial configuration", () => {
Expand All @@ -26,109 +32,116 @@ describe("Initial configuration", () => {
});
});

describe("protect middleware", () => {
let req, res, next, sandbox;
describe("userAuthorization middleware", () => {
let req, res, next, roles;

beforeEach(() => {
sandbox = sinon.createSandbox();
roles = ["admin", "user"];
req = {
headers: {},
user: null,
session: null,
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub().returnsThis(),
};
next = sinon.stub();
next = sinon.spy();
});

afterEach(() => {
sandbox.restore();
sinon.restore();
});

it("should call next() if token is valid and user exists", async () => {
const user = { id: "123", name: "John Doe" };
const token = jwt.sign({ id: user.id }, "SECRET");

sandbox.stub(jwt, "verify").resolves({ id: user.id });
sandbox.stub(authRepositories, "findUserByAttributes").resolves(user);

req.headers.authorization = `Bearer ${token}`;

await protect(req, res, next);
it("should respond with 401 if no authorization header", async () => {
const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(next.calledOnce).to.be.true;
expect(req.user).to.deep.equal(user);
expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should return 401 if no token is provided", async () => {
req.headers.authorization = "";
it("should respond with 401 if no session found", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves(null);

await protect(req, res, next);
const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
res.json.calledWith({
ok: false,
status: "fail",
message: "Login to get access to this resource",
})
).to.be.true;
expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should return 401 if token is invalid", async () => {
req.headers.authorization = "Bearer invalidtoken";
it("should respond with 401 if no user found", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves({});
sinon.stub(authRepositories, "findUserByAttributes").resolves(null);

sandbox
.stub(jwt, "verify")
.throws(new jwt.JsonWebTokenError("invalid token"));
const middleware = userAuthorization(roles);
await middleware(req, res, next);

await protect(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
res.json.calledWith({
ok: false,
status: "fail",
message: "Invalid token. Log in again to get a new one",
})
).to.be.true;
expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should return 401 if user does not exist", async () => {
const token = jwt.sign({ id: "123" }, "SECRET");

sandbox.stub(jwt, "verify").resolves({ id: "123" });
sandbox.stub(authRepositories, "findUserByAttributes").resolves(null);
it("should respond with 401 if user role is not authorized", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves({});
sinon
.stub(authRepositories, "findUserByAttributes")
.resolves({ role: "guest" });

const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

req.headers.authorization = `Bearer ${token}`;
it("should call next if user is authorized", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves({});
sinon
.stub(authRepositories, "findUserByAttributes")
.resolves({ role: "admin" });

await protect(req, res, next);
const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
res.json.calledWith({
ok: false,
status: "fail",
message: "User belonging to this token does not exist",
})
).to.be.true;
expect(next).to.have.been.calledOnce;
expect(req.user).to.deep.equal({ role: "admin" });
expect(req.session).to.deep.equal({});
});

it("should handle jwt token expiration errors", async () => {
req.headers.authorization = "Bearer expiredtoken";

const error = new jwt.TokenExpiredError("jwt expired", new Date());
sandbox.stub(jwt, "verify").throws(error);
it("should respond with 500 if an unexpected error occurs", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").rejects(new Error("Unexpected error"));

await protect(req, res, next);
const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
res.json.calledWith({
ok: false,
status: "fail",
message: "Invalid token. Log in again to get a new one",
})
).to.be.true;
expect(res.status).to.have.been.calledWith(
httpStatus.INTERNAL_SERVER_ERROR
);
expect(res.json).to.have.been.calledWith({
status: httpStatus.INTERNAL_SERVER_ERROR,
message: "Unexpected error",
});
});
});
64 changes: 64 additions & 0 deletions src/middlewares/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable comma-dangle */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response, NextFunction } from "express";
import { UsersAttributes } from "../databases/models/users";
import authRepository from "../modules/auth/repository/authRepositories";
import httpStatus from "http-status";
import { decodeToken } from "../helpers";
import Session from "../databases/models/session";

interface ExtendedRequest extends Request {
user: UsersAttributes;
session: Session;
}

export const userAuthorization = function (roles: string[]) {
return async (req: ExtendedRequest, res: Response, next: NextFunction) => {
try {
let token: string;
if (req.headers.authorization?.startsWith("Bearer")) {
token = req.headers.authorization.split(" ").at(-1);
}

if (!token) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

const decoded: any = await decodeToken(token);

const session: Session = await authRepository.findSessionByUserIdAndToken(
decoded.id, token
);
if (!session) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

const user = await authRepository.findUserByAttributes("id", decoded.id);

if (!user) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

if (!roles.includes(user.role)) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

req.user = user;
req.session = session;
next();
} catch (error: any) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
status: httpStatus.INTERNAL_SERVER_ERROR,
message: error.message,
});
}
};
};
Loading

0 comments on commit e5bfafe

Please sign in to comment.