impr: replace body based authorization in dev mode (fehmer) (#4821)

* impr: add authorization header Uid in favour of authorization on with body on dev (fehmer)

* refactor dev mode detection
This commit is contained in:
Christian Fehmer 2023-11-30 13:58:28 +01:00 committed by GitHub
parent e426cb3fc2
commit 3adbdf2cdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 134 additions and 68 deletions

View file

@ -56,6 +56,7 @@ describe("user controller test", () => {
await mockApp
.post("/users/signup")
.set("authorization", "Uid 123456789|newuser@mail.com")
.send(newUser)
.set({
Accept: "application/json",
@ -64,9 +65,8 @@ describe("user controller test", () => {
const response = await mockApp
.get("/users")
.send({
uid: "123456789",
})
.set("authorization", "Uid 123456789")
.send()
.set({
Accept: "application/json",
})

View file

@ -6,6 +6,8 @@ import { getCachedConfiguration } from "../../src/init/configuration";
import * as ApeKeys from "../../src/dal/ape-keys";
import { ObjectId } from "mongodb";
import { hashSync } from "bcrypt";
import MonkeyError from "../../src/utils/error";
import * as Misc from "../../src/utils/misc";
const mockDecodedToken: DecodedIdToken = {
uid: "123456789",
@ -28,6 +30,7 @@ const mockApeKey = {
};
jest.spyOn(ApeKeys, "getApeKey").mockResolvedValue(mockApeKey);
jest.spyOn(ApeKeys, "updateLastUsedOn").mockResolvedValue();
const isDevModeMock = jest.spyOn(Misc, "isDevEnvironment");
describe("middlewares/auth", () => {
let mockRequest: Partial<MonkeyTypes.Request>;
@ -35,6 +38,7 @@ describe("middlewares/auth", () => {
let nextFunction: NextFunction;
beforeEach(async () => {
isDevModeMock.mockReturnValue(true);
let config = await getCachedConfiguration(true);
config.apeKeys.acceptKeys = true;
@ -66,6 +70,10 @@ describe("middlewares/auth", () => {
}) as unknown as NextFunction;
});
afterEach(() => {
isDevModeMock.mockReset();
});
describe("authenticateRequest", () => {
it("should fail if token is not fresh", async () => {
Date.now = jest.fn(() => 60001);
@ -191,5 +199,63 @@ describe("middlewares/auth", () => {
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow request with Uid on dev", async () => {
mockRequest.headers = {
authorization: "Uid 123",
};
const authenticateRequest = Auth.authenticateRequest({});
await authenticateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
const decodedToken = mockRequest?.ctx?.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow request with Uid and email on dev", async () => {
mockRequest.headers = {
authorization: "Uid 123|test@example.com",
};
const authenticateRequest = Auth.authenticateRequest({});
await authenticateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
const decodedToken = mockRequest?.ctx?.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe("test@example.com");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should fail request with Uid on non-dev", async () => {
isDevModeMock.mockReturnValue(false);
mockRequest.headers = {
authorization: "Uid 123",
};
const authenticateRequest = Auth.authenticateRequest({});
await expect(() =>
authenticateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
)
).rejects.toThrow(
new MonkeyError(401, "Baerer type uid is not supported")
);
});
});
});

View file

@ -11,6 +11,7 @@ import * as PublicDAL from "../../dal/public";
import {
getCurrentDayTimestamp,
getStartOfDayTimestamp,
isDevEnvironment,
mapRange,
roundTo2,
stdDev,
@ -47,7 +48,7 @@ try {
if (anticheatImplemented() === false) throw new Error("undefined");
Logger.success("Anticheat module loaded");
} catch (e) {
if (process.env.MODE === "dev") {
if (isDevEnvironment()) {
Logger.warning(
"No anticheat module found. Continuing in dev mode, results will not be validated."
);
@ -274,7 +275,7 @@ export async function addResult(
throw new MonkeyError(status.code, "Result data doesn't make sense");
}
} else {
if (process.env.MODE !== "dev") {
if (!isDevEnvironment()) {
throw new Error("No anticheat module found");
}
Logger.warning(
@ -373,7 +374,7 @@ export async function addResult(
throw new MonkeyError(status.code, "Possible bot detected");
}
} else {
if (process.env.MODE !== "dev") {
if (!isDevEnvironment()) {
throw new Error("No anticheat module found");
}
Logger.warning(
@ -478,7 +479,7 @@ export async function addResult(
!result.bailedOut &&
user.banned !== true &&
user.lbOptOut !== true &&
(process.env.MODE === "dev" || (user.timeTyping ?? 0) > 7200);
(isDevEnvironment() || (user.timeTyping ?? 0) > 7200);
const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
@ -555,7 +556,7 @@ export async function addResult(
const eligibleForWeeklyXpLeaderboard =
user.banned !== true &&
user.lbOptOut !== true &&
(process.env.MODE === "dev" || (user.timeTyping ?? 0) > 7200);
(isDevEnvironment() || (user.timeTyping ?? 0) > 7200);
const weeklyXpLeaderboard = WeeklyXpLeaderboard.get(
weeklyXpLeaderboardConfig

View file

@ -7,6 +7,7 @@ import * as DiscordUtils from "../../utils/discord";
import {
MILLISECONDS_IN_DAY,
buildAgentLog,
isDevEnvironment,
sanitizeString,
} from "../../utils/misc";
import GeorgeQueue from "../../queues/george-queue";
@ -100,10 +101,9 @@ export async function sendVerificationEmail(
link = await FirebaseAdmin()
.auth()
.generateEmailVerificationLink(email, {
url:
process.env.MODE === "dev"
? "http://localhost:3000"
: "https://monkeytype.com",
url: isDevEnvironment()
? "http://localhost:3000"
: "https://monkeytype.com",
});
} catch (e) {
if (
@ -162,10 +162,9 @@ export async function sendForgotPasswordEmail(
const link = await FirebaseAdmin()
.auth()
.generatePasswordResetLink(email, {
url:
process.env.MODE === "dev"
? "http://localhost:3000"
: "https://monkeytype.com",
url: isDevEnvironment()
? "http://localhost:3000"
: "https://monkeytype.com",
});
await emailQueue.sendForgotPasswordEmail(email, userInfo.name, link);

View file

@ -24,6 +24,7 @@ import {
Router,
static as expressStatic,
} from "express";
import { isDevEnvironment } from "../../utils/misc";
const pathOverride = process.env.API_PATH_OVERRIDE;
const BASE_ROUTE = pathOverride ? `/${pathOverride}` : "";
@ -51,7 +52,7 @@ function addApiRoutes(app: Application): void {
// Cannot be added to the route map because it needs to be added before the maintenance handler
app.use("/configuration", configuration);
if (process.env.MODE === "dev") {
if (isDevEnvironment()) {
//disable csp to allow assets to load from unsecured http
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "");

View file

@ -7,6 +7,7 @@ import {
} from "swagger-ui-express";
import publicSwaggerSpec from "../../documentation/public-swagger.json";
import internalSwaggerSpec from "../../documentation/internal-swagger.json";
import { isDevEnvironment } from "../../utils/misc";
const SWAGGER_UI_OPTIONS = {
customCss: ".swagger-ui .topbar { display: none } .try-out { display: none }",
@ -18,7 +19,7 @@ function addSwaggerMiddlewares(app: Application): void {
getSwaggerMiddleware({
name: "Monkeytype API",
uriPath: "/stats",
authentication: process.env.MODE !== "dev",
authentication: !isDevEnvironment(),
apdexThreshold: 100,
swaggerSpec: internalSwaggerSpec,
onAuthenticate: (_req, username, password) => {

View file

@ -2,6 +2,7 @@ import * as db from "../init/db";
import Logger from "../utils/logger";
import { performance } from "perf_hooks";
import { setLeaderboard } from "../utils/prometheus";
import { isDevEnvironment } from "../utils/misc";
const leaderboardUpdating: { [key: string]: boolean } = {};
@ -111,7 +112,7 @@ export async function update(
$ne: true,
},
timeTyping: {
$gt: process.env.MODE === "dev" ? 0 : 7200,
$gt: isDevEnvironment() ? 0 : 7200,
},
},
},

View file

@ -6,6 +6,7 @@ import mjml2html from "mjml";
import mustache from "mustache";
import { recordEmail } from "../utils/prometheus";
import { EmailTaskContexts, EmailType } from "../queues/email-queue";
import { isDevEnvironment } from "../utils/misc";
interface EmailMetadata {
subject: string;
@ -35,10 +36,10 @@ export async function init(): Promise<void> {
return;
}
const { EMAIL_HOST, EMAIL_USER, EMAIL_PASS, EMAIL_PORT, MODE } = process.env;
const { EMAIL_HOST, EMAIL_USER, EMAIL_PASS, EMAIL_PORT } = process.env;
if (!EMAIL_HOST || !EMAIL_USER || !EMAIL_PASS) {
if (MODE === "dev") {
if (isDevEnvironment()) {
Logger.warning(
"No email client configuration provided. Running without email."
);

View file

@ -3,6 +3,7 @@ import Logger from "../utils/logger";
import { readFileSync, existsSync } from "fs";
import MonkeyError from "../utils/error";
import path from "path";
import { isDevEnvironment } from "../utils/misc";
const SERVICE_ACCOUNT_PATH = path.join(
__dirname,
@ -11,7 +12,7 @@ const SERVICE_ACCOUNT_PATH = path.join(
export function init(): void {
if (!existsSync(SERVICE_ACCOUNT_PATH)) {
if (process.env.MODE === "dev") {
if (isDevEnvironment()) {
Logger.warning(
"Firebase service account key not found! Continuing in dev mode, but authentication will throw errors."
);

View file

@ -3,6 +3,7 @@ import _ from "lodash";
import { join } from "path";
import IORedis from "ioredis";
import Logger from "../utils/logger";
import { isDevEnvironment } from "../utils/misc";
let connection: IORedis.Redis;
let connected = false;
@ -28,10 +29,10 @@ export async function connect(): Promise<void> {
return;
}
const { REDIS_URI, MODE } = process.env;
const { REDIS_URI } = process.env;
if (!REDIS_URI) {
if (MODE === "dev") {
if (isDevEnvironment()) {
Logger.warning("No redis configuration provided. Running without redis.");
return;
}
@ -53,7 +54,7 @@ export async function connect(): Promise<void> {
connected = true;
} catch (error) {
Logger.error(error.message);
if (MODE === "dev") {
if (isDevEnvironment()) {
await connection.quit();
Logger.warning(
`Failed to connect to redis. Continuing in dev mode, running without redis.`

View file

@ -5,8 +5,9 @@ import rateLimit, {
RateLimitRequestHandler,
Options,
} from "express-rate-limit";
import { isDevEnvironment } from "../utils/misc";
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 1 : 1;
const REQUEST_MULTIPLIER = isDevEnvironment() ? 1 : 1;
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
return req?.ctx?.decodedToken?.uid;

View file

@ -5,6 +5,7 @@ import { Response, NextFunction, RequestHandler } from "express";
import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response";
import { getUser } from "../dal/user";
import { isAdmin } from "../dal/admin-uids";
import { isDevEnvironment } from "../utils/misc";
interface ValidationOptions<T> {
criteria: (data: T) => boolean;
@ -132,18 +133,6 @@ interface ValidationSchema {
}
function validateRequest(validationSchema: ValidationSchema): RequestHandler {
/**
* In dev environments, as an alternative to token authentication,
* you can pass the authentication middleware by having a user id in the body.
* Inject the user id into the schema so that validation will not fail.
*/
if (process.env.MODE === "dev") {
validationSchema.body = {
uid: joi.any(),
...(validationSchema.body ?? {}),
};
}
const { validationErrorMessage } = validationSchema;
const normalizedValidationSchema: ValidationSchema = _.omit(
validationSchema,
@ -177,7 +166,7 @@ function validateRequest(validationSchema: ValidationSchema): RequestHandler {
*/
function useInProduction(middlewares: RequestHandler[]): RequestHandler[] {
return middlewares.map((middleware) =>
process.env.MODE === "dev" ? emptyMiddleware : middleware
isDevEnvironment() ? emptyMiddleware : middleware
);
}

View file

@ -2,7 +2,7 @@ import { compare } from "bcrypt";
import { getApeKey, updateLastUsedOn } from "../dal/ape-keys";
import MonkeyError from "../utils/error";
import { verifyIdToken } from "../utils/auth";
import { base64UrlDecode } from "../utils/misc";
import { base64UrlDecode, isDevEnvironment } from "../utils/misc";
import { NextFunction, Response, Handler } from "express";
import statuses from "../constants/monkey-status-codes";
import {
@ -57,8 +57,6 @@ function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler {
uid: "",
email: "",
};
} else if (process.env.MODE === "dev") {
token = authenticateWithBody(req.body);
} else {
throw new MonkeyError(
401,
@ -105,25 +103,6 @@ function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler {
};
}
function authenticateWithBody(
body: MonkeyTypes.Request["body"]
): MonkeyTypes.DecodedToken {
const { uid, email } = body;
if (!uid) {
throw new MonkeyError(
401,
"Running authorization in dev mode but still no uid was provided"
);
}
return {
type: "Bearer",
uid,
email: email ?? "",
};
}
async function authenticateWithAuthHeader(
authHeader: string,
configuration: MonkeyTypes.Configuration,
@ -137,6 +116,8 @@ async function authenticateWithAuthHeader(
return await authenticateWithBearerToken(token, options);
case "ApeKey":
return await authenticateWithApeKey(token, configuration, options);
case "Uid":
return await authenticateWithUid(token);
}
throw new MonkeyError(
@ -257,6 +238,20 @@ async function authenticateWithApeKey(
}
}
async function authenticateWithUid(
token: string
): Promise<MonkeyTypes.DecodedToken> {
if (!isDevEnvironment()) {
throw new MonkeyError(401, "Baerer type uid is not supported");
}
const uidAndEmail = token.split("|");
return {
type: "Bearer",
uid: uidAndEmail[0],
email: uidAndEmail.length > 1 ? uidAndEmail[1] : "",
};
}
function authenticateGithubWebhook(): Handler {
return async (
req: MonkeyTypes.Request,

View file

@ -6,6 +6,7 @@ import { incrementBadAuth } from "./rate-limit";
import { NextFunction, Response } from "express";
import { MonkeyResponse, handleMonkeyResponse } from "../utils/monkey-response";
import { recordClientErrorByVersion } from "../utils/prometheus";
import { isDevEnvironment } from "../utils/misc";
async function errorHandlingMiddleware(
error: Error,
@ -42,7 +43,7 @@ async function errorHandlingMiddleware(
recordClientErrorByVersion(req.headers["x-client-version"] as string);
}
if (process.env.MODE !== "dev" && monkeyResponse.status >= 500) {
if (!isDevEnvironment() && monkeyResponse.status >= 500) {
const { uid, errorId } = monkeyResponse.data;
try {

View file

@ -3,8 +3,9 @@ import MonkeyError from "../utils/error";
import { Response, NextFunction } from "express";
import { RateLimiterMemory } from "rate-limiter-flexible";
import rateLimit, { Options } from "express-rate-limit";
import { isDevEnvironment } from "../utils/misc";
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 100 : 1;
const REQUEST_MULTIPLIER = isDevEnvironment() ? 100 : 1;
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
return (req.headers["cf-connecting-ip"] ||

View file

@ -1,4 +1,5 @@
import fetch from "node-fetch";
import { isDevEnvironment } from "./misc";
interface CaptchaData {
success: boolean;
@ -8,7 +9,7 @@ interface CaptchaData {
}
export async function verify(captcha: string): Promise<boolean> {
if (process.env.MODE === "dev") {
if (isDevEnvironment()) {
return true;
}
const response = await fetch(

View file

@ -1,4 +1,5 @@
import fetch from "node-fetch";
import { isDevEnvironment } from "./misc";
const BASE_URL = "https://discord.com/api";
@ -35,7 +36,7 @@ export async function getDiscordUser(
export function getOauthLink(): string {
return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${
process.env.MODE === "dev"
isDevEnvironment()
? `http%3A%2F%2Flocalhost%3A3000%2Fverify`
: `https%3A%2F%2Fmonkeytype.com%2Fverify`
}&response_type=token&scope=identify`;

View file

@ -1,4 +1,5 @@
import { v4 as uuidv4 } from "uuid";
import { isDevEnvironment } from "./misc";
class MonkeyError extends Error {
status: number;
@ -12,7 +13,7 @@ class MonkeyError extends Error {
this.stack = stack;
this.uid = uid;
if (process.env.MODE === "dev") {
if (isDevEnvironment()) {
this.message = stack
? String(message) + "\nStack: " + String(stack)
: String(message);

View file

@ -298,3 +298,7 @@ export function stringToNumberOrDefault(
if (!Number.isFinite(value)) return defaultValue;
return value;
}
export function isDevEnvironment(): boolean {
return process.env.MODE === "dev";
}

View file

@ -1,5 +1,5 @@
import { join } from "path";
import { padNumbers } from "./utils/misc";
import { isDevEnvironment, padNumbers } from "./utils/misc";
import { readFileSync, writeFileSync, existsSync } from "fs";
const SERVER_VERSION_FILE_PATH = join(__dirname, "./server.version");
@ -21,7 +21,7 @@ function getDateVersion(): string {
}
function getVersion(): string {
if (process.env.MODE === "dev") {
if (isDevEnvironment()) {
return "DEVELOPMENT-VERSION";
}