Browse Source

API authentification (tbc...)

Gaël Métais 10 years ago
parent
commit
49015e3422

+ 31 - 5
Gruntfile.js

@@ -1,5 +1,11 @@
 module.exports = function(grunt) {
 
+    var DEV_SERVER_PORT = 8383;
+    var TEST_SERVER_PORT = 8387;
+
+    // Tell our Express server that Grunt launched it
+    process.env.GRUNTED = true;
+
     // Project configuration.
     grunt.initConfig({
         pkg: grunt.file.readJSON('package.json'),
@@ -77,7 +83,7 @@ module.exports = function(grunt) {
                 options: {
                     reporter: 'spec',
                 },
-                src: ['coverage/test/server/runsDatastoreTest.js']
+                src: ['coverage/test/api/apiTest.js']
             },
             coverage: {
                 options: {
@@ -85,11 +91,25 @@ module.exports = function(grunt) {
                     quiet: true,
                     captureFile: 'coverage/coverage.html'
                 },
-                src: ['coverage/test/api/*.js']
+                src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
             }
         },
         express: {
-            test: {
+            dev: {
+                options: {
+                    port: 8383,
+                    server: './bin/server.js',
+                    serverreload: true,
+                    showStack: true
+                }
+            },
+            testServer: {
+                options: {
+                    port: 8387,
+                    server: './bin/server.js'
+                }
+            },
+            testSuite: {
                 options: {
                     port: 8388,
                     bases: 'test/www'
@@ -114,10 +134,15 @@ module.exports = function(grunt) {
         'jshint'
     ]);
 
+    grunt.registerTask('dev', [
+        'express:dev'
+    ]);
+
     grunt.registerTask('test', [
         'build',
         'jshint',
-        'express:test',
+        'express:testServer',
+        'express:testSuite',
         'clean:coverage',
         'blanket',
         'copy:coverage',
@@ -128,7 +153,8 @@ module.exports = function(grunt) {
     grunt.registerTask('test-current-work', [
         'build',
         'jshint',
-        'express:test',
+        'express:testServer',
+        'express:testSuite',
         'clean:coverage',
         'blanket',
         'copy:coverage',

+ 13 - 4
bin/server.js

@@ -7,8 +7,11 @@ var server                  = require('http').createServer(app);
 var bodyParser              = require('body-parser');
 var compress                = require('compression');
 
+var authMiddleware          = require('../lib/server/authMiddleware');
+
 app.use(compress());
 app.use(bodyParser.json());
+app.use(authMiddleware);
 
 
 // Initialize the controllers
@@ -16,7 +19,13 @@ var apiController           = require('../lib/server/controllers/apiController')
 var uiController            = require('../lib/server/controllers/uiController')(app);
 
 
-// Launch the server
-server.listen(settings.serverPort, function() {
-    console.log('Listening on port %d', server.address().port);
-});
+// Let's start the server!
+if (!process.env.GRUNTED) {
+    // The server is not launched by Grunt
+    server.listen(settings.serverPort, function() {
+        console.log('Listening on port %d', server.address().port);
+    });
+}
+
+// For Grunt
+module.exports = app;

+ 1 - 1
bower.json

@@ -1,7 +1,7 @@
 {
   "name": "yellowlabtools",
   "dependencies": {
-    "angular": "~1.3.1",
+    "angular": "~1.3.5",
     "ngModal": "git://github.com/gmetais/ngModal.git#1.2.3"
   }
 }

+ 52 - 0
lib/server/authMiddleware.js

@@ -0,0 +1,52 @@
+var config      = require('../../server_config/settings.json');
+
+var jwt         = require('jwt-simple');
+var debug       = require('debug')('authMiddleware');
+
+
+var authMiddleware = function(req, res, next) {
+    'use strict';
+
+    if (req.path.indexOf('/api/') === 0) {
+        
+        // Test if it's an authorized key
+        if (req.headers && req.headers['x-api-key'] && isApiKeyValid(req.headers['x-api-key'])) {
+            next();
+            return;
+        }
+
+        // Test if it's an authorized token
+        if (req.headers && req.headers['x-api-token'] && isTokenValid(req.headers['x-api-token'])) {
+            next();
+            return;
+        }
+        
+        res.status(401).send('Unauthorized');
+    }
+};
+
+
+function isApiKeyValid(apiKey) {
+    return (config.authorizedKeys[apiKey]) ? true : false;
+}
+
+
+function isTokenValid(token) {
+
+    var data = null;
+
+    try {
+        jwt.decode(token, config.tokenSalt);
+    } catch(err) {
+        debug('Error while decoding token');
+        debug(err);
+        return false;
+    }
+
+    return data.expire &&
+        data.expire > Date.now() &&
+        data.application &&
+        config.authorizedApplications.indexOf(data.application) >= 0;
+}
+
+module.exports = authMiddleware;

+ 7 - 5
lib/server/controllers/apiController.js

@@ -13,10 +13,7 @@ var ApiController = function(app) {
     var runsDatastore = new RunsDatastore();
     var resultsDatastore = new ResultsDatastore();
 
-    // Retrieve the list of all runs
-    /*app.get('/api/runs', function(req, res) {
-        // NOT YET
-    });*/
+
 
     // Create a new run
     app.post('/api/runs', function(req, res) {
@@ -107,6 +104,11 @@ var ApiController = function(app) {
         }
     });
 
+    // Retrieve the list of all runs
+    /*app.get('/api/runs', function(req, res) {
+        // NOT YET
+    });*/
+
     // Delete one run by id
     /*app.delete('/api/runs/:id', function(req, res) {
         deleteRun()
@@ -125,7 +127,7 @@ var ApiController = function(app) {
     // Exists
     app.head('/api/runs/:id', function(req, res) {
         existsX();
-        // Retourne 200 si existe ou 404 si n'existe pas
+        // Returns 200 if the result exists or 404 if not
     });
     */
 

+ 7 - 5
package.json

@@ -11,16 +11,17 @@
   "main": "./lib/yellowlabtools.js",
   "dependencies": {
     "async": "~0.9.0",
-    "body-parser": "~1.9.2",
-    "compression": "~1.2.0",
+    "body-parser": "~1.10.0",
+    "compression": "~1.2.1",
     "debug": "^2.1.0",
     "express": "~4.10.4",
+    "jwt-simple": "^0.2.0",
     "phantomas": "1.7.0",
     "rimraf": "^2.2.8",
     "socket.io": "~1.2.0"
   },
   "devDependencies": {
-    "chai": "^1.9.2",
+    "chai": "^1.10.0",
     "grunt": "^0.4.5",
     "grunt-blanket": "^0.0.8",
     "grunt-contrib-clean": "^0.6.0",
@@ -29,11 +30,12 @@
     "grunt-contrib-less": "^0.12.0",
     "grunt-express": "^1.4.1",
     "grunt-fontsmith": "^0.9.1",
-    "grunt-mocha-test": "^0.12.2",
+    "grunt-mocha-test": "^0.12.4",
     "matchdep": "^0.3.0",
     "mocha": "^2.0.1",
-    "phantomjs": "^1.9.10",
+    "phantomjs": "^1.9.12",
     "q": "^1.1.2",
+    "request": "^2.49.0",
     "sinon": "^1.12.1",
     "sinon-chai": "^2.6.0"
   },

+ 9 - 1
server_config/settings-prod.json

@@ -1,4 +1,12 @@
 {
     "serverPort": 80,
-    "googleAnalyticsId": "UA-54493828-1"
+    "googleAnalyticsId": "UA-54493828-1",
+
+    "authorized-keys": {
+        
+    },
+    "tokenSalt": "",
+    "authorizedApplications": [
+        
+    ]
 }

+ 9 - 1
server_config/settings.json

@@ -1,4 +1,12 @@
 {
     "serverPort": 8383,
-    "googleAnalyticsId": ""
+    "googleAnalyticsId": "",
+
+    "authorizedKeys": {
+        "1234567890": "contact@gaelmetais.com"
+    },
+    "tokenSalt": "lake-city",
+    "authorizedApplications": [
+        "frontend"
+    ]
 }

+ 171 - 0
test/api/apiTest.js

@@ -0,0 +1,171 @@
+var should      = require('chai').should();
+var request     = require('request');
+var jwt         = require('jwt-simple');
+
+var config = {
+    "authorizedKeys": {
+        "1234567890": "test@test.com"
+    },
+    "tokenSalt": "test-salt",
+    "authorizedApplications": ["wooot"]
+};
+
+var apiUrl = 'http://localhost:8387/api';
+var wwwUrl = 'http://localhost:8388';
+
+describe('api', function() {
+
+    var runId;
+    
+    it('should not accept a query if there is no key in headers', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'POST',
+            url: apiUrl + '/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: false
+            },
+            json: true
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 401) {
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+    it('should refuse a query with an invalid key', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'POST',
+            url: apiUrl + '/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: false
+            },
+            json: true,
+            headers: {
+                'X-Api-Key': 'invalid'
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 401) {
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+    it('should accept a query with a valid key', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'POST',
+            url: apiUrl + '/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: false
+            },
+            json: true,
+            headers: {
+                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 200) {
+
+                runId = body.runId;
+                runId.should.be.a('string');
+                done();
+
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+    it('should refuse an expired token', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'POST',
+            url: apiUrl + '/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: false
+            },
+            json: true,
+            headers: {
+                'X-Api-Token': jwt.encode({
+                    application: config.authorizedApplications[0],
+                    expire: Date.now() - 60000
+                }, config.tokenSalt)
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 401) {
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+    it('should refuse a token from an unknown app', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'POST',
+            url: apiUrl + '/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: false
+            },
+            json: true,
+            headers: {
+                'X-Api-Token': jwt.encode({
+                    application: 'unknown-app',
+                    expire: Date.now() + 60000
+                }, config.tokenSalt)
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 401) {
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+    it('should accept a good token', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'POST',
+            url: apiUrl + '/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: false
+            },
+            json: true,
+            headers: {
+                'X-Api-Token': jwt.encode({
+                    application: config.authorizedApplications[0],
+                    expire: Date.now() + 60000
+                }, config.tokenSalt)
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 200) {
+
+                runId = body.runId;
+                runId.should.be.a('string');
+                done();
+
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+});