added authorization
This commit is contained in:
		
							
								
								
									
										11
									
								
								src/db.ts
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/db.ts
									
									
									
									
									
								
							@@ -39,6 +39,17 @@ export default class db {
 | 
			
		||||
      if (err) done(err);
 | 
			
		||||
    });
 | 
			
		||||
    mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
 | 
			
		||||
    mongoose.connection.on('disconnected', () => {  // reset state on disconnect
 | 
			
		||||
      console.log('Database disconnected');
 | 
			
		||||
      this.state.db = 0;
 | 
			
		||||
      done();
 | 
			
		||||
    });
 | 
			
		||||
    process.on('SIGINT', () => {  // close connection when app is terminated
 | 
			
		||||
      mongoose.connection.close(() => {
 | 
			
		||||
        console.log('Mongoose default connection disconnected through app termination');
 | 
			
		||||
        process.exit(0);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    mongoose.connection.once('open', () => {
 | 
			
		||||
      console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`);
 | 
			
		||||
      this.state.db = mongoose.connection;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
const globals = {
 | 
			
		||||
  levels: [
 | 
			
		||||
  levels: [  // access levels
 | 
			
		||||
    'read',
 | 
			
		||||
    'write',
 | 
			
		||||
    'maintain',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								src/helpers/authorize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/helpers/authorize.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
import basicAuth from 'basic-auth';
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import UserModel from '../models/user';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// appends req.auth(res, ['levels'], method = 'all')
 | 
			
		||||
// which returns sends error message and returns false if unauthorized, otherwise true
 | 
			
		||||
// req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'}
 | 
			
		||||
 | 
			
		||||
module.exports = async (req, res, next) => {
 | 
			
		||||
  let givenMethod = '';  // authorization method given by client, basic taken preferred
 | 
			
		||||
  let user = {name: '', level: ''};               // user object
 | 
			
		||||
 | 
			
		||||
  // test authentications
 | 
			
		||||
  const userBasic = await basic(req, next);
 | 
			
		||||
 | 
			
		||||
  if (userBasic) {  // basic available
 | 
			
		||||
    givenMethod = 'basic';
 | 
			
		||||
    user = userBasic;
 | 
			
		||||
  }
 | 
			
		||||
  else {  // if basic not available, test key
 | 
			
		||||
    const userKey = await key(req, next);
 | 
			
		||||
    if (userKey) {
 | 
			
		||||
      givenMethod = 'key';
 | 
			
		||||
      user = userKey;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  req.auth = (res, levels, method = 'all') => {
 | 
			
		||||
    if (givenMethod === method || (method === 'all' && givenMethod !== '')) {  // method is available
 | 
			
		||||
      if (levels.indexOf(user.level) > -1) {  // level is available
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        res.status(403).json({status: 'Forbidden'});
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      res.status(401).json({status: 'Unauthorized'});
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  req.authDetails = {
 | 
			
		||||
    method: givenMethod,
 | 
			
		||||
    username: user.name,
 | 
			
		||||
    level: user.level
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  next();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function basic (req, next): any {  // checks basic auth and returns changed user object
 | 
			
		||||
  return new Promise(resolve => {
 | 
			
		||||
    const auth = basicAuth(req);
 | 
			
		||||
    if (auth !== undefined) {  // basic auth available
 | 
			
		||||
      UserModel.find({name: auth.name}).lean().exec( 'find', (err, data) => {  // find user
 | 
			
		||||
        if (err) next(err);
 | 
			
		||||
        if (data.length === 1) {  // one user found
 | 
			
		||||
          bcrypt.compare(auth.pass, data[0].pass, (err, res) => {  // check password
 | 
			
		||||
            if (err) next(err);
 | 
			
		||||
            if (res === true) {
 | 
			
		||||
              resolve({level: data[0].level, name: data[0].name});
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
              resolve(null);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          resolve(null);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      resolve(null);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function key (req, next): any {  // checks API key and returns changed user object
 | 
			
		||||
  return new Promise(resolve => {
 | 
			
		||||
    if (req.query.key !== undefined) {
 | 
			
		||||
      UserModel.find({key: req.query.key}).lean().exec( 'find', (err, data) => {  // find user
 | 
			
		||||
        if (err) next(err);
 | 
			
		||||
        if (data.length === 1) {  // one user found
 | 
			
		||||
          resolve({level: data[0].level, name: data[0].name});
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          resolve(null);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      resolve(null);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/index.ts
									
									
									
									
									
								
							@@ -2,6 +2,8 @@ import express from 'express';
 | 
			
		||||
import bodyParser from 'body-parser';
 | 
			
		||||
import swagger from 'swagger-ui-express';
 | 
			
		||||
import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser';
 | 
			
		||||
import contentFilter from 'content-filter';
 | 
			
		||||
import mongoSanitize from 'mongo-sanitize';
 | 
			
		||||
import db from './db';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -23,9 +25,23 @@ const port = process.env.PORT || 3000;
 | 
			
		||||
app.use(express.json({ limit: '5mb'}));
 | 
			
		||||
app.use(express.urlencoded({ extended: false, limit: '5mb' }));
 | 
			
		||||
app.use(bodyParser.json());
 | 
			
		||||
app.use(contentFilter());  // filter URL query attacks
 | 
			
		||||
app.use((req, res, next) => {  // filter body query attacks
 | 
			
		||||
  req.body = mongoSanitize(req.body);
 | 
			
		||||
  next();
 | 
			
		||||
});
 | 
			
		||||
app.use((err, req, res, ignore) => {  // bodyParser error handling
 | 
			
		||||
  res.status(400).send({status: 'Invalid JSON body'});
 | 
			
		||||
});
 | 
			
		||||
app.use((req, res, next) => {  // no database connection error
 | 
			
		||||
  if (db.getState().db) {
 | 
			
		||||
    next();
 | 
			
		||||
  }
 | 
			
		||||
  else {
 | 
			
		||||
    res.status(500).send({status: 'Internal server error'});
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
app.use(require('./helpers/authorize'));  // handle authentication
 | 
			
		||||
 | 
			
		||||
// require routes
 | 
			
		||||
app.use('/', require('./routes/root'));
 | 
			
		||||
 
 | 
			
		||||
@@ -26,14 +26,16 @@ describe('/', () => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .get('/')
 | 
			
		||||
      .expect('Content-type', /json/)
 | 
			
		||||
      .expect(200, (err, res) => {
 | 
			
		||||
      .expect(200)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'API server up and running!'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('Testing unknown routes', () => {
 | 
			
		||||
describe('Unknown routes', () => {
 | 
			
		||||
  let server;
 | 
			
		||||
 | 
			
		||||
  before(done => {
 | 
			
		||||
@@ -50,10 +52,94 @@ describe('Testing unknown routes', () => {
 | 
			
		||||
  afterEach(done => {
 | 
			
		||||
    server.close(done);
 | 
			
		||||
  });
 | 
			
		||||
  it('returns a 404 message', done => {
 | 
			
		||||
  it('return a 404 message', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .get('/unknownroute')
 | 
			
		||||
      .expect(404);
 | 
			
		||||
    done();
 | 
			
		||||
      .expect(404)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Not found'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('An unauthorized request', () => {
 | 
			
		||||
  let server;
 | 
			
		||||
 | 
			
		||||
  before(done => {
 | 
			
		||||
    db.connect('test', done);
 | 
			
		||||
  });
 | 
			
		||||
  beforeEach(done => {
 | 
			
		||||
    delete require.cache[require.resolve('../index')];  // prevent loading from cache
 | 
			
		||||
    server = require('../index');
 | 
			
		||||
    db.drop(err => {  // reset database
 | 
			
		||||
     if (err) return done(err);
 | 
			
		||||
     db.loadJson(require('../test/db.json'), done);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  afterEach(done => {
 | 
			
		||||
    server.close(done);
 | 
			
		||||
  });
 | 
			
		||||
  it('returns a 401 message', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .get('/authorized')
 | 
			
		||||
      .expect(401)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Unauthorized'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
  it('does not work with correct username', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .get('/authorized')
 | 
			
		||||
      .auth('admin', 'Abc123!!')
 | 
			
		||||
      .expect(401)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Unauthorized'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('An authorized request', () => {
 | 
			
		||||
  let server;
 | 
			
		||||
 | 
			
		||||
  before(done => {
 | 
			
		||||
    db.connect('test', done);
 | 
			
		||||
  });
 | 
			
		||||
  beforeEach(done => {
 | 
			
		||||
    delete require.cache[require.resolve('../index')];  // prevent loading from cache
 | 
			
		||||
    server = require('../index');
 | 
			
		||||
    db.drop(err => {  // reset database
 | 
			
		||||
     if (err) return done(err);
 | 
			
		||||
     db.loadJson(require('../test/db.json'), done);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  afterEach(done => {
 | 
			
		||||
    server.close(done);
 | 
			
		||||
  });
 | 
			
		||||
  it('works with an API key', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .get('/authorized?key=5ea131671feb9c2ee0aafc9a')
 | 
			
		||||
      .expect(200)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Authorization successful', method: 'key'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
  it('works with basic auth', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .get('/authorized')
 | 
			
		||||
      .auth('admin', 'Abc123!#')
 | 
			
		||||
      .expect(200)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Authorization successful', method: 'basic'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import globals from '../globals';
 | 
			
		||||
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
 | 
			
		||||
@@ -6,4 +7,9 @@ router.get('/', (req, res) => {
 | 
			
		||||
  res.json({status: 'API server up and running!'});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get('/authorized', (req, res) => {
 | 
			
		||||
  if (!req.auth(res, globals.levels)) return;
 | 
			
		||||
  res.json({status: 'Authorization successful', method: req.authDetails.method});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,10 +26,12 @@ describe('/user/new', () => {
 | 
			
		||||
  it('returns the added user data', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .post('/user/new')
 | 
			
		||||
      .auth('admin', 'Abc123!#')
 | 
			
		||||
      .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'})
 | 
			
		||||
      .expect('Content-type', /json/)
 | 
			
		||||
      .expect(200, (err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
      .expect(200)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).have.property('_id').be.type('string');
 | 
			
		||||
        should(res.body).have.property('email', 'john.doe@bosch.com');
 | 
			
		||||
        should(res.body).have.property('name', 'johndoe');
 | 
			
		||||
@@ -42,9 +44,11 @@ describe('/user/new', () => {
 | 
			
		||||
  it('stores the data', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .post('/user/new')
 | 
			
		||||
      .auth('admin', 'Abc123!#')
 | 
			
		||||
      .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'})
 | 
			
		||||
      .expect(200, err => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
      .expect(200)
 | 
			
		||||
      .end(err => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        UserModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(data).have.lengthOf(1);
 | 
			
		||||
@@ -63,9 +67,11 @@ describe('/user/new', () => {
 | 
			
		||||
  it('rejects a username already in use', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .post('/user/new')
 | 
			
		||||
      .auth('admin', 'Abc123!#')
 | 
			
		||||
      .send({email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'})
 | 
			
		||||
      .expect(400, (err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
      .expect(400)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Username already taken'});
 | 
			
		||||
        UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
@@ -73,5 +79,30 @@ describe('/user/new', () => {
 | 
			
		||||
          done();
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  });  // TODO: authentication
 | 
			
		||||
  });
 | 
			
		||||
  it('rejects requests from non-admins', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .post('/user/new')
 | 
			
		||||
      .auth('janedoe', 'Abc123!#')
 | 
			
		||||
      .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'})
 | 
			
		||||
      .expect('Content-type', /json/)
 | 
			
		||||
      .expect(403)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Forbidden'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
  it('rejects requests from an admin API key', done => {
 | 
			
		||||
    supertest(server)
 | 
			
		||||
      .post('/user/new?key=5ea131671feb9c2ee0aafc9a')
 | 
			
		||||
      .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'})
 | 
			
		||||
      .expect('Content-type', /json/)
 | 
			
		||||
      .expect(401)
 | 
			
		||||
      .end((err, res) => {
 | 
			
		||||
        if (err) done (err);
 | 
			
		||||
        should(res.body).be.eql({status: 'Unauthorized'});
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -11,6 +11,9 @@ router.get('/users', (req, res) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.post('/user/new', (req, res, next) => {
 | 
			
		||||
  console.log(req.authDetails);
 | 
			
		||||
  if (!req.auth(res, ['admin'], 'basic')) return;
 | 
			
		||||
 | 
			
		||||
  // validate input
 | 
			
		||||
  const {error, value: user} = UserValidate.input(req.body);
 | 
			
		||||
  if(error !== undefined) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import joi from '@hapi/joi';
 | 
			
		||||
import globals from "../../globals";
 | 
			
		||||
import globals from '../../globals';
 | 
			
		||||
 | 
			
		||||
export default class UserValidate {  // validate input for user
 | 
			
		||||
  static input (data) {
 | 
			
		||||
@@ -27,6 +27,7 @@ export default class UserValidate {  // validate input for user
 | 
			
		||||
        .required(),
 | 
			
		||||
 | 
			
		||||
      device_name: joi.string()
 | 
			
		||||
        .allow('')
 | 
			
		||||
        .required()
 | 
			
		||||
    }).validate(data);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,23 @@
 | 
			
		||||
        "_id": "5ea0450ed851c30a90e70894",
 | 
			
		||||
        "email": "jane.doe@bosch.com",
 | 
			
		||||
        "name": "janedoe",
 | 
			
		||||
        "pass": "$2a$10$KDKZjCsgDXwhtKdXZ9oG2ueDuCZsRKOMSqHuBfCM/2R0V6DRns.sy",
 | 
			
		||||
        "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K",
 | 
			
		||||
        "level": "write",
 | 
			
		||||
        "location": "Rng",
 | 
			
		||||
        "device_name": "Alpha I",
 | 
			
		||||
        "key": "5ea0450ed851c30a90e70899",
 | 
			
		||||
        "__v": 0
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "_id": "5ea131671feb9c2ee0aafc9b",
 | 
			
		||||
        "email": "a.d.m.i.n@bosch.com",
 | 
			
		||||
        "name": "admin",
 | 
			
		||||
        "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K",
 | 
			
		||||
        "level": "admin",
 | 
			
		||||
        "location": "Rng",
 | 
			
		||||
        "device_name": "",
 | 
			
		||||
        "key": "5ea131671feb9c2ee0aafc9a",
 | 
			
		||||
        "__v": "0"
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user