Endpoint schemas/Improved Auth Middleware (#2411) by Bruception

* Lots of stuff

* Changed code order

* Change message

* Use strict comparison

* Fix Bearer auth

* changed failed validation message

* removed full stops

Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
Bruce Berrios 2022-02-04 15:18:22 -05:00 committed by GitHub
parent 0966a14316
commit 957b4cf1a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 235 additions and 152 deletions

View file

@ -2,16 +2,18 @@ const ConfigDAO = require("../../dao/config");
const { validateConfig } = require("../../handlers/validation");
class ConfigController {
static async getConfig(req, res) {
const { uid } = req.decodedToken;
let config = await ConfigDAO.getConfig(uid);
return res.status(200).json(config);
static async getConfig(req, _res) {
const { uid } = req.ctx.decodedToken;
return await ConfigDAO.getConfig(uid);
}
static async saveConfig(req, res) {
const { config } = req.body;
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
validateConfig(config);
await ConfigDAO.saveConfig(uid, config);
return res.sendStatus(200);
}
}

View file

@ -1,49 +1,40 @@
const _ = require("lodash");
const LeaderboardsDAO = require("../../dao/leaderboards");
const { verifyIdToken } = require("../../handlers/auth");
class LeaderboardsController {
static async get(req, res) {
static async get(req, _res) {
const { language, mode, mode2, skip, limit } = req.query;
const { uid } = req.ctx.decodedToken;
let uid;
const leaderboard = await LeaderboardsDAO.get(
mode,
mode2,
language,
skip,
limit
);
const { authorization } = req.headers;
if (authorization) {
const token = authorization.split(" ");
if (token[0].trim() == "Bearer")
req.decodedToken = await verifyIdToken(token[1]);
uid = req.decodedToken.uid;
}
if (!language || !mode || !mode2 || !skip) {
return res.status(400).json({
message: "Missing parameters",
});
}
let retval = await LeaderboardsDAO.get(mode, mode2, language, skip, limit);
retval.forEach((item) => {
if (uid && item.uid == uid) {
//
} else {
delete item.discordId;
delete item.uid;
delete item.difficulty;
delete item.language;
}
const normalizedLeaderboard = _.map(leaderboard, (entry) => {
return uid && entry.uid === uid
? entry
: _.omit(entry, ["discordId", "uid", "difficulty", "language"]);
});
return res.status(200).json(retval);
return normalizedLeaderboard;
}
static async getRank(req, res) {
const { language, mode, mode2 } = req.query;
const { uid } = req.decodedToken;
if (!language || !mode || !mode2 || !uid) {
const { uid } = req.ctx.decodedToken;
if (!uid) {
return res.status(400).json({
message: "Missing parameters",
message: "Missing user id.",
});
}
let retval = await LeaderboardsDAO.getRank(mode, mode2, language, uid);
return res.status(200).json(retval);
return await LeaderboardsDAO.getRank(mode, mode2, language, uid);
}
}

View file

@ -6,7 +6,7 @@ const Captcha = require("../../handlers/captcha");
class NewQuotesController {
static async getQuotes(req, _res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const userInfo = await UserDAO.getUser(uid);
if (!userInfo.quoteMod) {
throw new MonkeyError(403, "You don't have permission to do this");
@ -15,7 +15,7 @@ class NewQuotesController {
}
static async addQuote(req, _res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { text, source, language, captcha } = req.body;
if (!(await Captcha.verify(captcha))) {
throw new MonkeyError(400, "Captcha check failed");
@ -24,7 +24,7 @@ class NewQuotesController {
}
static async approve(req, _res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { quoteId, editText, editSource } = req.body;
const userInfo = await UserDAO.getUser(uid);
if (!userInfo.quoteMod) {
@ -37,7 +37,7 @@ class NewQuotesController {
}
static async refuse(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { quoteId } = req.body;
await NewQuotesDAO.refuse(quoteId, uid);

View file

@ -7,35 +7,33 @@ const MonkeyError = require("../../handlers/error");
class PresetController {
static async getPresets(req, res) {
const { uid } = req.decodedToken;
let presets = await PresetDAO.getPresets(uid);
return res.status(200).json(presets);
const { uid } = req.ctx.decodedToken;
return await PresetDAO.getPresets(uid);
}
static async addPreset(req, res) {
const { name, config } = req.body;
const { uid } = req.decodedToken;
if (!isTagPresetNameValid(name))
throw new MonkeyError(400, "Invalid preset name.");
validateConfig(config);
let preset = await PresetDAO.addPreset(uid, name, config);
return res.status(200).json(preset);
const { uid } = req.ctx.decodedToken;
return await PresetDAO.addPreset(uid, name, config);
}
static async editPreset(req, res) {
const { _id, name, config } = req.body;
const { uid } = req.decodedToken;
if (!isTagPresetNameValid(name))
throw new MonkeyError(400, "Invalid preset name.");
if (config) validateConfig(config);
const { uid } = req.ctx.decodedToken;
await PresetDAO.editPreset(uid, _id, name, config);
return res.sendStatus(200);
}
static async removePreset(req, res) {
const { _id } = req.body;
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
await PresetDAO.removePreset(uid, _id);
return res.sendStatus(200);
}
}

View file

@ -9,7 +9,7 @@ class QuoteRatingsController {
}
static async submitRating(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
let { quoteId, rating, language } = req.body;
quoteId = parseInt(quoteId);

View file

@ -7,7 +7,7 @@ const Logger = require("../../handlers/logger");
class QuotesController {
static async reportQuote(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const user = await UserDAO.getUser(uid);
if (user.cannotReport) {

View file

@ -33,27 +33,27 @@ try {
class ResultController {
static async getResults(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const results = await ResultDAO.getResults(uid);
return res.status(200).json(results);
}
static async deleteAll(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
await ResultDAO.deleteAll(uid);
Logger.log("user_results_deleted", "", uid);
return res.sendStatus(200);
}
static async updateTags(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { tags, resultid } = req.body;
await ResultDAO.updateTags(uid, resultid, tags);
return res.sendStatus(200);
}
static async addResult(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { result } = req.body;
result.uid = uid;
if (validateObjectValues(result) > 0)
@ -100,7 +100,7 @@ class ResultController {
let resulthash = result.hash;
delete result.hash;
if (req.context.configuration.resultObjectHashCheck.enabled) {
if (req.ctx.configuration.resultObjectHashCheck.enabled) {
const serverhash = objecthash(result);
if (serverhash !== resulthash) {
Logger.log(

View file

@ -16,14 +16,14 @@ const uaparser = require("ua-parser-js");
class UserController {
static async createNewUser(req, res) {
const { name } = req.body;
const { email, uid } = req.decodedToken;
const { email, uid } = req.ctx.decodedToken;
await UsersDAO.addUser(name, email, uid);
Logger.log("user_created", `${name} ${email}`, uid);
return res.sendStatus(200);
}
static async deleteUser(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const userInfo = await UsersDAO.getUser(uid);
await UsersDAO.deleteUser(uid);
Logger.log("user_deleted", `${userInfo.email} ${userInfo.name}`, uid);
@ -31,7 +31,7 @@ class UserController {
}
static async updateName(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { name } = req.body;
if (!isUsernameValid(name))
return res.status(400).json({
@ -49,7 +49,7 @@ class UserController {
}
static async clearPb(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
await UsersDAO.clearPb(uid);
Logger.log("user_cleared_pbs", "", uid);
return res.sendStatus(200);
@ -69,7 +69,7 @@ class UserController {
}
static async updateEmail(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { newEmail } = req.body;
try {
await UsersDAO.updateEmail(uid, newEmail);
@ -81,7 +81,7 @@ class UserController {
}
static async getUser(req, res) {
const { email, uid } = req.decodedToken;
const { email, uid } = req.ctx.decodedToken;
let userInfo;
try {
userInfo = await UsersDAO.getUser(uid);
@ -126,7 +126,7 @@ class UserController {
}
static async linkDiscord(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
let requser;
try {
@ -174,7 +174,7 @@ class UserController {
}
static async unlinkDiscord(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
let userInfo;
try {
userInfo = await UsersDAO.getUser(uid);
@ -191,7 +191,7 @@ class UserController {
}
static async addTag(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { tagName } = req.body;
if (!isTagPresetNameValid(tagName))
return res.status(400).json({
@ -203,14 +203,14 @@ class UserController {
}
static async clearTagPb(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { tagid } = req.body;
await UsersDAO.removeTagPb(uid, tagid);
return res.sendStatus(200);
}
static async editTag(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { tagid, newname } = req.body;
if (!isTagPresetNameValid(newname))
return res.status(400).json({
@ -222,21 +222,21 @@ class UserController {
}
static async removeTag(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { tagid } = req.body;
await UsersDAO.removeTag(uid, tagid);
return res.sendStatus(200);
}
static async getTags(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
let tags = await UsersDAO.getTags(uid);
if (tags == undefined) tags = [];
return res.status(200).json(tags);
}
static async updateLbMemory(req, res) {
const { uid } = req.decodedToken;
const { uid } = req.ctx.decodedToken;
const { mode, mode2, language, rank } = req.body;
await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank);
return res.sendStatus(200);

View file

@ -13,14 +13,14 @@ const router = Router();
router.get(
"/",
RateLimit.configGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(ConfigController.getConfig)
);
router.post(
"/save",
RateLimit.configUpdate,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
config: configSchema,

View file

@ -1,3 +1,4 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const LeaderboardsController = require("../controllers/leaderboards");
const RateLimit = require("../../middlewares/rate-limit");
@ -13,13 +14,32 @@ const router = Router();
router.get(
"/",
RateLimit.leaderboardsGet,
authenticateRequest({ isPublic: true }),
requestValidation({
query: {
language: joi.string().required(),
mode: joi.string().required(),
mode2: joi.string().required(),
skip: joi.number().min(0).required(),
limit: joi.number(),
},
validationErrorMessage: "Missing parameters",
}),
asyncHandlerWrapper(LeaderboardsController.get)
);
router.get(
"/rank",
RateLimit.leaderboardsGet,
authenticateRequest,
authenticateRequest(),
requestValidation({
query: {
language: joi.string().required(),
mode: joi.string().required(),
mode2: joi.string().required(),
},
validationErrorMessage: "Missing parameters",
}),
asyncHandlerWrapper(LeaderboardsController.getRank)
);

View file

@ -1,6 +1,8 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const PresetController = require("../controllers/preset");
const RateLimit = require("../../middlewares/rate-limit");
const configSchema = require("../schemas/config-schema");
const {
asyncHandlerWrapper,
requestValidation,
@ -10,31 +12,65 @@ const { Router } = require("express");
const router = Router();
const presetNameSchema = joi
.string()
.required()
.regex(/^[0-9a-zA-Z_.-]+$/)
.max(16)
.messages({
"string.pattern.base": "Invalid preset name",
"string.max": "Preset name exceeds maximum of 16 characters",
});
router.get(
"/",
RateLimit.presetsGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(PresetController.getPresets)
);
router.post(
"/add",
RateLimit.presetsAdd,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
name: presetNameSchema,
config: configSchema.keys({
tags: joi.array().items(joi.string()),
}),
},
}),
asyncHandlerWrapper(PresetController.addPreset)
);
router.post(
"/edit",
RateLimit.presetsEdit,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
_id: joi.string().required(),
name: presetNameSchema,
config: configSchema
.keys({
tags: joi.array().items(joi.string()),
})
.allow(null),
},
}),
asyncHandlerWrapper(PresetController.editPreset)
);
router.post(
"/remove",
RateLimit.presetsRemove,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
_id: joi.string().required(),
},
}),
asyncHandlerWrapper(PresetController.removePreset)
);

View file

@ -17,7 +17,7 @@ const quotesRouter = Router();
quotesRouter.get(
"/",
RateLimit.newQuotesGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(NewQuotesController.getQuotes)
);
@ -31,7 +31,7 @@ quotesRouter.post(
"Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.",
}),
RateLimit.newQuotesAdd,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
text: joi.string().min(60).required(),
@ -47,7 +47,7 @@ quotesRouter.post(
quotesRouter.post(
"/approve",
RateLimit.newQuotesAction,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
quoteId: joi.string().required(),
@ -62,7 +62,7 @@ quotesRouter.post(
quotesRouter.post(
"/reject",
RateLimit.newQuotesAction,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
quoteId: joi.string().required(),
@ -74,7 +74,7 @@ quotesRouter.post(
quotesRouter.get(
"/rating",
RateLimit.quoteRatingsGet,
authenticateRequest,
authenticateRequest(),
requestValidation({
query: {
quoteId: joi.string().regex(/^\d+$/).required(),
@ -87,7 +87,7 @@ quotesRouter.get(
quotesRouter.post(
"/rating",
RateLimit.quoteRatingsSubmit,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
quoteId: joi.number().required(),
@ -107,7 +107,7 @@ quotesRouter.post(
invalidMessage: "Quote reporting is unavailable.",
}),
RateLimit.quoteReportSubmit,
authenticateRequest,
authenticateRequest(),
requestValidation({
body: {
quoteId: joi.string().required(),

View file

@ -12,28 +12,28 @@ const router = Router();
router.get(
"/",
RateLimit.resultsGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(ResultController.getResults)
);
router.post(
"/add",
RateLimit.resultsAdd,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(ResultController.addResult)
);
router.post(
"/updateTags",
RateLimit.resultsTagsUpdate,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(ResultController.updateTags)
);
router.post(
"/deleteAll",
RateLimit.resultsDeleteAll,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(ResultController.deleteAll)
);
@ -46,7 +46,7 @@ router.get(
router.post(
"/checkLeaderboardQualification",
RateLimit.resultsLeaderboardQualificationGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(ResultController.checkLeaderboardQualification)
);

View file

@ -12,14 +12,14 @@ const router = Router();
router.get(
"/",
RateLimit.userGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.getUser)
);
router.post(
"/signup",
RateLimit.userSignup,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.createNewUser)
);
@ -32,84 +32,84 @@ router.post(
router.post(
"/delete",
RateLimit.userDelete,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.deleteUser)
);
router.post(
"/updateName",
RateLimit.userUpdateName,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.updateName)
);
router.post(
"/updateLbMemory",
RateLimit.userUpdateLBMemory,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.updateLbMemory)
);
router.post(
"/updateEmail",
RateLimit.userUpdateEmail,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.updateEmail)
);
router.post(
"/clearPb",
RateLimit.userClearPB,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.clearPb)
);
router.post(
"/tags/add",
RateLimit.userTagsAdd,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.addTag)
);
router.get(
"/tags",
RateLimit.userTagsGet,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.getTags)
);
router.post(
"/tags/clearPb",
RateLimit.userTagsClearPB,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.clearTagPb)
);
router.post(
"/tags/remove",
RateLimit.userTagsRemove,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.removeTag)
);
router.post(
"/tags/edit",
RateLimit.userTagsEdit,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.editTag)
);
router.post(
"/discord/link",
RateLimit.userDiscordLink,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.linkDiscord)
);
router.post(
"/discord/unlink",
RateLimit.userDiscordUnlink,
authenticateRequest,
authenticateRequest(),
asyncHandlerWrapper(UserController.unlinkDiscord)
);

View file

@ -10,7 +10,7 @@ function validateConfiguration(options) {
const { criteria, invalidMessage } = options;
return (req, res, next) => {
const configuration = req.context.configuration;
const configuration = req.ctx.configuration;
const validated = criteria(configuration);
if (!validated) {
@ -78,7 +78,7 @@ function requestValidation(validationSchema) {
throw new MonkeyError(
500,
validationErrorMessage ??
`Invalid request: ${errorMessage} (value ${error.details[0].context.value})`
`${errorMessage} (${error.details[0].context.value})`
);
}
});

View file

@ -1,47 +1,83 @@
const MonkeyError = require("../handlers/error");
const { verifyIdToken } = require("../handlers/auth");
module.exports = {
async authenticateRequest(req, res, next) {
const DEFAULT_OPTIONS = {
isPublic: false,
};
function authenticateRequest(options = DEFAULT_OPTIONS) {
return async (req, _res, next) => {
try {
if (process.env.MODE === "dev" && !req.headers.authorization) {
if (req.body.uid) {
req.decodedToken = {
uid: req.body.uid,
};
console.log("Running authorization in dev mode");
return next();
} else {
throw new MonkeyError(
400,
"Running authorization in dev mode but still no uid was provided"
);
}
}
const { authorization } = req.headers;
if (!authorization)
const { authorization: authHeader } = req.headers;
let token = null;
if (authHeader) {
token = await authenticateWithAuthHeader(authHeader);
} else if (options.isPublic) {
return next();
} else if (process.env.MODE === "dev") {
token = authenticateWithBody(req.body);
} else {
throw new MonkeyError(
401,
"Unauthorized",
`endpoint: ${req.baseUrl} no authorization header found`
);
const token = authorization.split(" ");
if (token[0].trim() !== "Bearer")
return next(
new MonkeyError(400, "Invalid Token", "Incorrect token type")
);
try {
req.decodedToken = await verifyIdToken(token[1]);
} catch (err) {
if (err.message == "auth/id-token-expired") {
new MonkeyError(401, "Unauthorized", "Token expired");
} else {
throw err;
}
}
return next();
} catch (e) {
return next(e);
req.ctx.decodedToken = token;
} catch (error) {
return next(error);
}
},
next();
};
}
function authenticateWithBody(body) {
const { uid } = body;
if (!uid) {
throw new MonkeyError(
400,
"Running authorization in dev mode but still no uid was provided"
);
}
return {
uid,
};
}
async function authenticateWithAuthHeader(authHeader) {
const token = authHeader.split(" ");
const authScheme = token[0].trim();
const credentials = token[1];
if (authScheme === "Bearer") {
return await authenticateWithBearerToken(credentials);
}
throw new MonkeyError(
400,
"Unknown authentication scheme",
`The authentication scheme "${authScheme}" is not implemented.`
);
}
async function authenticateWithBearerToken(token) {
try {
return await verifyIdToken(token);
} catch (error) {
if (error.message.includes("auth/id-token-expired")) {
throw new MonkeyError(401, "Unauthorized", "Token expired");
} else {
throw error;
}
}
}
module.exports = {
authenticateRequest,
};

View file

@ -3,8 +3,11 @@ const ConfigurationDAO = require("../dao/configuration");
async function contextMiddleware(req, res, next) {
const configuration = await ConfigurationDAO.getCachedConfiguration(true);
req.context = {
req.ctx = {
configuration,
decodedToken: {
uid: null,
},
};
next();

View file

@ -26,10 +26,7 @@ app.set("trust proxy", 1);
app.use(contextMiddleware);
app.use((req, res, next) => {
if (
process.env.MAINTENANCE === "true" ||
req.context.configuration.maintenance
) {
if (process.env.MAINTENANCE === "true" || req.ctx.configuration.maintenance) {
res.status(503).json({ message: "Server is down for maintenance" });
} else {
next();
@ -53,8 +50,8 @@ app.use(function (e, req, res, _next) {
//its a server error
monkeyError = new MonkeyError(e.status, e.message, e.stack);
}
if (!monkeyError.uid && req.decodedToken) {
monkeyError.uid = req.decodedToken.uid;
if (!monkeyError.uid && req.ctx?.decodedToken) {
monkeyError.uid = req.ctx.decodedToken.uid;
}
if (process.env.MODE !== "dev" && monkeyError.status > 400) {
Logger.log(