Browse Source

Merge pull request #556 from isocolsky/feat/login

Authentication with session token
activeAddress
Matias Alejo Garcia 8 years ago
committed by GitHub
parent
commit
e6f732f03a
  1. 2
      lib/common/defaults.js
  2. 36
      lib/expressapp.js
  3. 1
      lib/model/index.js
  4. 52
      lib/model/session.js
  5. 111
      lib/server.js
  6. 22
      lib/storage.js
  7. 99
      test/integration/server.js

2
lib/common/defaults.js

@ -88,4 +88,6 @@ Defaults.BLOCKHEIGHT_CACHE_TIME = 10 * 60;
Defaults.MAX_NOTIFICATIONS_TIMESPAN = 60 * 60 * 24 * 14; // ~ 2 weeks
Defaults.NOTIFICATIONS_TIMESPAN = 60;
Defaults.SESSION_EXPIRATION = 1 * 60 * 60; // 1 hour to session expiration
module.exports = Defaults;

36
lib/expressapp.js

@ -38,7 +38,7 @@ ExpressApp.prototype.start = function(opts, cb) {
this.app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-client-version,X-Requested-With,Content-Type,Authorization');
res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,X-Requested-With,Content-Type,Authorization');
res.setHeader('x-service-version', WalletService.getServiceVersion());
next();
});
@ -125,6 +125,7 @@ ExpressApp.prototype.start = function(opts, cb) {
return {
copayerId: identity,
signature: req.header('x-signature'),
session: req.header('x-session'),
};
};
@ -135,7 +136,13 @@ ExpressApp.prototype.start = function(opts, cb) {
return WalletService.getInstance(opts);
};
function getServerWithAuth(req, res, cb) {
function getServerWithAuth(req, res, opts, cb) {
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
opts = opts || {};
var credentials = getCredentials(req);
if (!credentials)
return returnError(new WalletService.ClientError({
@ -148,6 +155,9 @@ ExpressApp.prototype.start = function(opts, cb) {
signature: credentials.signature,
clientVersion: req.header('x-client-version'),
};
if (opts.allowSession) {
auth.session = credentials.session;
}
WalletService.getInstanceWithAuth(auth, function(err, server) {
if (err) return returnError(err, res, req);
@ -554,11 +564,29 @@ ExpressApp.prototype.start = function(opts, cb) {
res.end();
});
router.get('/v1/notifications/', function(req, res) {
router.post('/v1/login/', function(req, res) {
getServerWithAuth(req, res, function(server) {
var timeSpan = req.query.timeSpan ? Math.min(+req.query.timeSpan || 0, Defaults.MAX_NOTIFICATIONS_TIMESPAN) : Defaults.NOTIFICATIONS_TIMESPAN;
server.login({}, function(err, session) {
if (err) return returnError(err, res, req);
res.json(session);
});
});
});
router.post('/v1/logout/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.logout({}, function(err) {
if (err) return returnError(err, res, req);
res.end();
});
});
});
router.get('/v1/notifications/', function(req, res) {
getServerWithAuth(req, res, {
allowSession: true,
}, function(server) {
var timeSpan = req.query.timeSpan ? Math.min(+req.query.timeSpan || 0, Defaults.MAX_NOTIFICATIONS_TIMESPAN) : Defaults.NOTIFICATIONS_TIMESPAN;
var opts = {
minTs: +Date.now() - (timeSpan * 1000),
notificationId: req.query.notificationId,

1
lib/model/index.js

@ -8,5 +8,6 @@ Model.Notification = require('./notification');
Model.Preferences = require('./preferences');
Model.Email = require('./email');
Model.TxNote = require('./txnote');
Model.Session = require('./session');
module.exports = Model;

52
lib/model/session.js

@ -0,0 +1,52 @@
var _ = require('lodash');
var Uuid = require('uuid');
var Defaults = require('../common/defaults');
function Session() {};
Session.create = function(opts) {
opts = opts || {};
var now = Math.floor(Date.now() / 1000);
var x = new Session();
x.id = Uuid.v4();
x.version = 1;
x.createdOn = now;
x.updatedOn = now;
x.copayerId = opts.copayerId;
x.walletId = opts.walletId;
return x;
};
Session.fromObj = function(obj) {
var x = new Session();
x.id = obj.id;
x.version = obj.version;
x.createdOn = obj.createdOn;
x.updatedOn = obj.updatedOn;
x.copayerId = obj.copayerId;
x.walletId = obj.walletId;
return x;
};
Session.prototype.toObject = function() {
return this;
};
Session.prototype.isValid = function() {
var now = Math.floor(Date.now() / 1000);
return (now - this.updatedOn) <= Defaults.SESSION_EXPIRATION;
};
Session.prototype.touch = function() {
var now = Math.floor(Date.now() / 1000);
this.updatedOn = now;
};
module.exports = Session;

111
lib/server.js

@ -196,32 +196,65 @@ WalletService.getInstance = function(opts) {
* Gets an instance of the server after authenticating the copayer.
* @param {Object} opts
* @param {string} opts.copayerId - The copayer id making the request.
* @param {string} opts.message - The contents of the request to be signed.
* @param {string} opts.signature - Signature of message to be verified using one of the copayer's requestPubKeys
* @param {string} opts.message - (Optional) The contents of the request to be signed. Only needed if no session token is provided.
* @param {string} opts.signature - (Optional) Signature of message to be verified using one of the copayer's requestPubKeys. Only needed if no session token is provided.
* @param {string} opts.session - (Optional) A valid session token previously obtained using the #login method
* @param {string} opts.clientVersion - A string that identifies the client issuing the request
*/
WalletService.getInstanceWithAuth = function(opts, cb) {
if (!checkRequired(opts, ['copayerId', 'message', 'signature'], cb)) return;
function withSignature(cb) {
if (!checkRequired(opts, ['copayerId', 'message', 'signature'], cb)) return;
var server;
try {
server = WalletService.getInstance(opts);
} catch (ex) {
return cb(ex);
}
var server;
try {
server = WalletService.getInstance(opts);
} catch (ex) {
return cb(ex);
}
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
if (err) return cb(err);
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
if (err) return cb(err);
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
if (!isValid)
return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature'));
var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
if (!isValid)
return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature'));
server.copayerId = opts.copayerId;
server.walletId = copayer.walletId;
return cb(null, server);
});
server.copayerId = opts.copayerId;
server.walletId = copayer.walletId;
return cb(null, server);
});
};
function withSession(cb) {
if (!checkRequired(opts, ['copayerId', 'session'], cb)) return;
var server;
try {
server = WalletService.getInstance(opts);
} catch (ex) {
return cb(ex);
}
server.storage.getSession(opts.copayerId, function(err, s) {
if (err) return cb(err);
var isValid = s && s.id == opts.session && s.isValid();
if (!isValid) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Session expired'));
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
if (err) return cb(err);
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
server.copayerId = opts.copayerId;
server.walletId = copayer.walletId;
return cb(null, server);
});
});
};
var authFn = opts.session ? withSession : withSignature;
return authFn(cb);
};
WalletService.prototype._runLocked = function(cb, task) {
@ -229,6 +262,46 @@ WalletService.prototype._runLocked = function(cb, task) {
this.lock.runLocked(this.walletId, cb, task);
};
WalletService.prototype.login = function(opts, cb) {
var self = this;
var session;
async.series([
function(next) {
self.storage.getSession(self.copayerId, function(err, s) {
if (err) return next(err);
session = s;
next();
});
},
function(next) {
if (!session || !session.isValid()) {
session = Model.Session.create({
copayerId: self.copayerId,
walletId: self.walletId,
});
} else {
session.touch();
}
next();
},
function(next) {
self.storage.storeSession(session, next);
},
], function(err) {
if (err) return cb(err);
if (!session) return cb(new Error('Could not get current session for this copayer'));
return cb(null, session.id);
});
};
WalletService.prototype.logout = function(opts, cb) {
var self = this;
self.storage.removeSession(self.copayerId, cb);
};
/**
* Creates a new wallet.

22
lib/storage.js

@ -23,6 +23,7 @@ var collections = {
CACHE: 'cache',
FIAT_RATES: 'fiat_rates',
TX_NOTES: 'tx_notes',
SESSIONS: 'sessions',
};
var Storage = function(opts) {
@ -868,6 +869,27 @@ Storage.prototype.storeTxNote = function(txNote, cb) {
}, cb);
};
Storage.prototype.getSession = function(copayerId, cb) {
var self = this;
self.db.collection(collections.SESSIONS).findOne({
copayerId: copayerId,
},
function(err, result) {
if (err || !result) return cb(err);
return cb(null, Model.Session.fromObj(result));
});
};
Storage.prototype.storeSession = function(session, cb) {
this.db.collection(collections.SESSIONS).update({
copayerId: session.copayerId,
}, session.toObject(), {
w: 1,
upsert: true,
}, cb);
};
Storage.prototype._dump = function(cb, fn) {
fn = fn || console.log;
cb = cb || function() {};

99
test/integration/server.js

@ -142,6 +142,105 @@ describe('Wallet service', function() {
});
});
describe('Session management (#login, #logout, #authenticate)', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should get a new session & authenticate', function(done) {
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: 'dummy',
}, function(err, server2) {
should.exist(err);
err.code.should.equal('NOT_AUTHORIZED');
err.message.toLowerCase().should.contain('session');
should.not.exist(server2);
server.login({}, function(err, token) {
should.not.exist(err);
should.exist(token);
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: token,
}, function(err, server2) {
should.not.exist(err);
should.exist(server2);
server2.copayerId.should.equal(server.copayerId);
server2.walletId.should.equal(server.walletId);
done();
});
});
});
});
it('should get the same session token for two requests in a row', function(done) {
server.login({}, function(err, token) {
should.not.exist(err);
should.exist(token);
server.login({}, function(err, token2) {
should.not.exist(err);
token2.should.equal(token);
done();
});
});
});
it('should create a new session if the previous one has expired', function(done) {
var timer = sinon.useFakeTimers('Date');
var token;
async.series([
function(next) {
server.login({}, function(err, t) {
should.not.exist(err);
should.exist(t);
token = t;
next();
});
},
function(next) {
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: token,
}, function(err, server2) {
should.not.exist(err);
should.exist(server2);
next();
});
},
function(next) {
timer.tick((Defaults.SESSION_EXPIRATION + 1) * 1000);
next();
},
function(next) {
server.login({}, function(err, t) {
should.not.exist(err);
t.should.not.equal(token);
next();
});
},
function(next) {
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: token,
}, function(err, server2) {
should.exist(err);
err.code.should.equal('NOT_AUTHORIZED');
err.message.should.contain('expired');
next();
});
},
], function(err) {
should.not.exist(err);
timer.restore();
done();
});
});
});
describe('#createWallet', function() {
var server;
beforeEach(function() {

Loading…
Cancel
Save