diff --git a/packages/fxa-auth-server/.gitignore b/packages/fxa-auth-server/.gitignore index 711b212c53..0156885576 100644 --- a/packages/fxa-auth-server/.gitignore +++ b/packages/fxa-auth-server/.gitignore @@ -1,2 +1,3 @@ /node_modules +/config/*.json *.swp diff --git a/packages/fxa-auth-server/.travis.yml b/packages/fxa-auth-server/.travis.yml index 114c4cd505..85b0f15178 100644 --- a/packages/fxa-auth-server/.travis.yml +++ b/packages/fxa-auth-server/.travis.yml @@ -21,3 +21,7 @@ env: before_script: - "mysql -e 'DROP DATABASE IF EXISTS test;'" - "mysql -e 'CREATE DATABASE test;'" + - ./scripts/gen_keys.js + +before_install: + - sudo apt-get install libgmp3-dev diff --git a/packages/fxa-auth-server/README.md b/packages/fxa-auth-server/README.md index 8a8ec2d10d..56e76e6d68 100644 --- a/packages/fxa-auth-server/README.md +++ b/packages/fxa-auth-server/README.md @@ -9,9 +9,10 @@ You'll need node 0.10.x or higher and npm to run the server. Clone the git repository and install dependencies: - git://github.com/mozilla/picl-idp.git + git clone git://github.com/mozilla/picl-idp.git cd picl-idp npm install + node ./scripts/gen_keys.js To start the server, run: diff --git a/packages/fxa-auth-server/lib/account.js b/packages/fxa-auth-server/lib/account.js index 6c9275853c..b89a8a5fc0 100644 --- a/packages/fxa-auth-server/lib/account.js +++ b/packages/fxa-auth-server/lib/account.js @@ -137,6 +137,43 @@ exports.finishLogin = function(sessionId, verifier, cb) { ], cb); }; +// Takes an accountToken and creates a new signToken +exports.getSignToken = function(accountToken, cb) { + var accountKey = accountToken + '/accountToken'; + var uid, signToken; + + async.waterfall([ + // Check that the accountToken exists + // and get the associated user id + function(cb) { + kv.get(accountKey, function(err, account) { + if (err) return cb(err); + if (!account) return cb(notFound('UknownAccountToken')); + cb(null, account.value.uid); + }); + }, + // get new signToken + function(id, cb) { + uid = id; + util.getSignToken(cb); + }, + function(token, cb) { + signToken = token; + kv.set(token + '/signer', { + uid: uid, + accessTime: Date.now() + }, cb); + }, + // delete accountToken + function(cb) { + kv.delete(accountToken + '/accountToken', cb); + }, + function(cb) { + cb(null, { signToken: signToken }); + } + ], cb); +}; + // This method returns the userId currently associated with an email address. exports.getId = function(email, cb) { kv.get(email + '/uid', function(err, result) { @@ -155,3 +192,17 @@ exports.getUser = function(userId, cb) { }); }; +// This account principle associated with a singing token +// The principle is the userId combined with the IDP domain +// e.g. 1234@lcip.org +// +exports.getPrinciple = function(token, cb) { + kv.get(token + '/signer', function(err, result) { + if (err) return cb(internalError(err)); + if (!result) return cb(notFound('UnknownSignToken')); + + var principle = result.value.uid + '@' + config.get('domain'); + + cb(null, principle); + }); +}; diff --git a/packages/fxa-auth-server/lib/config.js b/packages/fxa-auth-server/lib/config.js index 04b8f3151b..12483b0409 100644 --- a/packages/fxa-auth-server/lib/config.js +++ b/packages/fxa-auth-server/lib/config.js @@ -23,7 +23,13 @@ var conf = module.exports = convict({ }, domain: { format: "url", - default: "http://127.0.0.1:9000" + default: "127.0.0.1:9000" + }, + secretKeyFile: { + default: "./config/secret-key.json" + }, + publicKeyFile: { + default: "./config/public-key.json" }, kvstore: { backend: { @@ -106,7 +112,7 @@ if (process.env.CONFIG_FILES) { } // set the public url as the issuer domain for assertions -conf.set('domain', url.parse(conf.get('public_url')).hostname); +conf.set('domain', url.parse(conf.get('public_url')).host); if (conf.get('env') === 'test') { if (conf.get('kvstore.backend') === 'mysql') { @@ -114,6 +120,12 @@ if (conf.get('env') === 'test') { } } +const configDir = fs.realpathSync(__dirname + "/../config"); +const pubKeyFile = configDir + "/public-key.json"; +const secretKeyFile = configDir + "/secret-key.json"; +conf.set('secretKeyFile', secretKeyFile); +conf.set('publicKeyFile', pubKeyFile); + conf.validate(); console.log('configuration: ', conf.toString()); diff --git a/packages/fxa-auth-server/lib/prereqs.js b/packages/fxa-auth-server/lib/prereqs.js new file mode 100644 index 0000000000..53c3027d76 --- /dev/null +++ b/packages/fxa-auth-server/lib/prereqs.js @@ -0,0 +1,30 @@ +/* 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 account = require('./account'); + +// 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 = { + principle: { + method: function(request, next) { + if(request.payload.email) return next(request.payload.email); + + var token = request.payload.token; + if (!token) next(Hapi.Error.badRequest('MissingSignToken')); + account.getPrinciple(token, function(err, principle) { + if (err) next(err); + else next(principle); + }); + }, + assign: 'principle' + } +}; diff --git a/packages/fxa-auth-server/lib/util.js b/packages/fxa-auth-server/lib/util.js index 311b5144e6..4fc2ff45b4 100644 --- a/packages/fxa-auth-server/lib/util.js +++ b/packages/fxa-auth-server/lib/util.js @@ -19,6 +19,12 @@ function getAccountToken(cb) { }); } +function getSignToken(cb) { + return crypto.randomBytes(32, function(err, buf) { + cb(null, buf.toString('hex')); + }); +} + function getUserId() { return uuid.v4(); } @@ -32,5 +38,6 @@ module.exports = { getDeviceId: getDeviceId, getUserId: getUserId, getSessionId: getSessionId, - getAccountToken: getAccountToken + getAccountToken: getAccountToken, + getSignToken: getSignToken }; diff --git a/packages/fxa-auth-server/routes/idp.js b/packages/fxa-auth-server/routes/idp.js index d52dfec792..2ecf76395e 100644 --- a/packages/fxa-auth-server/routes/idp.js +++ b/packages/fxa-auth-server/routes/idp.js @@ -3,8 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Hapi = require('hapi'); +const fs = require('fs'); const CC = require('compute-cluster'); const config = require('../lib/config'); +const prereqs = require('../lib/prereqs'); const hour = 1000 * 60 * 60; @@ -58,12 +60,13 @@ var routes = [ path: '/sign', config: { handler: sign, + pre: [ prereqs.principle ], validate: { payload: { - email: Hapi.types.String().required(), // for testing only + email: Hapi.types.String().without('token'), // for testing only publicKey: Hapi.types.String().required(), duration: Hapi.types.Number().integer().min(0).max(24 * hour).required(), - token: Hapi.types.String() + token: Hapi.types.String().without('email') } } } @@ -92,12 +95,29 @@ var routes = [ } } } + }, + { + method: 'POST', + path: '/signToken', + config: { + handler: getSignToken, + validate: { + payload: { + accountToken: Hapi.types.String().required() + }, + response: { + schema: { + signToken: Hapi.types.String().required() + } + } + } + } } ]; function wellKnown(request) { request.reply({ - 'public-key': config.idpPublicKey, + 'public-key': fs.readFileSync(config.get('publicKeyFile')), 'authentication': '/sign_in.html', 'provisioning': '/provision.html' }); @@ -122,9 +142,14 @@ function create(request) { } function sign(request) { - // TODO validate token, get email from token + var principle = request.pre.principle; + cc.enqueue( - request.payload, + { + email: principle, + publicKey: request.payload.publicKey, + duration: request.payload.duration + }, function (err, result) { if (err || result.err) { request.reply(Hapi.error.internal('Unable to sign certificate', err || result.err)); @@ -169,6 +194,21 @@ function finishLogin(request) { } +function getSignToken(request) { + + account.getSignToken( + request.payload.accountToken, + function (err, result) { + if (err) { + request.reply(err); + } + else { + request.reply(result); + } + } + ); +} + module.exports = { routes: routes }; diff --git a/packages/fxa-auth-server/routes/sign.js b/packages/fxa-auth-server/routes/sign.js index 2d021af808..62387534d3 100644 --- a/packages/fxa-auth-server/routes/sign.js +++ b/packages/fxa-auth-server/routes/sign.js @@ -2,12 +2,15 @@ * 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 fs = require('fs'); const jwcrypto = require('jwcrypto'); const config = require('../lib/config'); require('jwcrypto/lib/algs/rs'); require('jwcrypto/lib/algs/ds'); +const _privKey = jwcrypto.loadSecretKey(fs.readFileSync(config.get('secretKeyFile'))); + process.on('message', function (message) { var clientKey = jwcrypto.loadPublicKey(message.publicKey); var now = Date.now(); @@ -19,12 +22,12 @@ process.on('message', function (message) { //TODO: kA, etc }, { - issuer: config.domain, + issuer: config.get('domain'), issuedAt: new Date(now), expiresAt: new Date(now + message.duration) }, null, - config.idpSecretKey, + _privKey, function (err, cert) { process.send({ err: err, cert: cert}); } diff --git a/packages/fxa-auth-server/scripts/gen_keys.js b/packages/fxa-auth-server/scripts/gen_keys.js new file mode 100755 index 0000000000..80c12ddd4a --- /dev/null +++ b/packages/fxa-auth-server/scripts/gen_keys.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node +/* 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/. */ + +/* scripts/gen_keys.js creates public and private keys suitable for + key signing Persona Primary IdP's. + + Usage: + scripts/gen_keys.js + + Will create these files + + server/config/public-key.json + server/config/secret-key.json + + If these files already exist, this script will show an error message + and exit. You must remove both keys if you want to generate a new + keypair. +*/ + +const jwcrypto = require("jwcrypto"); +const fs = require('fs'); +const assert = require("assert"); + +const configDir = fs.realpathSync(__dirname + "/../config"); +const pubKeyFile = configDir + "/public-key.json"; +const secretKeyFile = configDir + "/secret-key.json"; + +require("jwcrypto/lib/algs/rs"); + +try { + assert(fs.existsSync(configDir), "Config dir" + configDir + " not found"); + assert(! fs.existsSync(pubKeyFile), "public key file: ["+pubKeyFile+"] already exists"); + assert(! fs.existsSync(secretKeyFile), "public key file: ["+secretKeyFile+"] already exists"); +} catch(e) { + console.error("Error: " + e.message); + process.exit(1); +} + +console.log("Generating keypair. (install libgmp if this takes more than a second)"); + +// wondering about `keysize: 256`? +// well, 257 = 2048bit key +// still confused? see: https://github.com/mozilla/jwcrypto/blob/master/lib/algs/ds.js#L37-L57 +jwcrypto.generateKeypair( + { algorithm: 'RS', keysize: 256 }, + function(err, keypair) { + + var pubKey = keypair.publicKey.serialize(); + var secretKey = keypair.secretKey.serialize(); + + + fs.writeFileSync(pubKeyFile, pubKey); + console.log("Public Key saved:", pubKeyFile); + + fs.writeFileSync(secretKeyFile, secretKey); + console.log("Secret Key saved:", pubKeyFile); + } +); diff --git a/packages/fxa-auth-server/test/integration/account.js b/packages/fxa-auth-server/test/integration/account.js index 9f140b88a9..9dee6cad2b 100644 --- a/packages/fxa-auth-server/test/integration/account.js +++ b/packages/fxa-auth-server/test/integration/account.js @@ -1,6 +1,10 @@ var assert = require('assert'); //var config = require('../../lib/config'); var helpers = require('../helpers'); +var jwcrypto = require('jwcrypto'); + +// algorithms +require("jwcrypto/lib/algs/rs"); var testClient = new helpers.TestClient(); @@ -9,7 +13,7 @@ var TEST_PASSWORD = 'foo'; var TEST_KB = 'secret!'; describe('user', function() { - var sessionId; + var sessionId, accountToken, pubkey, signToken; it('should create a new account', function(done) { testClient.makeRequest('POST', '/create', { @@ -121,6 +125,8 @@ describe('user', function() { } }, function(res) { try { + accountToken = res.result.accountToken; + assert.ok(res.result.accountToken); assert.ok(res.result.kA); assert.equal(res.result.kB, TEST_KB); @@ -148,5 +154,48 @@ describe('user', function() { }); }); + it('should get signToken', function(done) { + testClient.makeRequest('POST', '/signToken', { + payload: { + accountToken: accountToken + } + }, function(res) { + try { + assert.equal(res.statusCode, 200); + signToken = res.result.signToken; + assert.ok(res.result.signToken); + } catch (e) { + return done(e); + } + done(); + }); + }); + + it('should generate a pubkey', function(done) { + jwcrypto.generateKeypair({ algorithm: "RS", keysize: 64 }, function(err, keypair) { + pubkey = keypair.publicKey; + done(err); + }); + }); + + it('should sign a pubkey', function(done) { + testClient.makeRequest('POST', '/sign', { + payload: { + token: signToken, + publicKey: pubkey.serialize(), + duration: 50000 + } + }, function(res) { + try { + assert.equal(res.statusCode, 200); + // check for rough format of a cert + assert.equal(res.result.split(".").length, 3); + } catch (e) { + return done(e); + } + done(); + }); + }); + });