2020-04-22 15:52:17 +02:00
|
|
|
import ApplicationComponent from "../ApplicationComponent";
|
2020-08-28 16:52:43 +02:00
|
|
|
import {Request, Router} from "express";
|
2020-04-22 15:52:17 +02:00
|
|
|
import crypto from "crypto";
|
|
|
|
import {BadRequestError} from "../HttpError";
|
2020-09-25 22:03:22 +02:00
|
|
|
import {AuthMiddleware} from "../auth/AuthComponent";
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-09-25 22:03:22 +02:00
|
|
|
export default class CsrfProtectionComponent extends ApplicationComponent {
|
2020-08-28 16:52:43 +02:00
|
|
|
private static readonly excluders: ((req: Request) => boolean)[] = [];
|
2020-07-08 13:28:22 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public static getCsrfToken(session: Express.Session): string {
|
2020-09-23 16:11:51 +02:00
|
|
|
if (typeof session.csrf !== 'string') {
|
|
|
|
session.csrf = crypto.randomBytes(64).toString('base64');
|
|
|
|
}
|
|
|
|
return session.csrf;
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public static addExcluder(excluder: (req: Request) => boolean): void {
|
2020-08-28 16:52:43 +02:00
|
|
|
this.excluders.push(excluder);
|
2020-07-08 13:28:22 +02:00
|
|
|
}
|
|
|
|
|
2020-07-11 11:46:16 +02:00
|
|
|
public async handle(router: Router): Promise<void> {
|
2020-07-08 10:49:29 +02:00
|
|
|
router.use(async (req, res, next) => {
|
2020-08-28 16:52:43 +02:00
|
|
|
for (const excluder of CsrfProtectionComponent.excluders) {
|
|
|
|
if (excluder(req)) return next();
|
2020-07-08 13:28:22 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
const session = req.getSession();
|
|
|
|
res.locals.getCsrfToken = () => {
|
|
|
|
return CsrfProtectionComponent.getCsrfToken(session);
|
2020-04-22 15:52:17 +02:00
|
|
|
};
|
|
|
|
|
2020-07-08 10:49:29 +02:00
|
|
|
if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) {
|
|
|
|
try {
|
2020-09-25 22:03:22 +02:00
|
|
|
if (!await req.as(AuthMiddleware).getAuthGuard().isAuthenticatedViaRequest(req)) {
|
2020-09-25 23:42:15 +02:00
|
|
|
if (session.csrf === undefined) {
|
2020-09-25 22:03:22 +02:00
|
|
|
return next(new InvalidCsrfTokenError(req.baseUrl, `You weren't assigned any CSRF token.`));
|
2020-07-08 10:49:29 +02:00
|
|
|
} else if (req.body.csrf === undefined) {
|
2020-09-25 22:03:22 +02:00
|
|
|
return next(new InvalidCsrfTokenError(req.baseUrl, `You didn't provide any CSRF token.`));
|
2020-09-25 23:42:15 +02:00
|
|
|
} else if (session.csrf !== req.body.csrf) {
|
2020-09-25 22:03:22 +02:00
|
|
|
return next(new InvalidCsrfTokenError(req.baseUrl, `Tokens don't match.`));
|
2020-07-08 10:49:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
2020-09-25 22:03:22 +02:00
|
|
|
return next(e);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class InvalidCsrfTokenError extends BadRequestError {
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(url: string, details: string, cause?: Error) {
|
2020-04-22 15:52:17 +02:00
|
|
|
super(
|
|
|
|
`Invalid CSRF token`,
|
|
|
|
`${details} We can't process this request. Please try again.`,
|
|
|
|
url,
|
2020-09-25 23:42:15 +02:00
|
|
|
cause,
|
2020-04-22 15:52:17 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public get name(): string {
|
2020-04-22 15:52:17 +02:00
|
|
|
return 'Invalid CSRF Token';
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public get errorCode(): number {
|
2020-04-22 15:52:17 +02:00
|
|
|
return 401;
|
|
|
|
}
|
|
|
|
}
|