diff --git a/src/clients.js b/src/clients.js new file mode 100644 index 000000000..c1e62fd01 --- /dev/null +++ b/src/clients.js @@ -0,0 +1,53 @@ +'use strict'; + +var assert = require('assert'), + hat = require('hat'), + debug = require('debug')('box:clients'), + clientdb = require('./clientdb.js'), + DatabaseError = require('./databaseerror.js'), + uuid = require('node-uuid'); + +exports = module.exports = { + add: add, + get: get, + update: update +}; + +function add(appIdentifier, redirectURI, scope, callback) { + assert(typeof appIdentifier === 'string'); + assert(typeof redirectURI === 'string'); + assert(typeof scope === 'string'); + assert(typeof callback === 'function'); + + var id = 'cid-' + uuid.v4(); + var clientSecret = hat(); + + clientdb.add(id, appIdentifier, clientSecret, redirectURI, scope, function (error, result) { + if (error) return callback(error); + callback(null, result); + }); +} + +function get(id, callback) { + assert(typeof id === 'string'); + assert(typeof callback === 'function'); + + clientdb.get(id, function (error, result) { + if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error); + callback(null, result); + }); +} + +function update(id, appIdentifier, clientSecret, redirectURI, scope, callback) { + assert(typeof id === 'string'); + assert(typeof appIdentifier === 'string'); + assert(typeof clientSecret === 'string'); + assert(typeof redirectURI === 'string'); + assert(typeof scope === 'string'); + assert(typeof callback === 'function'); + + clientdb.update(id, appIdentifier, clientSecret, redirectURI, scope, function (error, result) { + if (error) return callback(error); + callback(null, result); + }); +} \ No newline at end of file diff --git a/src/routes/clients.js b/src/routes/clients.js new file mode 100644 index 000000000..48acfa0d9 --- /dev/null +++ b/src/routes/clients.js @@ -0,0 +1,56 @@ +/* jslint node:true */ + +'use strict'; + +var assert = require('assert'), + debug = require('debug')('box:routes/clients'), + clients = require('../clients.js'), + DatabaseError = require('../databaseerror.js'), + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess; + +exports = module.exports = { + add: add, + get: get, + update: update +}; + +function add(req, res, next) { + var data = req.body; + + if (!data) return next(new HttpError(400, 'Cannot parse data field')); + if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required')); + if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required')); + + // prefix as this route only allows external apps for developers + var appId = 'external-' + data.appId; + + clients.add(appId, data.redirectURI, 'profile,users', function (error, result) { + if (error) return next(new HttpError(500, error)); + next(new HttpSuccess(201, result)); + }); +} + +function get(req, res, next) { + assert(typeof req.param.clientId === 'string'); + + clients.get(req.param.clientId, function (error, result) { + if (error) return next(new HttpError(500, error)); + next(new HttpSuccess(200, result)); + }); +} + +function update(req, res, next) { + assert(typeof req.param.clientId === 'string'); + + var data = req.body; + + if (!data) return next(new HttpError(400, 'Cannot parse data field')); + if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required')); + if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required')); + + clients.update(req.param.clientId, data.appId, data.redirectURI, data.scope, function (error, result) { + if (error) return next(new HttpError(500, error)); + next(new HttpSuccess(200, result)); + }); +} diff --git a/src/routes/test/clients-test.js b/src/routes/test/clients-test.js new file mode 100644 index 000000000..094aeb115 --- /dev/null +++ b/src/routes/test/clients-test.js @@ -0,0 +1,168 @@ +'use strict'; + +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +var async = require('async'), + config = require('../../../config.js'), + database = require('../../database.js'), + expect = require('expect.js'), + nock = require('nock'), + request = require('superagent'), + server = require('../../server.js'); + +var SERVER_URL = 'http://localhost:' + config.get('port'); + +var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com'; +var token = null; // authentication token + +var server; +function setup(done) { + server.start(done); +} + +function cleanup(done) { + database._clear(function (error) { + expect(error).to.not.be.ok(); + + server.stop(done); + }); +} + +describe('OAuth Clients API', function () { + describe('add', function () { + before(function (done) { + async.series([ + setup, + + function (callback) { + var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); + var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {}); + + request.post(SERVER_URL + '/api/v1/cloudron/activate') + .query({ setupToken: 'somesetuptoken' }) + .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result).to.be.ok(); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + + // stash token for further use + token = result.body.token; + + callback(); + }); + }, + ], done); + }); + + after(cleanup); + + it('fails without token', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .send({ appId: 'someApp', redirectURI: 'http://foobar.com' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(401); + done(); + }); + }); + + it('fails if not in developerMode', function (done) { + config.set('developerMode', false); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'http://foobar.com' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(412); + done(); + }); + }); + + it('fails without appId', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ redirectURI: 'http://foobar.com' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(400); + done(); + }); + }); + + it('fails with empty appId', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: '', redirectURI: 'http://foobar.com' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(400); + done(); + }); + }); + + it('fails without callbackURI', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(400); + done(); + }); + }); + + it('fails with empty callbackURI', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: '' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(400); + done(); + }); + }); + + xit('fails with malformed callbackURI', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'foobar' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(400); + done(); + }); + }); + + it('succeeds', function (done) { + config.set('developerMode', true); + + request.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'http://foobar.com' }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(201); + done(); + }); + }); + }); +});