Add AntiCSRFService

This commit is contained in:
KernelDeimos 2024-05-13 19:08:51 -04:00
parent afb9d866b5
commit da7f73baa6
2 changed files with 121 additions and 0 deletions

View File

@ -207,6 +207,9 @@ const install = async ({ services, app }) => {
const { UserProtectedEndpointsService } = require("./services/web/UserProtectedEndpointsService");
services.registerService('__user-protected-endpoints', UserProtectedEndpointsService);
const { AntiCSRFService } = require('./services/auth/AntiCSRFService');
services.registerService('anti-csrf', AntiCSRFService);
}
const install_legacy = async ({ services }) => {

View File

@ -0,0 +1,118 @@
const eggspress = require("../../api/eggspress");
const config = require("../../config");
const { subdomain } = require("../../helpers");
const BaseService = require("../BaseService");
class CircularQueue {
constructor (size) {
this.size = size;
this.queue = [];
this.index = 0;
this.map = new Map();
}
push (item) {
if ( this.queue[this.index] ) {
this.map.delete(this.queue[this.index]);
}
this.queue[this.index] = item;
this.map.set(item, this.index);
this.index = (this.index + 1) % this.size;
}
get (index) {
return this.queue[(this.index + index) % this.size];
}
has (item) {
return this.map.has(item);
}
maybe_consume (item) {
if ( this.has(item) ) {
const index = this.map.get(item);
this.map.delete(item);
this.queue[index] = null;
return true;
}
return false;
}
}
class AntiCSRFService extends BaseService {
_construct () {
this.map_session_to_tokens = {};
}
['__on_install.routes'] () {
const { app } = this.services.get('web-server');
app.use(eggspress('/get-anticsrf-token', {
auth2: true,
allowedMethods: ['GET'],
}, async (req, res) => {
// We disallow `api.` because it has a more relaxed CORS policy
const subdomain_check = config.experimental_no_subdomain ||
(subdomain(req) !== 'api');
if ( ! subdomain_check ) {
return res.status(404).send('Hey, stop that!');
}
// TODO: session uuid instead of user
const token = this.create_token(req.user.uuid);
res.send({ token });
}));
}
create_token (session) {
let tokens = this.map_session_to_tokens[session];
if ( ! tokens ) {
tokens = new CircularQueue(10);
this.map_session_to_tokens[session] = tokens;
}
const token = this.generate_token_();
tokens.push(token);
return token;
}
consume_token (session, token) {
const tokens = this.map_session_to_tokens[session];
if ( ! tokens ) return false;
return tokens.maybe_consume(token);
}
generate_token_ () {
return require('crypto').randomBytes(32).toString('hex');
}
_test ({ assert }) {
// Do this several times, like a user would
for ( let i=0 ; i < 30 ; i++ ) {
// Generate 30 tokens
const tokens = [];
for ( let j=0 ; j < 30 ; j++ ) {
tokens.push(this.create_token('session'));
}
// Only the last 10 should be valid
const results_for_stale_tokens = [];
for ( let j=0 ; j < 20 ; j++ ) {
const result = this.consume_token('session', tokens[j]);
results_for_stale_tokens.push(result);
}
assert(() => results_for_stale_tokens.every(v => v === false));
// The last 10 should be valid
const results_for_valid_tokens = [];
for ( let j=20 ; j < 30 ; j++ ) {
const result = this.consume_token('session', tokens[j]);
results_for_valid_tokens.push(result);
}
assert(() => results_for_valid_tokens.every(v => v === true));
// A completely arbitrary token should not be valid
assert(() => this.consume_token('session', 'arbitrary') === false);
}
}
}
module.exports = {
AntiCSRFService,
};