From d8bb908b03c82348a12f689d48b95c7276defc1f Mon Sep 17 00:00:00 2001 From: Alie Date: Wed, 27 Dec 2023 18:19:46 +0100 Subject: [PATCH 1/5] added basic auth controler --- package.json | 2 ++ src/controllers/AuthControler.ts | 52 ++++++++++++++++++++++++++++++++ src/models/AppModel.ts | 19 ++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/controllers/AuthControler.ts create mode 100644 src/models/AppModel.ts diff --git a/package.json b/package.json index 299b1a8..7a8021d 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,11 @@ "dependencies": { "@types/express": "^4.17.21", "@types/express-list-endpoints": "^6.0.3", + "@types/jsonwebtoken": "^9.0.5", "@types/mongoose": "^5.11.97", "express": "^4.18.2", "express-list-endpoints": "^6.0.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.3" } } \ No newline at end of file diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts new file mode 100644 index 0000000..5d0e43e --- /dev/null +++ b/src/controllers/AuthControler.ts @@ -0,0 +1,52 @@ +import jwt from "jsonwebtoken"; +import AppModel from "../models/AppModel"; +import { Request, Response, NextFunction } from "express"; + +const authTokenSecret = process.env.JWTSECRET || "badsecret"; + +class AuthControler { + async login(req: Request, res: Response) { + // Read app and secret from request body + const { app, secret } = req.body; + + // Filter app from the apps by app and secret + const authenticated = await AppModel.findOne(app, secret); + + if (authenticated) { + console.log("Authenticated app ", authenticated.app); + // Generate an access token + const accessToken = jwt.sign({ app: authenticated.app }, authTokenSecret); + + res.json({ + accessToken, + }); + } else { + res.status(403).send("Credentials incorrect"); + } + } + + authorize(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization; + if (authHeader) { + const token = authHeader.split(" ")[1]; + + jwt.verify(token, authTokenSecret, (err, user) => { + if (err) { + return res.status(403).json("Invalid token provided"); + } + + console.log( + "Authorization provided for ", + next.name, + " to user ", + user + ); + next(); + }); + } else { + res.status(401).json("No Authorization header provided"); + } + } +} + +export default new AuthControler(); diff --git a/src/models/AppModel.ts b/src/models/AppModel.ts new file mode 100644 index 0000000..9ef6535 --- /dev/null +++ b/src/models/AppModel.ts @@ -0,0 +1,19 @@ +import mongoose, { Document } from "mongoose"; + +export interface App extends Document { + app: String, + secret: String +} + +const AppSchema = new mongoose.Schema({ + app: { + type: String, + required: true, + }, + secret: { + type: String, + required: true, + }, +}); + +export default mongoose.model("apps", AppSchema); From 95fd50a638d14c642b6e500e1b41d79dbaa4436a Mon Sep 17 00:00:00 2001 From: Alie Date: Wed, 27 Dec 2023 18:29:20 +0100 Subject: [PATCH 2/5] added auth service --- src/controllers/AuthControler.ts | 4 ++-- src/models/{AppModel.ts => AuthModel.ts} | 6 +++--- src/services/AuthService.ts | 9 +++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) rename src/models/{AppModel.ts => AuthModel.ts} (59%) create mode 100644 src/services/AuthService.ts diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts index 5d0e43e..0d0e95d 100644 --- a/src/controllers/AuthControler.ts +++ b/src/controllers/AuthControler.ts @@ -1,5 +1,5 @@ import jwt from "jsonwebtoken"; -import AppModel from "../models/AppModel"; +import AuthService from "../services/AuthService"; import { Request, Response, NextFunction } from "express"; const authTokenSecret = process.env.JWTSECRET || "badsecret"; @@ -10,7 +10,7 @@ class AuthControler { const { app, secret } = req.body; // Filter app from the apps by app and secret - const authenticated = await AppModel.findOne(app, secret); + const authenticated = await AuthService.find(app, secret); if (authenticated) { console.log("Authenticated app ", authenticated.app); diff --git a/src/models/AppModel.ts b/src/models/AuthModel.ts similarity index 59% rename from src/models/AppModel.ts rename to src/models/AuthModel.ts index 9ef6535..52dc172 100644 --- a/src/models/AppModel.ts +++ b/src/models/AuthModel.ts @@ -1,11 +1,11 @@ import mongoose, { Document } from "mongoose"; -export interface App extends Document { +export interface Auth extends Document { app: String, secret: String } -const AppSchema = new mongoose.Schema({ +const AuthSchema = new mongoose.Schema({ app: { type: String, required: true, @@ -16,4 +16,4 @@ const AppSchema = new mongoose.Schema({ }, }); -export default mongoose.model("apps", AppSchema); +export default mongoose.model("auth", AuthSchema); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..1b4bb62 --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,9 @@ +import AuthModel, { Auth } from "../models/AuthModel"; + +class AuthService { + async find(app: String, secret: String): Promise { + return await AuthModel.findOne(app, secret); + } +} + +export default new AuthService(); From c428e956bc943f609273766b7ef1f8a0aed2c01c Mon Sep 17 00:00:00 2001 From: Alie Date: Wed, 27 Dec 2023 18:44:09 +0100 Subject: [PATCH 3/5] added expiry times to jwt decided against adding refresh tokens due to they not being that usefull in our architecture --- src/controllers/AuthControler.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts index 0d0e95d..e983020 100644 --- a/src/controllers/AuthControler.ts +++ b/src/controllers/AuthControler.ts @@ -2,9 +2,9 @@ import jwt from "jsonwebtoken"; import AuthService from "../services/AuthService"; import { Request, Response, NextFunction } from "express"; -const authTokenSecret = process.env.JWTSECRET || "badsecret"; - class AuthControler { + authTokenSecret = process.env.JWTSECRET || "badsecret"; + async login(req: Request, res: Response) { // Read app and secret from request body const { app, secret } = req.body; @@ -15,7 +15,11 @@ class AuthControler { if (authenticated) { console.log("Authenticated app ", authenticated.app); // Generate an access token - const accessToken = jwt.sign({ app: authenticated.app }, authTokenSecret); + const accessToken = jwt.sign( + { app: authenticated.app }, + this.authTokenSecret, + { expiresIn: "1h" } + ); res.json({ accessToken, @@ -30,17 +34,12 @@ class AuthControler { if (authHeader) { const token = authHeader.split(" ")[1]; - jwt.verify(token, authTokenSecret, (err, user) => { + jwt.verify(token, this.authTokenSecret, (err, app) => { if (err) { return res.status(403).json("Invalid token provided"); } - console.log( - "Authorization provided for ", - next.name, - " to user ", - user - ); + console.log("Authorization provided for ", next.name, " to app ", app); next(); }); } else { From 8396d597f196ed26c364321d9914f3de2caf3727 Mon Sep 17 00:00:00 2001 From: Alie Date: Wed, 27 Dec 2023 20:03:22 +0100 Subject: [PATCH 4/5] fixed mongo issues, why does it need to end with s??? --- compose.yaml | 1 + mongo-init.js | 42 ++++++++++++++++++-------------- src/controllers/AuthControler.ts | 10 ++++---- src/index.ts | 2 ++ src/models/AuthModel.ts | 2 +- src/services/AuthService.ts | 3 ++- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/compose.yaml b/compose.yaml index 68d1912..ecec3f3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -27,6 +27,7 @@ services: MONGODB_URI: "mongodb://mongodb:27017/bot" MONGODB_USER: "root" MONGODB_PASS: "password" + JWTSECRET: "cooljwtsecret" volumes: - ./:/usr/src/app:ro diff --git a/mongo-init.js b/mongo-init.js index a7b191b..67b53c9 100644 --- a/mongo-init.js +++ b/mongo-init.js @@ -1,24 +1,30 @@ db.createUser({ - user: 'root', - pwd: 'password', - roles: [ - { - role: 'readWrite', - db: 'admin', - }, - { - role: 'readWrite', - db: 'bot', - }, - ], + user: "root", + pwd: "password", + roles: [ + { + role: "readWrite", + db: "admin", + }, + { + role: "readWrite", + db: "bot", + }, + ], }); db = new Mongo().getDB("bot"); -db.images.createIndex({ "status": 1 }); -db.images.createIndex({ "url": 1 }, { "unique": true }); +db.images.createIndex({ status: 1 }); +db.images.createIndex({ url: 1 }, { unique: true }); db.images.insert({ - url: "https://example.com", - status: "consumed", - tags: ["2girls", "sleeping"] -}); \ No newline at end of file + url: "https://example.com", + status: "consumed", + tags: ["2girls", "sleeping"], +}); + +db.authorizations.createIndex({ app: 1 }); +db.authorizations.insert({ + app: "tester", + secret: "test", +}); diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts index e983020..072638e 100644 --- a/src/controllers/AuthControler.ts +++ b/src/controllers/AuthControler.ts @@ -2,9 +2,9 @@ import jwt from "jsonwebtoken"; import AuthService from "../services/AuthService"; import { Request, Response, NextFunction } from "express"; -class AuthControler { - authTokenSecret = process.env.JWTSECRET || "badsecret"; +const authTokenSecret = process.env.JWTSECRET || "badsecret"; +class AuthControler { async login(req: Request, res: Response) { // Read app and secret from request body const { app, secret } = req.body; @@ -17,12 +17,12 @@ class AuthControler { // Generate an access token const accessToken = jwt.sign( { app: authenticated.app }, - this.authTokenSecret, + authTokenSecret, { expiresIn: "1h" } ); res.json({ - accessToken, + token: accessToken, }); } else { res.status(403).send("Credentials incorrect"); @@ -34,7 +34,7 @@ class AuthControler { if (authHeader) { const token = authHeader.split(" ")[1]; - jwt.verify(token, this.authTokenSecret, (err, app) => { + jwt.verify(token, authTokenSecret, (err, app) => { if (err) { return res.status(403).json("Invalid token provided"); } diff --git a/src/index.ts b/src/index.ts index 9dff064..d48f3fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import express from "express"; import mongoose from "mongoose"; import listEndpoints from "express-list-endpoints"; import imageController from "./controllers/ImageController"; +import authControler from "./controllers/AuthControler"; export const app = express(); @@ -14,6 +15,7 @@ app.get("/", (_, res) => { app.get("/images", imageController.getAllImages); app.post("/images", imageController.addImage); +app.post("/login", authControler.login) // Set the default port to 8080, or use the PORT environment variable diff --git a/src/models/AuthModel.ts b/src/models/AuthModel.ts index 52dc172..394a64b 100644 --- a/src/models/AuthModel.ts +++ b/src/models/AuthModel.ts @@ -16,4 +16,4 @@ const AuthSchema = new mongoose.Schema({ }, }); -export default mongoose.model("auth", AuthSchema); +export default mongoose.model("authorizations", AuthSchema); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 1b4bb62..aed8d12 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -2,7 +2,8 @@ import AuthModel, { Auth } from "../models/AuthModel"; class AuthService { async find(app: String, secret: String): Promise { - return await AuthModel.findOne(app, secret); + const auth = await AuthModel.findOne({ app: app, secret: secret }).exec(); + return auth; } } From ada964c49387994b7070a94d3d38cbebfda086f8 Mon Sep 17 00:00:00 2001 From: Alie Date: Wed, 27 Dec 2023 20:33:25 +0100 Subject: [PATCH 5/5] added tests for Authorization --- src/controllers/AuthControler.ts | 2 - src/index.ts | 2 +- tests/app.test.ts | 171 ++++++++++++++++++++----------- tests/auth.test.ts | 22 ++++ 4 files changed, 132 insertions(+), 65 deletions(-) create mode 100644 tests/auth.test.ts diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts index 072638e..a524f56 100644 --- a/src/controllers/AuthControler.ts +++ b/src/controllers/AuthControler.ts @@ -38,8 +38,6 @@ class AuthControler { if (err) { return res.status(403).json("Invalid token provided"); } - - console.log("Authorization provided for ", next.name, " to app ", app); next(); }); } else { diff --git a/src/index.ts b/src/index.ts index d48f3fe..f396445 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ app.get("/", (_, res) => { }); app.get("/images", imageController.getAllImages); -app.post("/images", imageController.addImage); +app.post("/images", authControler.authorize, imageController.addImage); app.post("/login", authControler.login) // Set the default port to 8080, or use the PORT environment variable diff --git a/tests/app.test.ts b/tests/app.test.ts index 455b873..3d249b9 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -1,88 +1,135 @@ -import { afterEach, describe, expect, it, mock } from "bun:test"; +import { afterEach, beforeAll, describe, expect, it, mock } from "bun:test"; import request from "supertest"; import { app } from "../src"; import imageService from "../src/services/ImageService"; const imageServiceOriginal = imageService; +const tok = await request(app) + .post("/login") + .send({ app: "tester", secret: "test" }); +const token = tok.body.token; afterEach(() => { - mock.restore(); - mock.module("../src/services/ImageService", () => ({ default: imageServiceOriginal })); -}) + mock.restore(); + mock.module("../src/services/ImageService", () => ({ + default: imageServiceOriginal, + })); +}); describe("GET / shows all of the endpoints", async () => { - const res = await request(app).get("/"); + const res = await request(app).get("/"); - it("should be", async () => { - expect(res.body).toHaveProperty("endpoints"); - }); + it("should be", async () => { + expect(res.body).toHaveProperty("endpoints"); + }); - it("should be an array", () => { - expect(Array.isArray(res.body.endpoints)).toBeTrue(); - }) -}) + it("should be an array", () => { + expect(Array.isArray(res.body.endpoints)).toBeTrue(); + }); +}); describe("GET /images works properly", async () => { - const res = await request(app).get("/images"); + const res = await request(app).get("/images"); - it("should be an array", () => { - expect(Array.isArray(res.body.images)).toBeTrue(); - }); + it("should be an array", () => { + expect(Array.isArray(res.body.images)).toBeTrue(); + }); - it("should return a 200", async () => { - expect(res.statusCode).toBe(200); - }); + it("should return a 200", async () => { + expect(res.statusCode).toBe(200); + }); }); describe("POST /images works properly", () => { - it("should return 201 for new image", async () => { - const res = await request(app).post("/images").send({ - url: "https://test.url.com/1", + + it("should return 401 for unauthenticated requests", async () => { + const res = await request(app) + .post("/images") + .send({ + url: "https://test.url.com/0", status: "available", - tags: ["2girls", "touhou"] - }); - expect(res.status).toSatisfy(status => [201].includes(status)); - }); + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(401); + }); - it("should return 409 for a repeated images", async () => { - await request(app).post("/images").send({ - url: "https://test.url.com/2", + it("should return 403 for invalid tokens", async () => { + const res = await request(app) + .post("/images") + .set("authorization", `Bearer token`) + .send({ + url: "https://test.url.com/0", status: "available", - tags: ["2girls", "touhou"] - }); + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(403); + }); - const res = await request(app).post("/images").send({ - url: "https://test.url.com/2", - status: "available", - tags: ["2girls", "touhou"] - }); + it("should return 201 for new image", async () => { + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/1", + status: "available", + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(201); + }); - expect(res.status).toBe(409); - }); + it("should return 409 for a repeated images", async () => { + await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/2", + status: "available", + tags: ["2girls", "touhou"], + }); - it("should return 500 for an error on the service", async () => { - mock.module("../src/services/ImageService", () => ({ - default: { - add: () => { throw new Error("This is an expected testing error"); } - } - })); + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/2", + status: "available", + tags: ["2girls", "touhou"], + }); - const res = await request(app).post("/images").send({ - url: "https://test.url.com/3", - status: "available", - tags: ["2girls", "touhou"] - }); - - expect(res.status).toBe(500); - }); + expect(res.status).toBe(409); + }); - it("should return 400 for malformed requests", async () => { - mock.restore(); - const res = await request(app).post("/images").send({ - url: "https://test.url.com/4", - status: "wrong", - tags: ["2girls", "touhou"] - }); - expect(res.status).toBe(400); - }); -}); \ No newline at end of file + it("should return 500 for an error on the service", async () => { + mock.module("../src/services/ImageService", () => ({ + default: { + add: () => { + throw new Error("This is an expected testing error"); + }, + }, + })); + + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/3", + status: "available", + tags: ["2girls", "touhou"], + }); + + expect(res.status).toBe(500); + }); + + it("should return 400 for malformed requests", async () => { + mock.restore(); + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/4", + status: "wrong", + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(400); + }); +}); diff --git a/tests/auth.test.ts b/tests/auth.test.ts new file mode 100644 index 0000000..6951875 --- /dev/null +++ b/tests/auth.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it, mock } from "bun:test"; +import request from "supertest"; +import { app } from "../src"; + +describe("/login", async () => { + const correctRespose = await request(app).post("/login").send({ + app: "tester", + secret: "test", + }); + it("should return 200 for correct login", () => { + expect(correctRespose.status).toBe(200); + }); + it("should contain a token", () => { + expect(correctRespose.body).toHaveProperty("token"); + }); + + it("should return 403 for invalid credentials", async () => { + const res = await request(app).post("/login").send({}); + expect(res.status).toBe(403); + }); +}); +