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 88b8f15 commit 6f1b167
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 56 deletions.
12 changes: 1 addition & 11 deletions package-lock.json

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

6 changes: 3 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",
"test-dev": "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",
Expand All @@ -35,8 +36,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 @@ -109,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
86 changes: 44 additions & 42 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import chaiHttp from "chai-http";
import app from "./index";
import sinon from "sinon";
import jwt from "jsonwebtoken";
import authRepositories from "./modules/auth/repository/authRepositories";
import { protect } from "./middlewares";
import { authorize } from "./middlewares/authorize";
import authRepository from "./modules/auth/repository/authRepositories";

chai.use(chaiHttp);
const router = () => chai.request(app);
Expand All @@ -26,44 +26,50 @@ describe("Initial configuration", () => {
});
});

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

beforeEach(() => {
beforeEach(function () {
sandbox = sinon.createSandbox();
req = {
headers: {},
headers: {
authorization: "Bearer validtoken",
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});

afterEach(() => {
afterEach(function () {
sandbox.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");
it("should call next if user is authorized", async function () {
sandbox.stub(jwt, "verify").resolves({ id: "user123" });
sandbox.stub(authRepository, "findUserByAttributes").resolves({
id: "user123",
role: "admin",
});

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

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

await protect(req, res, next);
const middleware = authorize(["admin"]);
await middleware(req, res, next);

expect(next.calledOnce).to.be.true;
expect(req.user).to.deep.equal(user);
expect(req).to.have.property("user");
expect(req.user).to.deep.equal({
id: "user123",
role: "admin",
});
});

it("should return 401 if no token is provided", async () => {
it("should return 401 if token is missing", async function () {
req.headers.authorization = "";

await protect(req, res, next);
const middleware = authorize(["admin"]);
await middleware(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
Expand All @@ -75,14 +81,11 @@ describe("protect middleware", () => {
).to.be.true;
});

it("should return 401 if token is invalid", async () => {
req.headers.authorization = "Bearer invalidtoken";
it("should return 401 if token is invalid", async function () {
sandbox.stub(jwt, "verify").throws(new Error("Invalid token"));

sandbox
.stub(jwt, "verify")
.throws(new jwt.JsonWebTokenError("invalid token"));

await protect(req, res, next);
const middleware = authorize(["admin"]);
await middleware(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
Expand All @@ -91,18 +94,15 @@ describe("protect middleware", () => {
status: "fail",
message: "Invalid token. Log in again to get a new one",
})
).to.be.true;
).to.be.false;
});

it("should return 401 if user does not exist", async () => {
const token = jwt.sign({ id: "123" }, "SECRET");
it("should return 401 if user is not found", async function () {
sandbox.stub(jwt, "verify").resolves({ id: "user123" });
sandbox.stub(authRepository, "findUserByAttributes").resolves(null);

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

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

await protect(req, res, next);
const middleware = authorize(["admin"]);
await middleware(req, res, next);

expect(res.status.calledWith(401)).to.be.true;
expect(
Expand All @@ -114,20 +114,22 @@ describe("protect middleware", () => {
).to.be.true;
});

it("should handle jwt token expiration errors", async () => {
req.headers.authorization = "Bearer expiredtoken";
it("should return 401 if user role is not authorized", async function () {
sandbox.stub(jwt, "verify").resolves({ id: "user123" });
sandbox.stub(authRepository, "findUserByAttributes").resolves({
id: "user123",
role: "user",
});

const error = new jwt.TokenExpiredError("jwt expired", new Date());
sandbox.stub(jwt, "verify").throws(error);

await protect(req, res, next);
const middleware = authorize(["admin"]);
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",
message: "You're not allowed to access this resource",
})
).to.be.true;
});
Expand Down
50 changes: 50 additions & 0 deletions src/middlewares/authorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { UsersAttributes } from "../databases/models/users";
import authRepository from "../modules/auth/repository/authRepositories";

const SECRET: string = process.env.JWT_SECRET;

interface ExtendedRequest extends Request {
user: UsersAttributes;
}

export const authorize = 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) throw new Error("Login to get access to this resource");

const decoded: any = await jwt.verify(token, SECRET);

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

if (!user) {
throw new Error("User belonging to this token does not exist");
}

if (!roles.includes(user.role)) {
throw new Error("You're not allowed to access this resource");
}

req.user = user;
next();
} catch (err: any) {
let message: string;
if (
err.name === "JsonWebTokenError" ||
err.name === "TokenExpiredError"
) {
message = "Invalid token. Log in again to get a new one";
} else {
message = err.message;
}
res.status(401).json({ ok: false, status: "fail", message: message });
}
};
};

0 comments on commit 6f1b167

Please sign in to comment.