forked from coolaj86/goldilocks.js
		
	
		
			
				
	
	
		
			226 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			226 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| 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;
 | |
|   var scmp = require('scmp');
 | |
|   var storageDir = path.join(__dirname, '..', 'var');
 | |
| 
 | |
|   function read(fileName) {
 | |
|     return fs.readFileAsync(path.join(storageDir, fileName))
 | |
|     .then(JSON.parse, function (err) {
 | |
|       if (err.code === 'ENOENT') {
 | |
|         return {};
 | |
|       }
 | |
|       throw err;
 | |
|     });
 | |
|   }
 | |
|   function write(fileName, obj) {
 | |
|     return fs.mkdirAsync(storageDir).catch(function (err) {
 | |
|       if (err.code !== 'EEXIST') {
 | |
|         console.error('failed to mkdir', storageDir, err.toString());
 | |
|       }
 | |
|     }).then(function () {
 | |
|       return fs.writeFileAsync(path.join(storageDir, fileName), JSON.stringify(obj), 'utf8');
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   var owners = {
 | |
|     _filename: 'owners.json'
 | |
|   , all: function () {
 | |
|       return read(this._filename).then(function (owners) {
 | |
|         return Object.keys(owners).map(function (id) {
 | |
|           var owner = owners[id];
 | |
|           owner.id = id;
 | |
|           return owner;
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   , get: function (id) {
 | |
|       // While we could directly read the owners file and access the id directly from
 | |
|       // the resulting object I'm not sure of the details of how the object key lookup
 | |
|       // works or whether that would expose us to timing attacks.
 | |
|       // See https://codahale.com/a-lesson-in-timing-attacks/
 | |
|       return this.all().then(function (owners) {
 | |
|         return owners.filter(function (owner) {
 | |
|           return scmp(id, owner.id);
 | |
|         })[0];
 | |
|       });
 | |
|     }
 | |
|   , exists: function (id) {
 | |
|       return this.get(id).then(function (owner) {
 | |
|         return !!owner;
 | |
|       });
 | |
|     }
 | |
|   , set: function (id, obj) {
 | |
|       var self = this;
 | |
|       return read(self._filename).then(function (owners) {
 | |
|         obj.id = id;
 | |
|         owners[id] = obj;
 | |
|         return write(self._filename, owners);
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   var confCb;
 | |
|   var config = {
 | |
|     save: function (changes) {
 | |
|       deps.messenger.send({
 | |
|         type: 'com.daplie.goldilocks/config'
 | |
|       , changes: changes
 | |
|       });
 | |
| 
 | |
|       return new deps.PromiseA(function (resolve, reject) {
 | |
|         var timeoutId = setTimeout(function () {
 | |
|           reject(new Error('Did not receive config update from main process in a reasonable time'));
 | |
|           confCb = null;
 | |
|         }, 15*1000);
 | |
| 
 | |
|         confCb = function (config) {
 | |
|           confCb = null;
 | |
|           clearTimeout(timeoutId);
 | |
|           resolve(config);
 | |
|         };
 | |
|       });
 | |
|     }
 | |
|   };
 | |
|   function updateConf(config) {
 | |
|     if (confCb) {
 | |
|       confCb(config);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   var userTokens = {
 | |
|     _filename: 'user-tokens.json'
 | |
|   , _cache: {}
 | |
|   , _convertToken: function 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);
 | |
|       if (!decoded) {
 | |
|         return null;
 | |
|       }
 | |
|       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;
 | |
|       if (self._cacheComplete) {
 | |
|         return deps.PromiseA.resolve(Object.values(self._cache));
 | |
|       }
 | |
| 
 | |
|       return read(self._filename).then(function (tokens) {
 | |
|         // We will read every single token into our cache, so it will be complete once we finish
 | |
|         // creating the result (it's set out of order so we can directly return the result).
 | |
|         self._cacheComplete = true;
 | |
| 
 | |
|         return Object.keys(tokens).map(function (id) {
 | |
|           self._cache[id] = self._convertToken(id, tokens[id]);
 | |
|           return self._cache[id];
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   , get: function getUserToken(id) {
 | |
|       var self = this;
 | |
|       if (self._cache.hasOwnProperty(id) || self._cacheComplete) {
 | |
|         return deps.PromiseA.resolve(self._cache[id] || null);
 | |
|       }
 | |
| 
 | |
|       return read(self._filename).then(function (tokens) {
 | |
|         self._cache[id] = self._convertToken(id, tokens[id]);
 | |
|         return self._cache[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 () {
 | |
|           // Delete the current cache so that if this is an update it will refresh
 | |
|           // the cache once we read the ID.
 | |
|           delete self._cache[id];
 | |
|           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 () {
 | |
|           delete self._cache[id];
 | |
|           return true;
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   var mdnsId = {
 | |
|     _filename: 'mdns-id'
 | |
|   , get: function () {
 | |
|       var self = this;
 | |
|       return read("mdns-id").then(function (result) {
 | |
|         if (typeof result !== 'string') {
 | |
|           throw new Error('mDNS ID not present');
 | |
|         }
 | |
|         return result;
 | |
|       }).catch(function () {
 | |
|         return self.set(hrIds.random());
 | |
|       });
 | |
|     }
 | |
| 
 | |
|   , set: function (value) {
 | |
|       var self = this;
 | |
|       return write(self._filename, value).then(function () {
 | |
|         return self.get();
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return {
 | |
|     owners: owners
 | |
|   , config: config
 | |
|   , updateConf: updateConf
 | |
|   , tokens: userTokens
 | |
|   , mdnsId: mdnsId
 | |
|   };
 | |
| };
 |