Skip to content

Commit d7cef3c

Browse files
joyguptaapankajjs
andauthored
feat: add Discord service verification and token generation (#2419)
* feat: add Discord service verification and token generation * refactor: update test descriptions for discord service authorization * fix: handle missing x-service-name header in Discord bot verification * fix: correct discord service public key configuration * fix: update discord service private key configuration in token generation * fix: use HEADERS constant for service name in Discord bot authorization * refactor: update description for discord service authorization tests * fix: update test description for unauthorized token handling in discord service * fix: update service name in authorization test for cloudflare worker * fix: remove unused import in authorizeBot test file * fix: update verifyDiscordBot tests to remove unused response object * fix: update discord service authorization test to check for unauthorized response * fix: refactor response handling in discord service authorization tests * chore: fix assert statement * chore: fix import statement * chore: remove duplicate test name * refactor: add logic to handle discord service verification * refactor: remove nested if conditions * fix: handle verification based on header --------- Co-authored-by: Pankaj <[email protected]>
1 parent 6369916 commit d7cef3c

File tree

6 files changed

+210
-10
lines changed

6 files changed

+210
-10
lines changed

constants/bot.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
const CLOUDFLARE_WORKER = "Cloudflare Worker";
22
const BAD_TOKEN = "BAD.JWT.TOKEN";
33
const CRON_JOB_HANDLER = "Cron Job Handler";
4+
const DISCORD_SERVICE = "Discord Service";
45

56
const Services = {
67
CLOUDFLARE_WORKER: CLOUDFLARE_WORKER,
78
CRON_JOB_HANDLER: CRON_JOB_HANDLER,
89
};
910

10-
module.exports = { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services };
11+
const DiscordServiceHeader = {
12+
name: "x-service-name"
13+
}
14+
15+
module.exports = { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services, DISCORD_SERVICE, DiscordServiceHeader };

middlewares/authorizeBot.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const botVerifcation = require("../services/botVerificationService");
2-
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot");
2+
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER, DISCORD_SERVICE, DiscordServiceHeader } = require("../constants/bot");
33

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

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

27-
return next();
27+
const data = botVerifcation.verifyToken(token);
28+
if (data.name === CLOUDFLARE_WORKER) {
29+
return next();
30+
}
31+
32+
return res.boom.unauthorized("Unauthorized Bot");
2833
} catch (error) {
2934
if (error.message === "invalid token") {
3035
return res.boom.unauthorized("Unauthorized Bot");
3136
}
3237
return res.boom.badRequest("Invalid Request");
3338
}
3439
};
35-
3640
module.exports = { verifyDiscordBot, verifyCronJob };

services/botVerificationService.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ const verifyToken = (token) => {
1010
return jwt.verify(token, config.get("botToken.botPublicKey"), { algorithms: ["RS256"] });
1111
};
1212

