Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e1f29ec
feat: add Discord service verification and token generation
joyguptaa May 3, 2025
ee1f6b7
refactor: update test descriptions for discord service authorization
joyguptaa May 3, 2025
36dfab0
fix: handle missing x-service-name header in Discord bot verification
joyguptaa May 3, 2025
c4d290b
fix: correct discord service public key configuration
joyguptaa May 10, 2025
7185388
fix: update discord service private key configuration in token genera…
joyguptaa May 10, 2025
8b2dfd3
fix: use HEADERS constant for service name in Discord bot authorization
joyguptaa May 11, 2025
ca7b4a9
refactor: update description for discord service authorization tests
joyguptaa May 11, 2025
90003e7
fix: update test description for unauthorized token handling in disco…
joyguptaa May 11, 2025
c825d89
fix: update service name in authorization test for cloudflare worker
joyguptaa May 11, 2025
92dad92
fix: remove unused import in authorizeBot test file
joyguptaa May 13, 2025
5ba8059
fix: update verifyDiscordBot tests to remove unused response object
joyguptaa May 13, 2025
1576c5e
fix: update discord service authorization test to check for unauthori…
joyguptaa May 14, 2025
b8a4db4
fix: refactor response handling in discord service authorization tests
joyguptaa May 14, 2025
6de11c4
chore: fix assert statement
pankajjs Jun 1, 2025
ca99392
chore: fix import statement
pankajjs Jun 1, 2025
2599d99
chore: remove duplicate test name
pankajjs Jun 1, 2025
d0e5da0
refactor: add logic to handle discord service verification
pankajjs Jun 4, 2025
042fb17
refactor: remove nested if conditions
pankajjs Jun 5, 2025
b447abd
fix: handle verification based on header
pankajjs Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion constants/bot.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
const CLOUDFLARE_WORKER = "Cloudflare Worker";
const BAD_TOKEN = "BAD.JWT.TOKEN";
const CRON_JOB_HANDLER = "Cron Job Handler";
const DISCORD_SERVICE = "Discord Service";

const Services = {
CLOUDFLARE_WORKER: CLOUDFLARE_WORKER,
CRON_JOB_HANDLER: CRON_JOB_HANDLER,
};

module.exports = { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services };
const DiscordServiceHeader = {
name: "x-service-name"
}

module.exports = { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services, DISCORD_SERVICE, DiscordServiceHeader };
16 changes: 10 additions & 6 deletions middlewares/authorizeBot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const botVerifcation = require("../services/botVerificationService");
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot");
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER, DISCORD_SERVICE, DiscordServiceHeader } = require("../constants/bot");

