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