13+
/**
14+
* Verifies if the JWT for Discord Service is valid. Throws error in case of signature error or expiry
15+
*
16+
* @param token {String} - JWT to be verified
17+
* @return {Object} - Decode value of JWT
18+
*/
19+
const verifyDiscordService = (token) => {
20+
return jwt.verify(token, config.get("discordService.publicKey"), { algorithms: ["RS256"] });
21+
};
22+
1323
/**
1424
* Verifies if the JWT is valid. Throws error in case of signature error or expiry
1525
*
@@ -20,4 +30,4 @@ const verifyCronJob = (token) => {
2030
return jwt.verify(token, config.get("cronJobHandler.publicKey"), { algorithms: ["RS256"] });
2131
};
2232

23-
module.exports = { verifyToken, verifyCronJob };
33+
module.exports = { verifyToken, verifyCronJob, verifyDiscordService };

test/config/test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,47 @@ module.exports = {
153153
"-----END RSA PRIVATE KEY-----",
154154
},
155155

156+
discordService: {
157+
publicKey:
158+
"-----BEGIN PUBLIC KEY-----\n" +
159+
"MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBK3CkprcpAYxme7vtdjpWO\n" +
160+
"gFFjoYsqU3OmhMEty/s1gnW5tgbK4ief4xk+cU+mu3YvjzWudT/SV17tAWxL4Y+G\n" +
161+
"incJwL5gpQwlnw9qOAdRGkpBriQLec7kNVIydZXbUitziy+iSimxNzdDmjvlK9ZG\n" +
162+
"miVLZm+MePbUtgaIpfgd+4bRWzudlITiNmWY7HppLzyBw+037iEICM4kwPPFI+SO\n" +
163+
"GJhpAAmD6vk0MeZk1NeQmyQp/uOPpWmVRzgyK+XVc6AwZHV+/n6xAIT91/DjJlD1\n" +
164+
"N+nS7Sqo3RJ04+KlNRUclzINOC7JBYkKtG7YQ0U9nNLkRrRlON+O6tY4OT86T1O1\n" +
165+
"AgMBAAE=\n" +
166+
"-----END PUBLIC KEY-----",
167+
privateKey:
168+
"-----BEGIN RSA PRIVATE KEY-----\n" +
169+
"MIIEoQIBAAKCAQBK3CkprcpAYxme7vtdjpWOgFFjoYsqU3OmhMEty/s1gnW5tgbK\n" +
170+
"4ief4xk+cU+mu3YvjzWudT/SV17tAWxL4Y+GincJwL5gpQwlnw9qOAdRGkpBriQL\n" +
171+
"ec7kNVIydZXbUitziy+iSimxNzdDmjvlK9ZGmiVLZm+MePbUtgaIpfgd+4bRWzud\n" +
172+
"lITiNmWY7HppLzyBw+037iEICM4kwPPFI+SOGJhpAAmD6vk0MeZk1NeQmyQp/uOP\n" +
173+
"pWmVRzgyK+XVc6AwZHV+/n6xAIT91/DjJlD1N+nS7Sqo3RJ04+KlNRUclzINOC7J\n" +
174+
"BYkKtG7YQ0U9nNLkRrRlON+O6tY4OT86T1O1AgMBAAECggEAAhInHV0ObEuRiOEJ\n" +
175+
"mSP5pTCNj9kHNYuLdn7TrUWoVGmgghu0AmbRO84Xg6+0yWMEOPqYPJRHyLTcDmhs\n" +
176+
"q4i45Lrt4hov6hKGzH+i+IhGQ4sbpMeBfcPH4m5LMNQp6iBSzWZ7Ud0FXD6vy7H3\n" +
177+
"mDZnPhrDj1ttGJC8G1RRx/P3cjTccU3lsae6wNjkXaSveWGgPS3m0x95eOPPwa2C\n" +
178+
"KvVLx+kYr2r0uLF5vHN6H9uWqUTWo1GVX3nO+obapYbtcIqCbGQI4eTkvgq0qG7J\n" +
179+
"Nh5IwYJz0bzYFfSQSRwRz9JaCzFRiP55aZnJgk2um5JdbxYCHpw5E1NV/7OsPXlE\n" +
180+
"e4vGHQKBgQCSD/ZQu/1TeyqBF8RRdl9YtOhVAFJDiHTPFNNz9V8eak+x6hFOOGOf\n" +
181+
"QHnbg0X4meYuilaBwXiEsSswPuVAW87VnRHrR2yyyC8knCMcvii3g9q+ed0+ri2+\n" +
182+
"cslDPaDkcvl98qoZEfv/lk7BA7jPFToLMNfNdoHrZXVezZxetVbsuwKBgQCDNJFB\n" +
183+
"XDxXlkIVkT8ozD/qvyQsDXz/wlOob6AkY0J7IdND5jPCi799Q1O1H7pJu50cAi+O\n" +
184+
"ar5EuFxA8TfTKJnIVJBZFrN0O1un86WhCvB8PjgguxqtmJlEPVveiZXnTTfvXVeq\n" +
185+
"G6+3eU/yRw9VDX61iidbWNc+SbMJ9sFQPKNyTwKBgFoaFqx/CyqwU+wGqUhHaVHj\n" +
186+
"Z17oL9cRGl2UT0y9FMxCcJ8j8UD7cBkRQRq0xDkzVtdm5y5sFthkImxEoE8vU0xa\n" +
187+
"9G7bRKaU7t/6oX5dn+h1Ij9WFbFQ6U8OqDEel13Vvyp+w4drnLRyGGrgzOSSB5hX\n" +
188+
"rQhGDqcTk2/EDq4t1015AoGAWDnv9vhz5x22AFS0GNYHoO25ABpt1Hmy0Y+GKxHH\n" +
189+
"8Y6URpM0ePyJ3kx4rFHSbaRICD58BhNHMGScPFs4A7jIeApNKmr2bxE/F9fhp0H4\n" +
190+
"5kLccT3/uX3kihuMfD8eWvP0yfOFcHC/nutnU+5uo+24J5Dn2CgMTOk4CFoyMack\n" +
191+
"7UcCgYBHdbFcXWGHfEqLJZChRrKhWLxn9jkJ0apvnO1j6c5yiAo3yJkSV5Z9IdAc\n" +
192+
"lgOC/dJBTZLcBtixdERqcJ+o4P7oFRS6hz/9n4s+kkzxXVqEmtJmBQvHUo3I/Qgc\n" +
193+
"Ba+XMCP64pXPC3r1llhKRwIl+6UFn+QlpbxtgQjhbULnSbc7fw==\n" +
194+
"-----END RSA PRIVATE KEY-----",
195+
},
196+
156197
rdsServerlessBot: {
157198
rdsServerLessPublicKey:
158199
"-----BEGIN PUBLIC KEY-----\n" +

test/unit/middlewares/authorizeBot.test.js

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ const authorizeBot = require("../../../middlewares/authorizeBot");
22
const sinon = require("sinon");
33
const expect = require("chai").expect;
44
const bot = require("../../utils/generateBotToken");
5-
const { BAD_TOKEN, CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../../../constants/bot");
5+
const jwt = require("jsonwebtoken");
6+
const {
7+
BAD_TOKEN,
8+
CLOUDFLARE_WORKER,
9+
CRON_JOB_HANDLER,
10+
DISCORD_SERVICE,
11+
DiscordServiceHeader,
12+
} = require("../../../constants/bot");
613

714
describe("Middleware | Authorize Bot", function () {
815
describe("Check authorization of bot", function (done) {
@@ -115,4 +122,124 @@ describe("Middleware | Authorize Bot", function () {
115122
expect(nextSpy.calledOnce).to.be.equal(true);
116123
});
117124
});
125+
126+
describe("Check authorization for discord service", function () {
127+
let nextSpy, boomBadRequestSpy, boomUnauthorizedSpy;
128+
129+
beforeEach(function () {
130+
nextSpy = sinon.spy();
131+
boomBadRequestSpy = sinon.spy();
132+
boomUnauthorizedSpy = sinon.spy();
133+
});
134+
135+
afterEach(function () {
136+
sinon.restore();
137+
});
138+
139+
it("should return unauthorized when token is invalid for discord service", function () {
140+
const jwtStub = sinon.stub(jwt, "verify").throws(new Error("invalid token"));
141+
142+
const request = {
143+
headers: {
144+
authorization: `Bearer ${BAD_TOKEN}`,
145+
[DiscordServiceHeader.name]: DISCORD_SERVICE,
146+
},
147+
};
148+
149+
const response = {
150+
boom: {
151+
unauthorized: boomUnauthorizedSpy,
152+
},
153+
};
154+
155+
authorizeBot.verifyDiscordBot(request, response, nextSpy);
156+
157+
expect(nextSpy.calledOnce).to.be.equal(false);
158+
expect(boomUnauthorizedSpy.calledOnce).to.be.equal(true);
159+
160+
jwtStub.restore();
161+
});
162+
163+
it("should return bad request when passing bad token in header for discord service", function () {
164+
const request = {
165+
headers: {
166+
authorization: `Bearer BAD_TOKEN`,
167+
[DiscordServiceHeader.name]: DISCORD_SERVICE,
168+
},
169+
};
170+
171+
const response = {
172+
boom: {
173+
badRequest: boomBadRequestSpy,
174+
},
175+
};
176+
177+
authorizeBot.verifyDiscordBot(request, response, nextSpy);
178+
expect(nextSpy.calledOnce).to.be.equal(false);
179+
expect(boomBadRequestSpy.calledOnce).to.be.equal(true);
180+
});
181+
182+
it("should allow request propagation when token is valid for discord service", function () {
183+
const jwtToken = bot.generateDiscordServiceToken({ name: DISCORD_SERVICE });
184+
const request = {
185+
headers: {
186+
authorization: `Bearer ${jwtToken}`,
187+
[DiscordServiceHeader.name]: DISCORD_SERVICE,
188+
},
189+
};
190+
191+
authorizeBot.verifyDiscordBot(request, {}, nextSpy);
192+
expect(nextSpy.calledOnce).to.be.equal(true);
193+
});
194+
195+
it("should allow request propagation when token is valid for cloudflare worker", function () {
196+
const jwtToken = bot.generateToken({ name: CLOUDFLARE_WORKER });
197+
const request = {
198+
headers: {
199+
authorization: `Bearer ${jwtToken}`,
200+
},
201+
};
202+
203+
authorizeBot.verifyDiscordBot(request, {}, nextSpy);
204+
expect(nextSpy.calledOnce).to.be.equal(true);
205+
});
206+
207+
it("should return unauthorized when token is valid but not for discord service", function () {
208+
const jwtToken = bot.generateDiscordServiceToken({ name: "Invalid" });
209+
const request = {
210+
headers: {
211+
authorization: `Bearer ${jwtToken}`,
212+
[DiscordServiceHeader.name]: DISCORD_SERVICE,
213+
},
214+
};
215+
216+
const response = {
217+
boom: {
218+
unauthorized: boomUnauthorizedSpy,
219+
},
220+
};
221+
222+
authorizeBot.verifyDiscordBot(request, response, nextSpy);
223+
expect(nextSpy.calledOnce).to.be.equal(false);
224+
expect(boomUnauthorizedSpy.calledOnce).to.be.equal(true);
225+
});
226+
227+
it("should return unauthorized when token is invalid for cloudflare worker", function () {
228+
const jwtToken = bot.generateToken({ name: "Invalid" });
229+
const request = {
230+
headers: {
231+
authorization: `Bearer ${jwtToken}`,
232+
},
233+
};
234+
235+
const response = {
236+
boom: {
237+
unauthorized: boomUnauthorizedSpy,
238+
},
239+
};
240+
authorizeBot.verifyDiscordBot(request, response, nextSpy);
241+
expect(nextSpy.calledOnce).to.be.equal(false);
242+
expect(boomUnauthorizedSpy.calledOnce).to.be.equal(true);
243+
});
244+
});
118245
});

test/utils/generateBotToken.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ const generateToken = (data) => {
1313
});
1414
};
1515

16+
/**
17+
* Generates the JWT
18+
*
19+
* @param payload {Object} - Payload to be added in the JWT
20+
* @return {String} - Generated JWT
21+
*/
22+
const generateDiscordServiceToken = (data) => {
23+
return jwt.sign(data, config.get("discordService.privateKey"), {
24+
algorithm: "RS256",
25+
expiresIn: "1m",
26+
});
27+
};
28+
1629
const generateCronJobToken = (data) => {
1730
const token = jwt.sign(data, config.get("cronJobHandler.privateKey"), {
1831
algorithm: "RS256",
@@ -21,4 +34,4 @@ const generateCronJobToken = (data) => {
2134
return token;
2235
};
2336

24-
module.exports = { generateToken, generateCronJobToken };
37+
module.exports = { generateToken, generateCronJobToken, generateDiscordServiceToken };

0 commit comments

Comments
 (0)