const verifyCronJob = async (req, res, next) => {
try {
Expand All @@ -18,19 +18,23 @@ const verifyCronJob = async (req, res, next) => {
const verifyDiscordBot = async (req, res, next) => {
try {
const token = req.headers.authorization.split(" ")[1];
const data = botVerifcation.verifyToken(token);
const serviceName = req.headers[DiscordServiceHeader.name] || "";

if (data.name !== CLOUDFLARE_WORKER) {
return res.boom.unauthorized("Unauthorized Bot");
if (serviceName === DISCORD_SERVICE && botVerifcation.verifyDiscordService(token).name === DISCORD_SERVICE) {
return next();
}

return next();
const data = botVerifcation.verifyToken(token);
if (data.name === CLOUDFLARE_WORKER) {
return next();
}

return res.boom.unauthorized("Unauthorized Bot");
} catch (error) {
if (error.message === "invalid token") {
return res.boom.unauthorized("Unauthorized Bot");
}
return res.boom.badRequest("Invalid Request");
}
};

module.exports = { verifyDiscordBot, verifyCronJob };
12 changes: 11 additions & 1 deletion services/botVerificationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ const verifyToken = (token) => {
return jwt.verify(token, config.get("botToken.botPublicKey"), { algorithms: ["RS256"] });
};

/**
* Verifies if the JWT for Discord Service is valid. Throws error in case of signature error or expiry
*
* @param token {String} - JWT to be verified
* @return {Object} - Decode value of JWT
*/
const verifyDiscordService = (token) => {
return jwt.verify(token, config.get("discordService.publicKey"), { algorithms: ["RS256"] });
};

/**
* Verifies if the JWT is valid. Throws error in case of signature error or expiry
*
Expand All @@ -20,4 +30,4 @@ const verifyCronJob = (token) => {
return jwt.verify(token, config.get("cronJobHandler.publicKey"), { algorithms: ["RS256"] });
};

module.exports = { verifyToken, verifyCronJob };
module.exports = { verifyToken, verifyCronJob, verifyDiscordService };
41 changes: 41 additions & 0 deletions test/config/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,47 @@ module.exports = {
"-----END RSA PRIVATE KEY-----",
},

discordService: {
publicKey:
"-----BEGIN PUBLIC KEY-----\n" +
"MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBK3CkprcpAYxme7vtdjpWO\n" +
"gFFjoYsqU3OmhMEty/s1gnW5tgbK4ief4xk+cU+mu3YvjzWudT/SV17tAWxL4Y+G\n" +
"incJwL5gpQwlnw9qOAdRGkpBriQLec7kNVIydZXbUitziy+iSimxNzdDmjvlK9ZG\n" +
"miVLZm+MePbUtgaIpfgd+4bRWzudlITiNmWY7HppLzyBw+037iEICM4kwPPFI+SO\n" +
"GJhpAAmD6vk0MeZk1NeQmyQp/uOPpWmVRzgyK+XVc6AwZHV+/n6xAIT91/DjJlD1\n" +
"N+nS7Sqo3RJ04+KlNRUclzINOC7JBYkKtG7YQ0U9nNLkRrRlON+O6tY4OT86T1O1\n" +
"AgMBAAE=\n" +
"-----END PUBLIC KEY-----",
privateKey:
"-----BEGIN RSA PRIVATE KEY-----\n" +
"MIIEoQIBAAKCAQBK3CkprcpAYxme7vtdjpWOgFFjoYsqU3OmhMEty/s1gnW5tgbK\n" +
"4ief4xk+cU+mu3YvjzWudT/SV17tAWxL4Y+GincJwL5gpQwlnw9qOAdRGkpBriQL\n" +
"ec7kNVIydZXbUitziy+iSimxNzdDmjvlK9ZGmiVLZm+MePbUtgaIpfgd+4bRWzud\n" +
"lITiNmWY7HppLzyBw+037iEICM4kwPPFI+SOGJhpAAmD6vk0MeZk1NeQmyQp/uOP\n" +
"pWmVRzgyK+XVc6AwZHV+/n6xAIT91/DjJlD1N+nS7Sqo3RJ04+KlNRUclzINOC7J\n" +
"BYkKtG7YQ0U9nNLkRrRlON+O6tY4OT86T1O1AgMBAAECggEAAhInHV0ObEuRiOEJ\n" +
"mSP5pTCNj9kHNYuLdn7TrUWoVGmgghu0AmbRO84Xg6+0yWMEOPqYPJRHyLTcDmhs\n" +
"q4i45Lrt4hov6hKGzH+i+IhGQ4sbpMeBfcPH4m5LMNQp6iBSzWZ7Ud0FXD6vy7H3\n" +
"mDZnPhrDj1ttGJC8G1RRx/P3cjTccU3lsae6wNjkXaSveWGgPS3m0x95eOPPwa2C\n" +
"KvVLx+kYr2r0uLF5vHN6H9uWqUTWo1GVX3nO+obapYbtcIqCbGQI4eTkvgq0qG7J\n" +
"Nh5IwYJz0bzYFfSQSRwRz9JaCzFRiP55aZnJgk2um5JdbxYCHpw5E1NV/7OsPXlE\n" +
"e4vGHQKBgQCSD/ZQu/1TeyqBF8RRdl9YtOhVAFJDiHTPFNNz9V8eak+x6hFOOGOf\n" +
"QHnbg0X4meYuilaBwXiEsSswPuVAW87VnRHrR2yyyC8knCMcvii3g9q+ed0+ri2+\n" +
"cslDPaDkcvl98qoZEfv/lk7BA7jPFToLMNfNdoHrZXVezZxetVbsuwKBgQCDNJFB\n" +
"XDxXlkIVkT8ozD/qvyQsDXz/wlOob6AkY0J7IdND5jPCi799Q1O1H7pJu50cAi+O\n" +
"ar5EuFxA8TfTKJnIVJBZFrN0O1un86WhCvB8PjgguxqtmJlEPVveiZXnTTfvXVeq\n" +
"G6+3eU/yRw9VDX61iidbWNc+SbMJ9sFQPKNyTwKBgFoaFqx/CyqwU+wGqUhHaVHj\n" +
"Z17oL9cRGl2UT0y9FMxCcJ8j8UD7cBkRQRq0xDkzVtdm5y5sFthkImxEoE8vU0xa\n" +
"9G7bRKaU7t/6oX5dn+h1Ij9WFbFQ6U8OqDEel13Vvyp+w4drnLRyGGrgzOSSB5hX\n" +
"rQhGDqcTk2/EDq4t1015AoGAWDnv9vhz5x22AFS0GNYHoO25ABpt1Hmy0Y+GKxHH\n" +
"8Y6URpM0ePyJ3kx4rFHSbaRICD58BhNHMGScPFs4A7jIeApNKmr2bxE/F9fhp0H4\n" +
"5kLccT3/uX3kihuMfD8eWvP0yfOFcHC/nutnU+5uo+24J5Dn2CgMTOk4CFoyMack\n" +
"7UcCgYBHdbFcXWGHfEqLJZChRrKhWLxn9jkJ0apvnO1j6c5yiAo3yJkSV5Z9IdAc\n" +
"lgOC/dJBTZLcBtixdERqcJ+o4P7oFRS6hz/9n4s+kkzxXVqEmtJmBQvHUo3I/Qgc\n" +
"Ba+XMCP64pXPC3r1llhKRwIl+6UFn+QlpbxtgQjhbULnSbc7fw==\n" +
"-----END RSA PRIVATE KEY-----",
},

rdsServerlessBot: {
rdsServerLessPublicKey:
"-----BEGIN PUBLIC KEY-----\n" +
Expand Down
129 changes: 128 additions & 1 deletion test/unit/middlewares/authorizeBot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ const authorizeBot = require("../../../middlewares/authorizeBot");
const sinon = require("sinon");
const expect = require("chai").expect;
const bot = require("../../utils/generateBotToken");
const { BAD_TOKEN, CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../../../constants/bot");
const jwt = require("jsonwebtoken");
const {
BAD_TOKEN,
CLOUDFLARE_WORKER,
CRON_JOB_HANDLER,
DISCORD_SERVICE,
DiscordServiceHeader,
} = require("../../../constants/bot");

describe("Middleware | Authorize Bot", function () {
describe("Check authorization of bot", function (done) {
Expand Down Expand Up @@ -115,4 +122,124 @@ describe("Middleware | Authorize Bot", function () {
expect(nextSpy.calledOnce).to.be.equal(true);
});
});

describe("Check authorization for discord service", function () {
let nextSpy, boomBadRequestSpy, boomUnauthorizedSpy;

beforeEach(function () {
nextSpy = sinon.spy();
boomBadRequestSpy = sinon.spy();
boomUnauthorizedSpy = sinon.spy();
});

afterEach(function () {
sinon.restore();
});

it("should return unauthorized when token is invalid for discord service", function () {
const jwtStub = sinon.stub(jwt, "verify").throws(new Error("invalid token"));

const request = {
headers: {
authorization: `Bearer ${BAD_TOKEN}`,
[DiscordServiceHeader.name]: DISCORD_SERVICE,
},
};

const response = {
boom: {
unauthorized: boomUnauthorizedSpy,
},
};

authorizeBot.verifyDiscordBot(request, response, nextSpy);

expect(nextSpy.calledOnce).to.be.equal(false);
expect(boomUnauthorizedSpy.calledOnce).to.be.equal(true);

jwtStub.restore();
});

it("should return bad request when passing bad token in header for discord service", function () {
const request = {
headers: {
authorization: `Bearer BAD_TOKEN`,
[DiscordServiceHeader.name]: DISCORD_SERVICE,
},
};

const response = {
boom: {
badRequest: boomBadRequestSpy,
},
};

authorizeBot.verifyDiscordBot(request, response, nextSpy);
expect(nextSpy.calledOnce).to.be.equal(false);
expect(boomBadRequestSpy.calledOnce).to.be.equal(true);
});

it("should allow request propagation when token is valid for discord service", function () {
const jwtToken = bot.generateDiscordServiceToken({ name: DISCORD_SERVICE });
const request = {
headers: {
authorization: `Bearer ${jwtToken}`,
[DiscordServiceHeader.name]: DISCORD_SERVICE,
},
};

authorizeBot.verifyDiscordBot(request, {}, nextSpy);
expect(nextSpy.calledOnce).to.be.equal(true);
});

it("should allow request propagation when token is valid for cloudflare worker", function () {
const jwtToken = bot.generateToken({ name: CLOUDFLARE_WORKER });
const request = {
headers: {
authorization: `Bearer ${jwtToken}`,
},
};

authorizeBot.verifyDiscordBot(request, {}, nextSpy);
expect(nextSpy.calledOnce).to.be.equal(true);
});

it("should return unauthorized when token is valid but not for discord service", function () {
const jwtToken = bot.generateDiscordServiceToken({ name: "Invalid" });
const request = {
headers: {
authorization: `Bearer ${jwtToken}`,
[DiscordServiceHeader.name]: DISCORD_SERVICE,
},
};

const response = {
boom: {
unauthorized: boomUnauthorizedSpy,
},
};

authorizeBot.verifyDiscordBot(request, response, nextSpy);
expect(nextSpy.calledOnce).to.be.equal(false);
expect(boomUnauthorizedSpy.calledOnce).to.be.equal(true);
});

it("should return unauthorized when token is invalid for cloudflare worker", function () {
const jwtToken = bot.generateToken({ name: "Invalid" });
const request = {
headers: {
authorization: `Bearer ${jwtToken}`,
},
};

const response = {
boom: {
unauthorized: boomUnauthorizedSpy,
},
};
authorizeBot.verifyDiscordBot(request, response, nextSpy);
expect(nextSpy.calledOnce).to.be.equal(false);
expect(boomUnauthorizedSpy.calledOnce).to.be.equal(true);
});
});
});
15 changes: 14 additions & 1 deletion test/utils/generateBotToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ const generateToken = (data) => {
});
};

/**
* Generates the JWT
*
* @param payload {Object} - Payload to be added in the JWT
* @return {String} - Generated JWT
*/
const generateDiscordServiceToken = (data) => {
return jwt.sign(data, config.get("discordService.privateKey"), {
algorithm: "RS256",
expiresIn: "1m",
});
};

const generateCronJobToken = (data) => {
const token = jwt.sign(data, config.get("cronJobHandler.privateKey"), {
algorithm: "RS256",
Expand All @@ -21,4 +34,4 @@ const generateCronJobToken = (data) => {
return token;
};

module.exports = { generateToken, generateCronJobToken };
module.exports = { generateToken, generateCronJobToken, generateDiscordServiceToken };
Loading