فهرست منبع

Add AntiCSRFService

KernelDeimos 1 سال پیش
والد
کامیت
da7f73baa6
2فایلهای تغییر یافته به همراه121 افزوده شده و 0 حذف شده
  1. 3 0
      packages/backend/src/CoreModule.js
  2. 118 0
      packages/backend/src/services/auth/AntiCSRFService.js

+ 3 - 0
packages/backend/src/CoreModule.js

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

+ 118 - 0
packages/backend/src/services/auth/AntiCSRFService.js

@@ -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,
+};