initial keyserver implementation
This commit is contained in:
Родитель
50006fbb6a
Коммит
e608288407
|
@ -1,14 +1 @@
|
|||
lib-cov
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.gz
|
||||
|
||||
pids
|
||||
logs
|
||||
results
|
||||
|
||||
npm-debug.log
|
||||
/node_modules
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Mozilla Public License (MPL) v.2
|
||||
|
||||
http://www.mozilla.org/MPL/2.0/
|
64
README.md
64
README.md
|
@ -1,4 +1,66 @@
|
|||
picl-keyserver
|
||||
==============
|
||||
|
||||
Key management for PICL users
|
||||
Key management for PICL users
|
||||
|
||||
## API
|
||||
|
||||
You can currently create a new user account, add additional devices, and bump the class A key version.
|
||||
|
||||
All API calls take a JSON payload of an email address. E.g.:
|
||||
|
||||
{
|
||||
email: bob@example.com
|
||||
}
|
||||
|
||||
Eventuall, they might take an assertion:
|
||||
|
||||
{
|
||||
assertion: <persona generated assertion>
|
||||
}
|
||||
|
||||
### PUT /user/create
|
||||
Creates a new user account and generates a class A key.
|
||||
|
||||
*Returns*:
|
||||
|
||||
{
|
||||
success: true,
|
||||
kA: <32 random bytes in hex>,
|
||||
version: 1,
|
||||
deviceId: <32 random bytes in hex>
|
||||
}
|
||||
|
||||
### PUT /device/create
|
||||
Registers a new device with the user account.
|
||||
|
||||
*Returns*
|
||||
{
|
||||
success: true,
|
||||
kA: <user's current kA>,
|
||||
version: <kA version>
|
||||
deviceId: <newly generated deviceId>
|
||||
}
|
||||
|
||||
### PUT /user/get/{deviceId}
|
||||
Fetches the user's current key.
|
||||
|
||||
*Returns*
|
||||
{
|
||||
success: true,
|
||||
kA: <user's current kA>,
|
||||
version: <kA version>
|
||||
}
|
||||
|
||||
### PUT /user/bump/{deviceId}
|
||||
This creates a new class A key for the user and bumps the version number.
|
||||
All devices besides the device that initiated the call will be marked as having
|
||||
an outdated key.
|
||||
|
||||
*Returns*
|
||||
{
|
||||
success: true,
|
||||
kA: <newly generated kA>,
|
||||
version: <kA version>
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var server = require('./server.js');
|
||||
|
||||
// Start the server
|
||||
server.start(function() {
|
||||
console.log("running on http://" + server.settings.host + ":" + server.settings.port);
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var convict = require('convict');
|
||||
|
||||
const AVAILABLE_BACKENDS = ["memory", "couchbase"];
|
||||
|
||||
|
||||
var conf = module.exports = convict({
|
||||
env: {
|
||||
doc: "The current node.js environment",
|
||||
default: "production",
|
||||
format: [ "production", "local", "test" ],
|
||||
env: 'NODE_ENV'
|
||||
},
|
||||
public_url: {
|
||||
format: "url",
|
||||
// the real url is set by awsbox
|
||||
default: "http://127.0.0.1:8090"
|
||||
},
|
||||
verifier_url: {
|
||||
doc: "Service used to verify Persona assertions",
|
||||
format: "url",
|
||||
default: "https://verifier.login.persona.org/verify",
|
||||
env: 'VERIFIER_URL'
|
||||
},
|
||||
kvstore: {
|
||||
backend: {
|
||||
format: AVAILABLE_BACKENDS,
|
||||
default: "memory",
|
||||
env: 'KVSTORE_BACKEND'
|
||||
},
|
||||
available_backends: {
|
||||
doc: "List of available key-value stores",
|
||||
default: AVAILABLE_BACKENDS
|
||||
}
|
||||
},
|
||||
couchbase: {
|
||||
user: {
|
||||
default: 'Administrator',
|
||||
env: 'KVSTORE_USERNAME'
|
||||
},
|
||||
password: {
|
||||
default: 'password',
|
||||
env: 'KVSTORE_PASSWORD'
|
||||
},
|
||||
bucket: {
|
||||
default: 'picl',
|
||||
env: 'KVSTORE_BUCKET'
|
||||
},
|
||||
hosts: [ "localhost:8091" ]
|
||||
},
|
||||
bind_to: {
|
||||
host: {
|
||||
doc: "The ip address the server should bind",
|
||||
default: '127.0.0.1',
|
||||
format: 'ipaddress',
|
||||
env: 'IP_ADDRESS'
|
||||
},
|
||||
port: {
|
||||
doc: "The port the server should bind",
|
||||
default: 8090,
|
||||
format: 'port',
|
||||
env: 'PORT'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// handle configuration files. you can specify a CSV list of configuration
|
||||
// files to process, which will be overlayed in order, in the CONFIG_FILES
|
||||
// environment variable
|
||||
if (process.env.CONFIG_FILES) {
|
||||
var files = process.env.CONFIG_FILES.split(',');
|
||||
files.forEach(function(file) {
|
||||
conf.loadFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
if (conf.get('env') === 'test') {
|
||||
if (conf.get('kvstore.backend') === 'couchbase') {
|
||||
conf.set('couchbase.bucket', 'default');
|
||||
}
|
||||
}
|
||||
|
||||
conf.validate();
|
|
@ -0,0 +1,71 @@
|
|||
const Hapi = require('hapi');
|
||||
const kvstore = require('picl-server/lib/kvstore');
|
||||
const config = require('./config.js');
|
||||
|
||||
/* Data is stored using the abstract 'kvstore' interface from picl-server.
|
||||
* We use a single, shared connection to the store, which is established
|
||||
* automatically but asynchronously. Thus, there is a little bit of magic
|
||||
* here to ensure the API functions in this module will wait for a connection
|
||||
* to be established. Just wrap any such functions with 'waitKV' like so:
|
||||
*
|
||||
* var get_tabs = waitKV(function(user, device, cb) {
|
||||
* // Calls will be delayed until the shared connection is ready.
|
||||
* kv.get('whatever', function(err, res) {
|
||||
* // ...do whatever with the result here...
|
||||
* });
|
||||
* });
|
||||
*
|
||||
*/
|
||||
|
||||
var kv = null;
|
||||
var kvErr = null;
|
||||
var kvWaitlist = [];
|
||||
var connectList = [];
|
||||
|
||||
function waitKV(func) {
|
||||
return function() {
|
||||
// If the kv connection is ready, immediately call the function.
|
||||
// It can safely use the shared global variable.
|
||||
if (kv !== null) {
|
||||
func.apply(this, arguments);
|
||||
}
|
||||
// If the connection errored out, immediately call the callback function
|
||||
// to report the error. Callback is assumed to be the last argument.
|
||||
else if (kvErr !== null) {
|
||||
arguments[arguments.length - 1].call(this, kvErr);
|
||||
}
|
||||
// Otherwise, we have to wait for the database connection.
|
||||
// Re-wrap the function so that the above logic will be applied when ready.
|
||||
else {
|
||||
kvWaitlist.push(waitKV(func));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
kvstore.connect(config.get('kvstore'), function(err, conn) {
|
||||
if (err) {
|
||||
kvErr = err;
|
||||
} else {
|
||||
kv = conn;
|
||||
}
|
||||
while (connectList.length) {
|
||||
connectList.pop()(kvErr, conn);
|
||||
}
|
||||
while (kvWaitlist.length) {
|
||||
process.nextTick(kvWaitlist.pop());
|
||||
}
|
||||
});
|
||||
|
||||
function onconnect(cb) {
|
||||
if (kv || kvErr) {
|
||||
cb(kvErr, kv);
|
||||
} else {
|
||||
connectList.push(cb);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wait: waitKV,
|
||||
onconnect: onconnect
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
var util = require('./util');
|
||||
|
||||
module.exports = function(db) {
|
||||
|
||||
// creates a new device
|
||||
function create(user, cb) {
|
||||
util.getDeviceID(function (err, id) {
|
||||
var device = {
|
||||
user: user,
|
||||
latest_version: true,
|
||||
last_update: +new Date
|
||||
};
|
||||
db.set(id, device, function(err) {
|
||||
cb(err, id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// returns device info
|
||||
function get(id, cb) {
|
||||
db.get(id, cb);
|
||||
}
|
||||
|
||||
return { create: create, get: get };
|
||||
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
var Hapi = require('Hapi');
|
||||
var verify = require('picl-server/lib/verify.js');
|
||||
var config = require('./config.js');
|
||||
var users = require('./users.js');
|
||||
|
||||
// fake auth just returns the email address
|
||||
exports.email = function(email, next) {
|
||||
next(email);
|
||||
};
|
||||
|
||||
// verify an assertion and return the email address
|
||||
exports.verify = function(assertion, audience, next) {
|
||||
verify(assertion, audience, config.get('verifier_url'),
|
||||
function(err, result) {
|
||||
if (err) next(Hapi.Error.badRequest(err));
|
||||
else next(result.email);
|
||||
});
|
||||
};
|
||||
|
||||
// retrieve the userId associated with an email address
|
||||
// TODO add heavy cacheing of this. Changing email should be rare.
|
||||
exports.userId = function(email, next) {
|
||||
users.getId(email, function(err, userId) {
|
||||
if (err) next(Hapi.Error.badRequest(err));
|
||||
else next(userId);
|
||||
});
|
||||
};
|
||||
|
||||
// retrieve the meta data for a user
|
||||
exports.user = function(userId, next) {
|
||||
users.getUser(userId, function(err, user) {
|
||||
if (err) next(Hapi.Error.badRequest(err));
|
||||
else next(user);
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Prerequesites can be included in a route's configuration and will run
|
||||
// before the route's handler is called. Results are set on
|
||||
// the request.pre object using the method's name for the property name,
|
||||
// or otherwise using the value of the "assign" property.
|
||||
//
|
||||
// Methods specified as strings are helpers. Check ./helpers.js for
|
||||
// definitions.
|
||||
|
||||
module.exports = {
|
||||
email: {
|
||||
method: 'email(payload.email)'
|
||||
},
|
||||
assertion: {
|
||||
method: 'verify(payload.assertion, server.settings.uri)',
|
||||
assign: 'email'
|
||||
},
|
||||
userId: {
|
||||
method: 'userId(pre.email)'
|
||||
},
|
||||
user: {
|
||||
method: 'user(pre.userId)'
|
||||
}
|
||||
};
|
|
@ -0,0 +1,183 @@
|
|||
var uuid = require('node-uuid');
|
||||
var util = require('./util.js');
|
||||
var async = require('async');
|
||||
var db = require('./db.js');
|
||||
var devices = require('./devices.js');
|
||||
|
||||
// kv will be initialized once the database connection has been established
|
||||
var kv;
|
||||
db.onconnect(function(err, conn) { kv = conn; });
|
||||
|
||||
/* user account model
|
||||
*
|
||||
* user should have account id
|
||||
* <email>/userid = <userId>
|
||||
* <userId>/devices = {
|
||||
* <deviceId> = {
|
||||
* latestKey: <true/false>
|
||||
* lastKeyRequest: <time>
|
||||
* },
|
||||
* <deviceId> = { },
|
||||
* <userId>/meta = {
|
||||
* kA: <kA key>
|
||||
* kA_version: <kA version>
|
||||
* }
|
||||
*
|
||||
* */
|
||||
|
||||
exports.create = db.wait(function(email, cb) {
|
||||
// generate user id
|
||||
var userId = uuid.v4();
|
||||
var metaKey = userId + '/meta';
|
||||
var devicesKey = userId + '/devices';
|
||||
var kA, deviceId;
|
||||
|
||||
async.waterfall([
|
||||
// link email to userid
|
||||
function(cb) {
|
||||
kv.set(email + '/userid', userId, cb);
|
||||
},
|
||||
// get new class A key
|
||||
util.getKA,
|
||||
// create user account
|
||||
function(key, cb) {
|
||||
kA = key;
|
||||
kv.set(metaKey, { kA: key, kA_version: 1 }, cb);
|
||||
},
|
||||
// get new device id
|
||||
util.getDeviceId,
|
||||
// init devices
|
||||
function(id, cb) {
|
||||
deviceId = id;
|
||||
var devices = {};
|
||||
devices[deviceId] = initDevice();
|
||||
kv.set(devicesKey, devices, cb);
|
||||
},
|
||||
// return data
|
||||
function(cb) {
|
||||
cb(null, { kA: kA, deviceId: deviceId, version: 1 });
|
||||
}
|
||||
], cb);
|
||||
});
|
||||
|
||||
// This method returns the userId currently associated with an email address.
|
||||
exports.getId = db.wait(function(email, cb) {
|
||||
kv.get(email + '/userid', function(err, result) {
|
||||
cb(err, result && result.value);
|
||||
});
|
||||
});
|
||||
|
||||
// This method creates a new device for the user
|
||||
exports.addDevice = db.wait(function(userId, cb) {
|
||||
var metaKey = userId + '/meta';
|
||||
var devicesKey = userId + '/devices';
|
||||
var id;
|
||||
async.waterfall([
|
||||
// get new device id
|
||||
util.getDeviceId,
|
||||
// get user devices
|
||||
function(deviceId, cb) {
|
||||
id = deviceId;
|
||||
kv.get(devicesKey, cb);
|
||||
},
|
||||
// save account
|
||||
function(devices, cb) {
|
||||
if (!devices) return cb('UnknownUser');
|
||||
devices.value[id] = initDevice();
|
||||
kv.set(devicesKey, devices.value, function(err) { cb(err, id); });
|
||||
}
|
||||
], cb);
|
||||
});
|
||||
|
||||
// Whenever a device requests the latest key, we update
|
||||
// its meta data to show that it's up to date
|
||||
exports.updateDevice = db.wait(function(userId, deviceId, cb) {
|
||||
var devicesKey = userId + '/devices';
|
||||
async.waterfall([
|
||||
|
||||
// get device info
|
||||
function(cb) { kv.get(devicesKey, cb); },
|
||||
|
||||
// update devices info and save
|
||||
function(devices, cb) {
|
||||
if (!devices) return cb('UnknownUser');
|
||||
if (!devices.value[deviceId]) return cb('UnknownDevice');
|
||||
// device has the latest version
|
||||
devices.value[deviceId].latestVersion = true;
|
||||
// device's last request was now
|
||||
devices.value[deviceId].lastKeyRequest = +new Date();
|
||||
kv.set(devicesKey, devices.value, cb);
|
||||
}
|
||||
], cb);
|
||||
});
|
||||
|
||||
// When a device bumps the user's class A key, we indicate that all other
|
||||
// devices have a stale key
|
||||
exports.outdateDevices = db.wait(function(userId, deviceId, cb) {
|
||||
var devicesKey = userId + '/devices';
|
||||
async.waterfall([
|
||||
|
||||
// get device info
|
||||
function(cb) { kv.get(devicesKey, cb); },
|
||||
|
||||
// update devices info and save
|
||||
function(devices, cb) {
|
||||
if (!devices) return cb('UnknownUser');
|
||||
if (!devices.value[deviceId]) return cb('UnknownDevice');
|
||||
|
||||
for (var id in devices.value) {
|
||||
// all other devices have a stale key
|
||||
if (id !== deviceId) devices.value[id].latestVersion = false;
|
||||
}
|
||||
|
||||
// device's last request was now
|
||||
devices.value[deviceId].lastKeyRequest = +new Date();
|
||||
kv.set(devicesKey, devices.value, cb);
|
||||
}
|
||||
], cb);
|
||||
});
|
||||
|
||||
// When a device bumps the user's class A key, we indicate that all other
|
||||
// devices have a stale key
|
||||
exports.bumpkA = db.wait(function(userId, cb) {
|
||||
var metaKey = userId + '/meta';
|
||||
var user;
|
||||
|
||||
async.waterfall([
|
||||
|
||||
// get user info
|
||||
function(cb) { kv.get(metaKey, cb); },
|
||||
function(meta, cb) { user = meta; cb(null); },
|
||||
|
||||
// get new class A key
|
||||
util.getKA,
|
||||
|
||||
// update devices info and save
|
||||
function(kA, cb) {
|
||||
if (!user) return cb('UnknownUser');
|
||||
|
||||
// set new class A key and increment version
|
||||
user.value.kA = kA;
|
||||
user.value.kA_version++;
|
||||
kv.set(metaKey, user.value, function(err) { cb(err, user.value); });
|
||||
}
|
||||
], cb);
|
||||
});
|
||||
|
||||
// get meta data associated with a user
|
||||
exports.getUser = db.wait(function(userId, cb) {
|
||||
kv.get(userId + '/meta', function(err, doc) {
|
||||
if (err) return cb(err);
|
||||
if (!doc) return cb('UnknownUser');
|
||||
cb(null, doc.value);
|
||||
});
|
||||
});
|
||||
|
||||
// utility for returning a new device object
|
||||
function initDevice() {
|
||||
return {
|
||||
hasLatestKey: true,
|
||||
lastKeyRequest: +new Date()
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
var crypto = require('crypto');
|
||||
|
||||
function getKA(cb) {
|
||||
return crypto.randomBytes(32, function(err, buf) {
|
||||
cb(null, buf.toString('hex'));
|
||||
});
|
||||
}
|
||||
|
||||
function getDeviceId(cb) {
|
||||
return crypto.randomBytes(32, function(err, buf) {
|
||||
cb(null, buf.toString('hex'));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getKA: getKA,
|
||||
getDeviceId: getDeviceId
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
var https = require('https');
|
||||
var http = require('http');
|
||||
var url = require('url');
|
||||
|
||||
var protocols = {
|
||||
'http:': http,
|
||||
'https:': https
|
||||
};
|
||||
|
||||
// sends an assertion to a verification server
|
||||
module.exports = function verify(assertion, audience, verifier_url, cb) {
|
||||
var assertion = JSON.stringify({
|
||||
assertion: assertion,
|
||||
audience: audience
|
||||
});
|
||||
|
||||
var verifier = url.parse(verifier_url);
|
||||
|
||||
var vreq = protocols[verifier.protocol].request({
|
||||
host: verifier.hostname,
|
||||
port: verifier.port,
|
||||
path: verifier.path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Length': assertion.length,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, function (vres) {
|
||||
var result = "";
|
||||
vres.on('data', function(chunk) { result += chunk; });
|
||||
vres.on('end', function() {
|
||||
try {
|
||||
var data = JSON.parse(result);
|
||||
if (data.status === 'okay') {
|
||||
cb(null, data);
|
||||
} else {
|
||||
cb(data.reason);
|
||||
}
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
vreq.on('error', function(e) {
|
||||
console.error('problem with request: ' + e.message, e.stack);
|
||||
});
|
||||
vreq.write(assertion);
|
||||
vreq.end();
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "picl-keyserver",
|
||||
"version": "0.0.0",
|
||||
"description": "keyserver bits for Profile-In-the-CLoud",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test ./node_modules/.bin/mocha -R spec --recursive --timeout 10000 --ignore-leaks",
|
||||
"start": "NODE_ENV=local node index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/mozilla/picl-keyserver.git"
|
||||
},
|
||||
"license": "MPL 2.0",
|
||||
"dependencies": {
|
||||
"hapi": "git://github.com/zaach/hapi.git#develop",
|
||||
"awsbox": "0.4.1",
|
||||
"convict": "git://github.com/zaach/node-convict.git#disorderly",
|
||||
"picl-server": "git://github.com/mozilla/picl-server.git",
|
||||
"async": "0.1.22",
|
||||
"node-uuid": "1.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"couchbase": "0.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.8.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const Hapi = require('hapi');
|
||||
const users = require('../lib/users.js');
|
||||
const prereqs = require('../lib/prereqs.js');
|
||||
|
||||
const S = Hapi.Types.String;
|
||||
const B = Hapi.Types.Boolean;
|
||||
const N = Hapi.Types.Number;
|
||||
|
||||
var routes = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/__heartbeat__',
|
||||
config: {
|
||||
handler: heartbeat
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/user/create',
|
||||
handler: create,
|
||||
config: {
|
||||
description: 'create a new user',
|
||||
pre: [ prereqs.email ],
|
||||
validate: {
|
||||
schema: {
|
||||
assertion: S(),
|
||||
email: S()
|
||||
}
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
success: B().required(),
|
||||
kA: S().required(),
|
||||
deviceId: S().required(),
|
||||
version: N().integer().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/device/create',
|
||||
handler: device,
|
||||
config: {
|
||||
description: 'create a new device for the user',
|
||||
pre: [ prereqs.email, prereqs.userId, prereqs.user ],
|
||||
validate: {
|
||||
schema: {
|
||||
assertion: S(),
|
||||
email: S()
|
||||
}
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
success: B().required(),
|
||||
kA: S().required(),
|
||||
deviceId: S().required(),
|
||||
version: N().integer().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/user/get/{deviceId}',
|
||||
handler: get,
|
||||
config: {
|
||||
description: 'get user meta data',
|
||||
pre: [ prereqs.email, prereqs.userId, prereqs.user ],
|
||||
validate: {
|
||||
schema: {
|
||||
assertion: S(),
|
||||
email: S()
|
||||
}
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
success: B().required(),
|
||||
kA: S().required(),
|
||||
version: N().integer().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/user/bump/{deviceId}',
|
||||
handler: bump,
|
||||
config: {
|
||||
description: 'get user meta data',
|
||||
pre: [ prereqs.email, prereqs.userId ],
|
||||
validate: {
|
||||
schema: {
|
||||
assertion: S(),
|
||||
email: S()
|
||||
}
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
success: B().required(),
|
||||
kA: S().required(),
|
||||
version: N().integer().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// heartbeat
|
||||
function heartbeat(request) {
|
||||
request.reply.payload('ok').type('text/plain').send();
|
||||
}
|
||||
|
||||
// create a user by assertion
|
||||
function create(request) {
|
||||
users.create(request.pre.email, function(err, result) {
|
||||
if (err) return request.reply(Hapi.Error.badRequest(err));
|
||||
|
||||
request.reply({
|
||||
success: true,
|
||||
kA: result.kA,
|
||||
deviceId: result.deviceId,
|
||||
version: result.version
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// add a new device to a user's account
|
||||
function device(request) {
|
||||
var user = request.pre.user;
|
||||
|
||||
users.addDevice(request.pre.userId, function(err, deviceId) {
|
||||
if (err) return request.reply(Hapi.Error.badRequest(err));
|
||||
|
||||
request.reply({
|
||||
success: true,
|
||||
kA: user.kA,
|
||||
deviceId: deviceId,
|
||||
version: user.kA_version
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// get a user's class A key and its version number
|
||||
function get(request) {
|
||||
var pre = request.pre;
|
||||
|
||||
// update the device's last kA request time
|
||||
users.updateDevice(pre.userId, request.params.deviceId, function(err) {
|
||||
if (err) return request.reply(Hapi.Error.badRequest(err));
|
||||
|
||||
request.reply({
|
||||
success: true,
|
||||
kA: pre.user.kA,
|
||||
version: pre.user.kA_version
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// create a new class A key and bump the version number
|
||||
function bump(request) {
|
||||
var pre = request.pre;
|
||||
|
||||
// mark all other devices as having a stale key
|
||||
users.outdateDevices(pre.userId, request.params.deviceId, function(err) {
|
||||
if (err) return request.reply(Hapi.Error.badRequest(err));
|
||||
|
||||
// create a new key and bump the version
|
||||
users.bumpkA(pre.userId, function(err, user) {
|
||||
if (err) return request.reply(Hapi.Error.badRequest(err));
|
||||
|
||||
request.reply({
|
||||
success: true,
|
||||
kA: user.kA,
|
||||
version: user.kA_version
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = routes;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const Hapi = require('hapi');
|
||||
|
||||
var config = require('./lib/config.js');
|
||||
var helpers = require('./lib/helpers.js');
|
||||
|
||||
// load array of routes
|
||||
var routes = require('./routes');
|
||||
|
||||
// server settings
|
||||
var settings = {
|
||||
monitor: true
|
||||
};
|
||||
|
||||
// Create a server with a host and port
|
||||
var port = config.get('bind_to.port');
|
||||
var host = config.get('bind_to.host');
|
||||
var server = new Hapi.Server(host, port, settings);
|
||||
|
||||
|
||||
server.addHelper('email', helpers.email);
|
||||
server.addHelper('verify', helpers.verify);
|
||||
server.addHelper('userId', helpers.userId);
|
||||
server.addHelper('user', helpers.user);
|
||||
|
||||
server.addRoutes(routes);
|
||||
|
||||
|
||||
module.exports = server;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
var http = require('http');
|
||||
|
||||
exports.server = require('../server');
|
||||
|
||||
exports.makeRequest = function (method, path, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var next = function (res) {
|
||||
return callback(res);
|
||||
};
|
||||
|
||||
this.inject({
|
||||
method: method,
|
||||
url: path,
|
||||
payload: JSON.stringify(options.payload),
|
||||
headers: options.headers
|
||||
}, next);
|
||||
};
|
||||
|
||||
exports.getUser = function(audience, cb) {
|
||||
console.log('getting aud', encodeURIComponent(audience));
|
||||
|
||||
var req = http.request({
|
||||
host: 'personatestuser.org',
|
||||
path: '/email_with_assertion/' + encodeURIComponent(audience) + '/prod',
|
||||
method: 'GET'
|
||||
}, function (res) {
|
||||
var result = '';
|
||||
res.on('data', function(chunk) { result += chunk; });
|
||||
res.on('end', function() {
|
||||
try {
|
||||
var data = JSON.parse(result);
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
cb(null, data);
|
||||
});
|
||||
});
|
||||
req.on('error', function(e) {
|
||||
console.error('get user error:', e);
|
||||
cb(e);
|
||||
});
|
||||
req.end();
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
var assert = require('assert');
|
||||
var helpers = require('../helpers');
|
||||
var server = helpers.server;
|
||||
var makeRequest = helpers.makeRequest.bind(server);
|
||||
|
||||
describe('heartbeat', function() {
|
||||
it('returns ok', function(done) {
|
||||
makeRequest('GET', '/__heartbeat__', function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(res.result, 'ok');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
var assert = require('assert');
|
||||
var config = require('../../lib/config');
|
||||
var helpers = require('../helpers');
|
||||
|
||||
var server = helpers.server;
|
||||
var makeRequest = helpers.makeRequest.bind(server);
|
||||
|
||||
var TEST_AUDIENCE = config.get('public_url');
|
||||
var TEST_EMAIL;
|
||||
var TEST_ASSERTION;
|
||||
var TEST_TOKEN = 'foobar';
|
||||
|
||||
/*describe('get user', function() {*/
|
||||
//it('can get user email and assertion', function(done) {
|
||||
//helpers.getUser(TEST_AUDIENCE, function(err, user) {
|
||||
|
||||
//TEST_EMAIL = user.email;
|
||||
//TEST_ASSERTION = user.assertion;
|
||||
|
||||
//assert.ok(TEST_EMAIL);
|
||||
//assert.ok(TEST_ASSERTION);
|
||||
|
||||
//done();
|
||||
//});
|
||||
//});
|
||||
/*});*/
|
||||
|
||||
describe('user', function() {
|
||||
var kA, version, deviceId;
|
||||
|
||||
it('should create a new account', function(done) {
|
||||
makeRequest('PUT', '/user/create', {
|
||||
//payload: { assertion: TEST_ASSERTION }
|
||||
payload: { email: TEST_EMAIL }
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.ok(res.result.kA);
|
||||
assert.ok(res.result.deviceId);
|
||||
assert.equal(res.result.version, 1);
|
||||
|
||||
kA = res.result.kA;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a new device', function(done) {
|
||||
makeRequest('PUT', '/device/create', {
|
||||
//payload: { assertion: TEST_ASSERTION }
|
||||
payload: { email: TEST_EMAIL }
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(kA, res.result.kA);
|
||||
assert.ok(res.result.deviceId);
|
||||
assert.equal(res.result.version, 1);
|
||||
|
||||
deviceId = res.result.deviceId;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get user info', function(done) {
|
||||
makeRequest('PUT', '/user/get/' + deviceId, {
|
||||
//payload: { assertion: TEST_ASSERTION }
|
||||
payload: { email: TEST_EMAIL }
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(kA, res.result.kA);
|
||||
assert.equal(res.result.version, 1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should bump version', function(done) {
|
||||
makeRequest('PUT', '/user/bump/' + deviceId, {
|
||||
//payload: { assertion: TEST_ASSERTION }
|
||||
payload: { email: TEST_EMAIL }
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.notEqual(kA, res.result.kA);
|
||||
assert.equal(res.result.version, 2);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Загрузка…
Ссылка в новой задаче