From cfaa8d495984f0d6efde45cb737858b579620aed Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 17 Oct 2017 18:36:36 -0600 Subject: [PATCH] added interface to save user tokens --- lib/admin/apis.js | 27 ++++++++++++++++ lib/storage.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/lib/admin/apis.js b/lib/admin/apis.js index 19c05b9..5bbe49a 100644 --- a/lib/admin/apis.js +++ b/lib/admin/apis.js @@ -50,6 +50,9 @@ module.exports.create = function (deps, conf) { prom.then(function (result) { res.send(deps.recase.snakeCopy(result)); }).catch(function (err) { + if (conf.debug) { + console.log(err); + } res.statusCode = err.statusCode || 500; err.message = err.message || err.toString(); res.end(JSON.stringify({error: {message: err.message, code: err.code}})); @@ -524,6 +527,24 @@ module.exports.create = function (deps, conf) { handlePromise(req, res, promise); }; + var tokens = { restful: {} }; + tokens.restful.getAll = function (req, res) { + handlePromise(req, res, deps.storage.tokens.all()); + }; + tokens.restful.getOne = function (req, res) { + handlePromise(req, res, deps.storage.tokens.get(req.params.id)); + }; + tokens.restful.save = function (req, res) { + handlePromise(req, res, deps.storage.tokens.save(req.body)); + }; + tokens.restful.revoke = function (req, res) { + var promise = deps.storage.tokens.remove(req.params.id).then(function (success) { + return {success: success}; + }); + handlePromise(req, res, promise); + }; + + var app = require('express')(); // Handle all of the API endpoints using the old definition style, and then we can @@ -555,5 +576,11 @@ module.exports.create = function (deps, conf) { app.put( '/config/domains/:domId', config.restful.updateDomain); app.delete('/config/domains/:domId', config.restful.removeDomain); + app.use( '/tokens', makeCorsHandler(['GET', 'POST', 'DELETE'])); + app.get( '/tokens', tokens.restful.getAll); + app.get( '/tokens/:id', tokens.restful.getOne); + app.post( '/tokens', tokens.restful.save); + app.delete('/tokens/:id', tokens.restful.revoke); + return app; }; diff --git a/lib/storage.js b/lib/storage.js index 33fc8f6..81d64d1 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -3,6 +3,8 @@ var PromiseA = require('bluebird'); var path = require('path'); var fs = PromiseA.promisifyAll(require('fs')); +var jwt = require('jsonwebtoken'); +var crypto = require('crypto'); module.exports.create = function (deps, conf) { var hrIds = require('human-readable-ids').humanReadableIds; @@ -93,6 +95,82 @@ module.exports.create = function (deps, conf) { } } + var userTokens = { + _filename: 'user-tokens.json' + , _convertToken(id, token) { + // convert the token into something that looks more like what OAuth3 uses internally + // as sessions so we can use it with OAuth3. We don't use OAuth3's internal session + // storage because it effectively only supports storing tokens based on provider URI. + // We also use the token as the `access_token` instead of `refresh_token` because the + // refresh functionality is closely tied to the storage. + var decoded = jwt.decode(token); + return { + id: id + , access_token: token + , token: decoded + , provider_uri: decoded.iss || decoded.issuer || decoded.provider_uri + , client_uri: decoded.azp + , scope: decoded.scp || decoded.scope || decoded.grants + }; + } + , all: function allUserTokens() { + var self = this; + return read(self._filename).then(function (tokens) { + return Object.keys(tokens).map(function (id) { + return self._convertToken(id, tokens[id]); + }); + }); + } + , get: function getUserToken(id) { + var self = this; + return read(self._filename).then(function (tokens) { + return self._convertToken(id, tokens[id]); + }); + } + , save: function saveUserToken(newToken) { + var self = this; + return read(self._filename).then(function (tokens) { + var rawToken; + if (typeof newToken === 'string') { + rawToken = newToken; + } else { + rawToken = newToken.refresh_token || newToken.access_token; + } + if (typeof rawToken !== 'string') { + throw new Error('cannot save invalid session: missing refresh_token and access_token'); + } + + var decoded = jwt.decode(rawToken); + var idHash = crypto.createHash('sha256'); + idHash.update(decoded.sub || decoded.ppid || decoded.appScopedId || ''); + idHash.update(decoded.iss || decoded.issuer || ''); + idHash.update(decoded.aud || decoded.audience || ''); + + var scope = decoded.scope || decoded.scp || decoded.grants || ''; + idHash.update(scope.split(/[,\s]+/mg).sort().join(',')); + + var id = idHash.digest('hex'); + tokens[id] = rawToken; + return write(self._filename, tokens).then(function () { + return self.get(id); + }); + }); + } + , remove: function removeUserToken(id) { + var self = this; + return read(self._filename).then(function (tokens) { + var present = delete tokens[id]; + if (!present) { + return present; + } + + return write(self._filename, tokens).then(function () { + return true; + }); + }); + } + }; + var mdnsId = { _filename: 'mdns-id' , get: function () { @@ -119,6 +197,7 @@ module.exports.create = function (deps, conf) { owners: owners , config: config , updateConf: updateConf + , tokens: userTokens , mdnsId: mdnsId }; };