Compare commits
	
		
			7 Commits
		
	
	
		
			master
			...
			installer-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 82b0fcf00f | |||
| aa28f00a4b | |||
| fc07a8dd30 | |||
| 1e9618f5ec | |||
| 5f1191a6b9 | |||
| 66850535d3 | |||
| 1eb98edd2b | 
							
								
								
									
										43
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								README.md
									
									
									
									
									
								
							@ -19,7 +19,7 @@ Security Features
 | 
			
		||||
* disallows cookies, except for protected static assets
 | 
			
		||||
* api.* subdomain for apis
 | 
			
		||||
* assets.* subdomain for protected assets
 | 
			
		||||
* *must* sit behind a trusted https proxy (such as [Goldilocks](https://git.coolaj86.com/coolaj86/goldilocks.js))
 | 
			
		||||
* *must* sit behind a trusted https proxy (such as [Goldilocks](https://git.daplie.com/Daplie/goldilocks.js))
 | 
			
		||||
* HTTPS-only (checks for X-Forwarded-For)
 | 
			
		||||
* AES, RSA, and ECDSA encryption and signing
 | 
			
		||||
* Safe against CSRF, XSS, and SQL injection
 | 
			
		||||
@ -34,14 +34,14 @@ Application Features
 | 
			
		||||
 | 
			
		||||
* JSON-only expressjs APIs
 | 
			
		||||
* Capability-based permissions system for (oauth3-discoverable) packages such as
 | 
			
		||||
  * large file access (files@oauth3.org)
 | 
			
		||||
  * database access (data@oauth3.org)
 | 
			
		||||
  * scheduling (for background tasks, alerts, alarms, calendars, reminders, etc) (events@oauth3.org)
 | 
			
		||||
  * payments (credit card) (payments@oauth3.org)
 | 
			
		||||
  * email (email@oauth3.org)
 | 
			
		||||
  * SMS (texting) (tel@oauth3.org)
 | 
			
		||||
  * voice (calls and answering machine) (tel@oauth3.org)
 | 
			
		||||
  * lamba-style functions (functions@oauth3.org)
 | 
			
		||||
  * large file access (files@daplie.com)
 | 
			
		||||
  * database access (data@daplie.com)
 | 
			
		||||
  * scheduling (for background tasks, alerts, alarms, calendars, reminders, etc) (events@daplie.com)
 | 
			
		||||
  * payments (credit card) (payments@daplie.com)
 | 
			
		||||
  * email (email@daplie.com)
 | 
			
		||||
  * SMS (texting) (tel@daplie.com)
 | 
			
		||||
  * voice (calls and answering machine) (tel@daplie.com)
 | 
			
		||||
  * lamba-style functions (functions@daplie.com)
 | 
			
		||||
* Per-app, per-site, and per-user configurations
 | 
			
		||||
* Multi-Tentated Application Management
 | 
			
		||||
* Built-in OAuth2 & OAuth3 support
 | 
			
		||||
@ -54,7 +54,7 @@ Installation
 | 
			
		||||
We're still in a stage where the installation generally requires many manual steps.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
curl https://git.coolaj86.com/coolaj86/walnut.js/raw/v1.2/installer/get.sh | bash
 | 
			
		||||
curl https://git.daplie.com/Daplie/walnut.js/raw/v1.2/installer/get.sh | bash
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
See [INSTALL.md](/INSTALL.md)
 | 
			
		||||
@ -104,6 +104,7 @@ Understanding Walnut
 | 
			
		||||
│   ├── rest
 | 
			
		||||
│   └── services
 | 
			
		||||
└── var
 | 
			
		||||
    ├── <<pkgname>>/config.json
 | 
			
		||||
    └── sites
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -131,7 +132,7 @@ Initialization
 | 
			
		||||
needs to know its primary domain
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
POST https://api.<domain.tld>/api/walnut@oauth3.org/init
 | 
			
		||||
POST https://api.<domain.tld>/api/walnut@daplie.com/init
 | 
			
		||||
 | 
			
		||||
{ "domain": "<domain.tld>" }
 | 
			
		||||
```
 | 
			
		||||
@ -153,18 +154,18 @@ api.<domain.tld>
 | 
			
		||||
assets.<domain.tld>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The domains can be setup through the OAuth3 Desktop App or with `oauth3-tools`
 | 
			
		||||
The domains can be setup through the Daplie Desktop App or with `daplie-tools`
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# set device address and attach primary domain
 | 
			
		||||
oauth3 devices:attach -d foodevice -n example.com -a 127.0.0.1
 | 
			
		||||
daplie devices:attach -d foodevice -n example.com -a 127.0.0.1
 | 
			
		||||
 | 
			
		||||
# attach all other domains with same device/address
 | 
			
		||||
oauth3 devices:attach -d foodevice -n www.example.com
 | 
			
		||||
oauth3 devices:attach -d foodevice -n api.example.com
 | 
			
		||||
oauth3 devices:attach -d foodevice -n assets.example.com
 | 
			
		||||
oauth3 devices:attach -d foodevice -n cloud.example.com
 | 
			
		||||
oauth3 devices:attach -d foodevice -n api.cloud.example.com
 | 
			
		||||
daplie devices:attach -d foodevice -n www.example.com
 | 
			
		||||
daplie devices:attach -d foodevice -n api.example.com
 | 
			
		||||
daplie devices:attach -d foodevice -n assets.example.com
 | 
			
		||||
daplie devices:attach -d foodevice -n cloud.example.com
 | 
			
		||||
daplie devices:attach -d foodevice -n api.cloud.example.com
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Example `/etc/goldilocks/goldilocks.yml`:
 | 
			
		||||
@ -194,7 +195,7 @@ Resetting the Initialization
 | 
			
		||||
Once you run the app the initialization files will appear in these locations
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
/srv/walnut/var/walnut+config@oauth3.org.sqlite3
 | 
			
		||||
/srv/walnut/var/walnut+config@daplie.com.sqlite3
 | 
			
		||||
/srv/walnut/config/<domain.tld>/config.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -279,7 +280,7 @@ The packages:
 | 
			
		||||
The permissions:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
/srv/walnut/packages/
 | 
			
		||||
/srv/walnut/etc/
 | 
			
		||||
└── client-api-grants
 | 
			
		||||
    └── cloud.foobar.me
 | 
			
		||||
          '''
 | 
			
		||||
@ -290,7 +291,7 @@ The permissions:
 | 
			
		||||
```
 | 
			
		||||
/srv/walnut/var/
 | 
			
		||||
└── sites
 | 
			
		||||
    └── example.com
 | 
			
		||||
    └── daplie.me
 | 
			
		||||
          '''
 | 
			
		||||
          seed@example.com      # refers to /srv/walnut/packages/pages/seed@example.com
 | 
			
		||||
          '''
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								dist/var/example.com/config.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								dist/var/example.com/config.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
{ "mailgun.org": {
 | 
			
		||||
    "apiKey": "key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 | 
			
		||||
  , "apiPublicKey": "pubkey-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 | 
			
		||||
  , "auth": {
 | 
			
		||||
      "user": "mailer@example.com"
 | 
			
		||||
    , "pass": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 | 
			
		||||
    , "api_key": "key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 | 
			
		||||
    , "domain": "example.com"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -7,7 +7,7 @@ my_ver=master
 | 
			
		||||
my_tmp=$(mktemp -d)
 | 
			
		||||
 | 
			
		||||
mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name
 | 
			
		||||
git clone https://git.coolaj86.com/coolaj86/walnut.js.git $my_tmp/opt/$my_name/core
 | 
			
		||||
git clone https://git.daplie.com/Daplie/walnut.js.git $my_tmp/opt/$my_name/core
 | 
			
		||||
 | 
			
		||||
echo "Installing to $my_tmp (will be moved after install)"
 | 
			
		||||
pushd $my_tmp/opt/$my_name/core
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
#                             #
 | 
			
		||||
###############################
 | 
			
		||||
 | 
			
		||||
# See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh
 | 
			
		||||
# See https://git.daplie.com/Daplie/daplie-snippets/blob/master/bash/http-get.sh
 | 
			
		||||
 | 
			
		||||
_h_http_get=""
 | 
			
		||||
_h_http_opts=""
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ set -u
 | 
			
		||||
### IMPORTANT ###
 | 
			
		||||
###  VERSION  ###
 | 
			
		||||
my_name=walnut
 | 
			
		||||
my_app_pkg_name=org.oauth3.walnut.web
 | 
			
		||||
my_app_pkg_name=com.daplie.walnut.web
 | 
			
		||||
my_app_ver="v1.2"
 | 
			
		||||
my_azp_oauth3_ver="v1.2"
 | 
			
		||||
# is the old version still needed in launchpad?
 | 
			
		||||
@ -30,7 +30,7 @@ my_app_ver="v1.2"
 | 
			
		||||
my_launchpad_ver="v1.2"
 | 
			
		||||
my_iss_oauth3_rest_ver="v1.2.0"
 | 
			
		||||
my_iss_oauth3_pages_ver="v1.2.1"
 | 
			
		||||
my_www_ppl_ver=v1.0.15
 | 
			
		||||
my_www_daplie_ver=v1.0.15
 | 
			
		||||
export NODE_VERSION="v8.9.0"
 | 
			
		||||
#################
 | 
			
		||||
export NODE_PATH=$my_tmp/opt/$my_name/lib/node_modules
 | 
			
		||||
@ -44,7 +44,7 @@ my_npm="$NPM_CONFIG_PREFIX/bin/npm"
 | 
			
		||||
# TODO un-hardcode core at al
 | 
			
		||||
#my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist
 | 
			
		||||
my_app_dist=$my_tmp/opt/$my_name/core/dist
 | 
			
		||||
installer_base="https://git.coolaj86.com/coolaj86/goldilocks.js/raw/$my_app_ver"
 | 
			
		||||
installer_base="https://git.daplie.com/Daplie/goldilocks.js/raw/$my_app_ver"
 | 
			
		||||
 | 
			
		||||
# Backwards compat
 | 
			
		||||
# some scripts still use the old names
 | 
			
		||||
@ -102,8 +102,8 @@ pushd $my_tmp/opt/$my_name/core
 | 
			
		||||
  $my_npm install
 | 
			
		||||
popd
 | 
			
		||||
 | 
			
		||||
git clone https://git.coolaj86.com/coolaj86/walnut_launchpad.html.git $my_tmp/opt/$my_name/core/lib/walnut@oauth3.org/setup
 | 
			
		||||
pushd $my_tmp/opt/$my_name/core/lib/walnut@oauth3.org/setup
 | 
			
		||||
git clone https://git.daplie.com/Daplie/walnut_launchpad.git $my_tmp/opt/$my_name/core/lib/walnut@daplie.com/setup
 | 
			
		||||
pushd $my_tmp/opt/$my_name/core/lib/walnut@daplie.com/setup
 | 
			
		||||
  git pull
 | 
			
		||||
  git checkout $my_launchpad_ver
 | 
			
		||||
 | 
			
		||||
@ -130,9 +130,9 @@ pushd $my_tmp/opt/$my_name/packages
 | 
			
		||||
    popd
 | 
			
		||||
  popd
 | 
			
		||||
 | 
			
		||||
  git clone https://git.coolaj86.com/coolaj86/walnut_rest_www_oauth3.org.js.git rest/www@oauth3.org
 | 
			
		||||
  pushd rest/www@oauth3.org
 | 
			
		||||
    git checkout $my_www_ppl_ver
 | 
			
		||||
  git clone https://git.daplie.com/Daplie/walnut_rest_www_daplie.com.git rest/www@daplie.com
 | 
			
		||||
  pushd rest/www@daplie.com
 | 
			
		||||
    git checkout $my_www_daplie_ver
 | 
			
		||||
    $my_npm install
 | 
			
		||||
  popd
 | 
			
		||||
popd
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								lib/apis.js
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								lib/apis.js
									
									
									
									
									
								
							@ -124,6 +124,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
			
		||||
      var tok = req.oauth3.token;
 | 
			
		||||
      var accountId = req.params.accountId || '__NO_ID_GIVEN__';
 | 
			
		||||
      var ppid;
 | 
			
		||||
      var iss = tok.iss;
 | 
			
		||||
 | 
			
		||||
      if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
 | 
			
		||||
        return ppid === accountId;
 | 
			
		||||
@ -131,6 +132,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
			
		||||
        ppid = accountId;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Deprecated backwards compat. To be removed.
 | 
			
		||||
      if (tok.axs && tok.axs.filter(function (acc) {
 | 
			
		||||
        return acc.id === accountId || acc.appScopedId === accountId;
 | 
			
		||||
      }).length) {
 | 
			
		||||
@ -145,10 +147,9 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
			
		||||
        return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token"));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return req.oauth3.rescope(ppid).then(function (accountIdx) {
 | 
			
		||||
      return req.oauth3.rescope().then(function (accountIdx) {
 | 
			
		||||
        req.oauth3.accountIdx = accountIdx;
 | 
			
		||||
        req.oauth3.ppid = ppid;
 | 
			
		||||
        req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
 | 
			
		||||
        //console.log('[walnut@daplie.com] accountIdx:', accountIdx);
 | 
			
		||||
        //console.log('[walnut@daplie.com] ppid:', ppid);
 | 
			
		||||
 | 
			
		||||
@ -160,19 +161,31 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function accountRequired(req, res, next) {
 | 
			
		||||
    console.log('[accountRequired] [enter]');
 | 
			
		||||
 | 
			
		||||
    var myIss = req.experienceId;
 | 
			
		||||
    var isPpid;
 | 
			
		||||
 | 
			
		||||
    // if this already has auth, great
 | 
			
		||||
    if (req.oauth3.ppid && req.oauth3.accountIdx) {
 | 
			
		||||
      next();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // being public does not disallow authentication
 | 
			
		||||
    if (req.isPublic && !req.oauth3.encodedToken) {
 | 
			
		||||
      next();
 | 
			
		||||
      return;
 | 
			
		||||
      // except that if it's a ppid, we have to internally exchange it for the real token
 | 
			
		||||
      isPpid = (myIss === req.oauth3.iss && myIss !== req.oauth3.azp);
 | 
			
		||||
      if (!isPpid) {
 | 
			
		||||
        console.log('[accountRequired] has token already');
 | 
			
		||||
        console.log(req.oauth3);
 | 
			
		||||
        console.log('');
 | 
			
		||||
        next();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!req.oauth3.encodedToken) {
 | 
			
		||||
      // being public does not disallow authentication
 | 
			
		||||
      if (req.isPublic) {
 | 
			
		||||
        next();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      rejectableRequest(
 | 
			
		||||
        req
 | 
			
		||||
      , res
 | 
			
		||||
@ -187,6 +200,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
			
		||||
      var tok = req.oauth3.token;
 | 
			
		||||
      var ppid;
 | 
			
		||||
      var err;
 | 
			
		||||
      var iss = tok.iss;
 | 
			
		||||
 | 
			
		||||
      if (tok.sub) {
 | 
			
		||||
        if (tok.sub.split(/,/g).length > 1) {
 | 
			
		||||
@ -195,25 +209,30 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
			
		||||
        }
 | 
			
		||||
        ppid = tok.sub;
 | 
			
		||||
      }
 | 
			
		||||
      else if (tok.axs && tok.axs.length) {
 | 
			
		||||
        if (tok.axs.length > 1) {
 | 
			
		||||
          err = new Error("more than one 'axs' specified in token (also, update to using 'sub' instead)");
 | 
			
		||||
          return PromiseA.reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        ppid = tok.axs[0].appScopedId || tok.axs[0].id;
 | 
			
		||||
      }
 | 
			
		||||
      else if (tok.acx) {
 | 
			
		||||
        ppid = tok.acx.appScopedId || tok.acx.id || tok.acx;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!ppid) {
 | 
			
		||||
        return PromiseA.reject(new Error("could not determine accountId from access token"));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return req.oauth3.rescope(ppid).then(function (accountIdx) {
 | 
			
		||||
      return req.oauth3.rescope().then(function (accountIdx) {
 | 
			
		||||
        console.log('[accountRequired] req.oauth3');
 | 
			
		||||
        console.log(accountIdx);
 | 
			
		||||
 | 
			
		||||
        var sub = accountIdx.split('@')[0];
 | 
			
		||||
        var iss = accountIdx.split('@')[1];
 | 
			
		||||
        var id = sub + '@' + iss;
 | 
			
		||||
 | 
			
		||||
        req.oauth3.profile = {
 | 
			
		||||
          id: id
 | 
			
		||||
        , sub: sub
 | 
			
		||||
        , iss: iss
 | 
			
		||||
        };
 | 
			
		||||
        req.oauth3.id = id;
 | 
			
		||||
        req.oauth3.sub = sub;
 | 
			
		||||
        req.oauth3.iss = iss;
 | 
			
		||||
 | 
			
		||||
        req.oauth3.accountIdx = accountIdx;
 | 
			
		||||
        req.oauth3.ppid = ppid;
 | 
			
		||||
        req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
 | 
			
		||||
 | 
			
		||||
        next();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										242
									
								
								lib/oauth3.js
									
									
									
									
									
								
							
							
						
						
									
										242
									
								
								lib/oauth3.js
									
									
									
									
									
								
							@ -2,15 +2,17 @@
 | 
			
		||||
 | 
			
		||||
var PromiseA = require('bluebird');
 | 
			
		||||
 | 
			
		||||
function generateRescope(req, Models, decoded, fullPpid, ppid) {
 | 
			
		||||
function generateRescope(req, Models, decoded) {
 | 
			
		||||
  var fullPpid = decoded.sub+'@'+decoded.iss;
 | 
			
		||||
  var ppid = decoded.sub;
 | 
			
		||||
  return function (/*sub*/) {
 | 
			
		||||
    // TODO: this function is supposed to convert PPIDs of different parties to some account
 | 
			
		||||
    // ID that allows application to keep track of permisions and what-not.
 | 
			
		||||
    console.log('[rescope] Attempting ', fullPpid);
 | 
			
		||||
    return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) {
 | 
			
		||||
      if (results[0]) {
 | 
			
		||||
        console.log('[rescope] lukcy duck: got it on the 1st try');
 | 
			
		||||
        return PromiseA.resolve(results);
 | 
			
		||||
        console.log('[rescope] lucky duck: got it on the 1st try');
 | 
			
		||||
        return results;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // XXX BUG XXX
 | 
			
		||||
@ -19,71 +21,46 @@ function generateRescope(req, Models, decoded, fullPpid, ppid) {
 | 
			
		||||
    }).then(function (results) {
 | 
			
		||||
      var result = results[0];
 | 
			
		||||
 | 
			
		||||
      if (!result || !result.sub || !decoded.iss) {
 | 
			
		||||
        console.log('[rescope] Not a 2nd party token...');
 | 
			
		||||
        return Models.IssuerOauth3OrgProfiles.get(fullPpid);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return result;
 | 
			
		||||
    }).then(function (result) {
 | 
			
		||||
      var err;
 | 
			
		||||
 | 
			
		||||
      if (!result || !result.sub || !decoded.iss) {
 | 
			
		||||
        // XXX BUG XXX TODO swap this external ppid for an internal (and ask user to link with existing profile)
 | 
			
		||||
        //req.oauth3.accountIdx = fullPpid;
 | 
			
		||||
        throw new Error("internal / external ID swapping not yet implemented. TODO: "
 | 
			
		||||
          + "No profile found with that credential. Would you like to create a new profile or link to an existing profile?");
 | 
			
		||||
        console.log('[DEBUG] decoded:');
 | 
			
		||||
        console.log(decoded);
 | 
			
		||||
        console.log('[DEBUG] decoded.iss:', decoded.iss);
 | 
			
		||||
        console.log('[DEBUG] fullPpid:', fullPpid);
 | 
			
		||||
        console.log('[DEBUG] ppid:', ppid);
 | 
			
		||||
 | 
			
		||||
        err = new Error(
 | 
			
		||||
          "TODO: No profile found with that credential. Would you like to create a new profile or link to an existing profile?"
 | 
			
		||||
        );
 | 
			
		||||
        err.code = "E_NO_PROFILE@oauth3.org"
 | 
			
		||||
        throw err;
 | 
			
		||||
        //return req.oauth3.token.sub + '@' + req.oauth3.token.iss;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // XXX BUG XXX need to pass own url in to use as issuer for own tokens
 | 
			
		||||
      req.oauth3.accountIdx = result.sub + '@' + decoded.iss;
 | 
			
		||||
      req.oauth3.accountIdx = result.sub + '@' + (result.iss || decoded.iss);
 | 
			
		||||
 | 
			
		||||
      console.log('[rescope] result:');
 | 
			
		||||
      console.log(results);
 | 
			
		||||
      console.log(req.oauth3.accountIdx);
 | 
			
		||||
      console.log(result);
 | 
			
		||||
      console.log('[rescope] req.oauth3.accountIdx:', req.oauth3.accountIdx);
 | 
			
		||||
 | 
			
		||||
      return PromiseA.resolve(req.oauth3.accountIdx);
 | 
			
		||||
      return req.oauth3.accountIdx;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function extractAccessToken(req) {
 | 
			
		||||
  var token = null;
 | 
			
		||||
  var parts;
 | 
			
		||||
  var scheme;
 | 
			
		||||
  var credentials;
 | 
			
		||||
 | 
			
		||||
  if (req.headers && req.headers.authorization) {
 | 
			
		||||
    // Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
 | 
			
		||||
    parts = req.headers.authorization.split(' ');
 | 
			
		||||
 | 
			
		||||
    if (parts.length !== 2) {
 | 
			
		||||
      return PromiseA.reject(new Error("malformed Authorization header"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    scheme = parts[0];
 | 
			
		||||
    credentials = parts[1];
 | 
			
		||||
 | 
			
		||||
    if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
 | 
			
		||||
      token = credentials;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (req.body && req.body.access_token) {
 | 
			
		||||
    if (token) { PromiseA.reject(new Error("token exists in header and body")); }
 | 
			
		||||
    token = req.body.access_token;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO disallow query with req.method === 'GET'
 | 
			
		||||
  // NOTE: the case of DDNS on routers requires a GET and access_token
 | 
			
		||||
  // (cookies should be used for protected static assets)
 | 
			
		||||
  if (req.query && req.query.access_token) {
 | 
			
		||||
    if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
 | 
			
		||||
    token = req.query.access_token;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
  err = new Error(challenge());
 | 
			
		||||
  err.code = 'E_BEARER_REALM';
 | 
			
		||||
 | 
			
		||||
  if (!token) { return PromiseA.reject(err); }
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  return PromiseA.resolve(token);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function verifyToken(token) {
 | 
			
		||||
function verifyToken(token, opts) {
 | 
			
		||||
  opts = opts || { audiences: [], complete: false };
 | 
			
		||||
  var jwt = require('jsonwebtoken');
 | 
			
		||||
  var decoded;
 | 
			
		||||
 | 
			
		||||
@ -98,6 +75,7 @@ function verifyToken(token) {
 | 
			
		||||
  try {
 | 
			
		||||
    decoded = jwt.decode(token, {complete: true});
 | 
			
		||||
  } catch (e) {}
 | 
			
		||||
 | 
			
		||||
  if (!decoded) {
 | 
			
		||||
    return PromiseA.reject({
 | 
			
		||||
      message: 'provided token not a JSON Web Token'
 | 
			
		||||
@ -130,6 +108,27 @@ function verifyToken(token) {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var audMatch = decoded.payload.aud && ('*' === decoded.payload.aud || opts.audiences.some(function (aud) { return -1 !== decoded.payload.aud.split(',').indexOf(aud); }));
 | 
			
		||||
  var azpMatch = decoded.payload.azp && ('*' === decoded.payload.azp || opts.audiences.some(function (aud) { return -1 !== decoded.payload.azp.split(',').indexOf(aud); }));
 | 
			
		||||
 | 
			
		||||
  if (!audMatch) {
 | 
			
		||||
    console.log("[verifyToken] 'aud' '" + decoded.payload.aud + "' does not match '" + opts.audiences.join(',') + "'");
 | 
			
		||||
  }
 | 
			
		||||
  // TODO needs an option to verify that the sender of the token was, in fact, the azp (i.e. the Origin and/or Referer Headers)
 | 
			
		||||
  if (!azpMatch) {
 | 
			
		||||
    console.log("[verifyToken] 'azp' '" + decoded.payload.azp + "' does not match '" + opts.audiences.join(',') + "'");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!audMatch && !azpMatch) {
 | 
			
		||||
    err = new Error(
 | 
			
		||||
      "Application '" + req.experienceId + "' refused token because '" + decoded.payload.aud + "' is not an accepted audience (aud)"
 | 
			
		||||
    + " and '" + decoded.payload.azp + "' is not an authorized party (azp)"
 | 
			
		||||
    );
 | 
			
		||||
    err.code = 'E_TOKEN_AUD';
 | 
			
		||||
    err.url = 'https://oauth3.org/docs/errors#E_TOKEN_AUD'
 | 
			
		||||
    return PromiseA.reject(err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var OAUTH3 = require('oauth3.js');
 | 
			
		||||
  OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js');
 | 
			
		||||
  return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
 | 
			
		||||
@ -188,16 +187,27 @@ function verifyToken(token) {
 | 
			
		||||
    if (res.data.error) {
 | 
			
		||||
      return PromiseA.reject(res.data.error);
 | 
			
		||||
    }
 | 
			
		||||
    var opts = {};
 | 
			
		||||
    var opts2 = {};
 | 
			
		||||
    if (Array.isArray(res.data.alg)) {
 | 
			
		||||
      opts.algorithms = res.data.alg;
 | 
			
		||||
      opts2.algorithms = res.data.alg;
 | 
			
		||||
    } else if (typeof res.data.alg === 'string') {
 | 
			
		||||
      opts.algorithms = [res.data.alg];
 | 
			
		||||
      opts2.algorithms = [res.data.alg];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      return jwt.verify(token, require('jwk-to-pem')(res.data), opts);
 | 
			
		||||
      if (opts.complete) {
 | 
			
		||||
        opts2.complete = true;
 | 
			
		||||
      }
 | 
			
		||||
      return jwt.verify(token, require('jwk-to-pem')(res.data), opts2);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if ('TokenExpiredError' === err.name) {
 | 
			
		||||
        return PromiseA.reject({
 | 
			
		||||
          message: 'TokenExpiredError: jwt expired'
 | 
			
		||||
        , code: 'E_TOKEN_EXPIRED'
 | 
			
		||||
        , url: 'https://oauth3.org/docs/errors#E_TOKEN_EXPIRED'
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return PromiseA.reject({
 | 
			
		||||
        message: 'token verification failed'
 | 
			
		||||
      , code: 'E_INVALID_TOKEN'
 | 
			
		||||
@ -217,32 +227,47 @@ function deepFreeze(obj) {
 | 
			
		||||
  Object.freeze(obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cookieOauth3(Models, req, res, next) {
 | 
			
		||||
  req.oauth3 = {};
 | 
			
		||||
function fiddleOauth3(Models, req) {
 | 
			
		||||
  var token = req.oauth3.encodedToken;
 | 
			
		||||
 | 
			
		||||
  var token = req.cookies.jwt;
 | 
			
		||||
 | 
			
		||||
  req.oauth3.encodedToken = token;
 | 
			
		||||
  req.oauth3.verifyAsync = function (jwt) {
 | 
			
		||||
    return verifyToken(jwt || token);
 | 
			
		||||
  req.oauth3.verifyAsync = function (jwt, opts) {
 | 
			
		||||
    return verifyToken(jwt || token, opts || { audiences: [ req.experienceId ] });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return verifyToken(token).then(function  (decoded) {
 | 
			
		||||
  if (!token) {
 | 
			
		||||
    return PromiseA.resolve(null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return verifyToken(token, { complete: false, audiences: [ req.experienceId ] }).then(function  (decoded) {
 | 
			
		||||
    var err;
 | 
			
		||||
    req.oauth3.token = decoded;
 | 
			
		||||
 | 
			
		||||
    if (!decoded) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
 | 
			
		||||
    req.oauth3.ppid = ppid;
 | 
			
		||||
    req.oauth3.accountIdx = ppid+'@'+decoded.iss;
 | 
			
		||||
    req.oauth3.ppid = decoded.sub;
 | 
			
		||||
 | 
			
		||||
    var hash = require('crypto').createHash('sha256').update(req.oauth3.accountIdx).digest('base64');
 | 
			
		||||
    hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
 | 
			
		||||
    req.oauth3.accountHash = hash;
 | 
			
		||||
    req.oauth3.id = decoded.sub + '@' + decoded.iss;
 | 
			
		||||
    req.oauth3.sub = decoded.sub;
 | 
			
		||||
    req.oauth3.iss = decoded.iss;
 | 
			
		||||
    req.oauth3.azp = decoded.azp;
 | 
			
		||||
    req.oauth3.aud = decoded.aud;
 | 
			
		||||
 | 
			
		||||
    req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
 | 
			
		||||
  }).then(function () {
 | 
			
		||||
    req.oauth3.accountIdx = req.oauth3.id;
 | 
			
		||||
 | 
			
		||||
    req.oauth3.rescope = generateRescope(req, Models, decoded);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cookieOauth3(Models, req, res, next) {
 | 
			
		||||
  req.oauth3 = {};
 | 
			
		||||
 | 
			
		||||
  var cookieName = 'jwt';
 | 
			
		||||
  var token = req.cookies[cookieName];
 | 
			
		||||
 | 
			
		||||
  req.oauth3.encodedToken = token;
 | 
			
		||||
  fiddleOauth3(Models, req).then(function () {
 | 
			
		||||
    deepFreeze(req.oauth3);
 | 
			
		||||
    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
			
		||||
    next();
 | 
			
		||||
@ -251,6 +276,11 @@ function cookieOauth3(Models, req, res, next) {
 | 
			
		||||
      next();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if ('E_TOKEN_EXPIRED' === err.code) {
 | 
			
		||||
      res.clearCookie(cookieName);
 | 
			
		||||
      next();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    console.error('[walnut] cookie lib/oauth3 error:');
 | 
			
		||||
    console.error(err);
 | 
			
		||||
    res.send(err);
 | 
			
		||||
@ -260,37 +290,49 @@ function cookieOauth3(Models, req, res, next) {
 | 
			
		||||
function attachOauth3(Models, req, res, next) {
 | 
			
		||||
  req.oauth3 = {};
 | 
			
		||||
 | 
			
		||||
  extractAccessToken(req).then(function (token) {
 | 
			
		||||
    req.oauth3.encodedToken = token;
 | 
			
		||||
    req.oauth3.verifyAsync = function (jwt) {
 | 
			
		||||
      return verifyToken(jwt || token);
 | 
			
		||||
    };
 | 
			
		||||
  var token = null;
 | 
			
		||||
  var parts;
 | 
			
		||||
  var scheme;
 | 
			
		||||
  var credentials;
 | 
			
		||||
 | 
			
		||||
    if (!token) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return verifyToken(token);
 | 
			
		||||
  }).then(function  (decoded) {
 | 
			
		||||
    req.oauth3.token = decoded;
 | 
			
		||||
    if (!decoded) {
 | 
			
		||||
      return null;
 | 
			
		||||
  if (req.headers && req.headers.authorization) {
 | 
			
		||||
    // Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
 | 
			
		||||
    parts = req.headers.authorization.split(' ');
 | 
			
		||||
 | 
			
		||||
    if (parts.length !== 2) {
 | 
			
		||||
      return PromiseA.reject(new Error("malformed Authorization header"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
 | 
			
		||||
    var fullPpid = ppid+'@'+decoded.iss;
 | 
			
		||||
    req.oauth3.ppid = ppid;
 | 
			
		||||
    scheme = parts[0];
 | 
			
		||||
    credentials = parts[1];
 | 
			
		||||
 | 
			
		||||
    // TODO we can anonymize the relationship between our user as the other service's user
 | 
			
		||||
    // in our own database by hashing the remote service's ppid and using that as the lookup
 | 
			
		||||
    var hash = require('crypto').createHash('sha256').update(fullPpid).digest('base64');
 | 
			
		||||
    hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
 | 
			
		||||
    req.oauth3.accountHash = hash;
 | 
			
		||||
    if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
 | 
			
		||||
      token = credentials;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
 | 
			
		||||
  if (req.body && req.body.access_token) {
 | 
			
		||||
    if (token) { PromiseA.reject(new Error("token exists in header and body")); }
 | 
			
		||||
    token = req.body.access_token;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    console.log('############### assigned req.oauth3:');
 | 
			
		||||
    console.log(req.oauth3);
 | 
			
		||||
  }).then(function () {
 | 
			
		||||
  // TODO disallow query with req.method === 'GET'
 | 
			
		||||
  // NOTE: the case of DDNS on routers requires a GET and access_token
 | 
			
		||||
  // (cookies should be used for protected static assets)
 | 
			
		||||
  if (req.query && req.query.access_token) {
 | 
			
		||||
    if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
 | 
			
		||||
    token = req.query.access_token;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
  err = new Error(challenge());
 | 
			
		||||
  err.code = 'E_BEARER_REALM';
 | 
			
		||||
 | 
			
		||||
  if (!token) { return PromiseA.reject(err); }
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  req.oauth3.encodedToken = token;
 | 
			
		||||
  fiddleOauth3(Models, req).then(function () {
 | 
			
		||||
    //deepFreeze(req.oauth3);
 | 
			
		||||
    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
			
		||||
    next();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								package.json
									
									
									
									
									
								
							@ -8,7 +8,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "https://git.coolaj86.com/coolaj86/walnut.js.git"
 | 
			
		||||
    "url": "https://github.com/Daplie/walnut.git"
 | 
			
		||||
  },
 | 
			
		||||
  "bin": {
 | 
			
		||||
    "walnut": "./bin/walnut.js"
 | 
			
		||||
@ -33,16 +33,16 @@
 | 
			
		||||
    "private",
 | 
			
		||||
    "public"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com)",
 | 
			
		||||
  "author": "AJ ONeal <aj@daplie.com> (https://daplie.com)",
 | 
			
		||||
  "license": "(MIT or Apache2)",
 | 
			
		||||
  "bugs": {
 | 
			
		||||
    "url": "https://git.coolaj86.com/coolaj86/walnut.js/issues"
 | 
			
		||||
    "url": "https://github.com/Daplie/walnut/issues"
 | 
			
		||||
  },
 | 
			
		||||
  "homepage": "https://git.coolaj86.com/coolaj86/walnut.js",
 | 
			
		||||
  "homepage": "https://github.com/Daplie/walnut",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "bluebird": "3.x",
 | 
			
		||||
    "body-parser": "1.x",
 | 
			
		||||
    "cluster-store": "^2.0.8",
 | 
			
		||||
    "cluster-store": "git+https://git.daplie.com/Daplie/cluster-store.git#v2",
 | 
			
		||||
    "connect": "3.x",
 | 
			
		||||
    "connect-cors": "0.5.x",
 | 
			
		||||
    "connect-recase": "^1.0.2",
 | 
			
		||||
@ -57,7 +57,7 @@
 | 
			
		||||
    "jwk-to-pem": "^1.2.6",
 | 
			
		||||
    "mailchimp-api-v3": "^1.7.0",
 | 
			
		||||
    "mandrill-api": "^1.0.45",
 | 
			
		||||
    "masterquest-sqlite3": "^1.1.1",
 | 
			
		||||
    "masterquest-sqlite3": "git+https://git.daplie.com/node/masterquest-sqlite3.git",
 | 
			
		||||
    "mkdirp": "^0.5.1",
 | 
			
		||||
    "multiparty": "^4.1.3",
 | 
			
		||||
    "nodemailer": "^1.4.0",
 | 
			
		||||
@ -67,14 +67,8 @@
 | 
			
		||||
    "request": "^2.81.0",
 | 
			
		||||
    "scmp": "^2.0.0",
 | 
			
		||||
    "serve-static": "1.x",
 | 
			
		||||
    "sqlite3-cluster": "^2.1.2",
 | 
			
		||||
    "sqlite3-cluster": "git+https://git.daplie.com/coolaj86/sqlite3-cluster.git#v2",
 | 
			
		||||
    "stripe": "^4.22.0",
 | 
			
		||||
    "twilio": "1.x"
 | 
			
		||||
  },
 | 
			
		||||
  "gitDependencies": {
 | 
			
		||||
    "cluster-store": "git+https://git.coolaj86.com/coolaj86/cluster-store.git#v2",
 | 
			
		||||
    "masterquest-sqlite3": "git+https://git.coolaj86.com/coolaj86/masterquest-sqlite3.git",
 | 
			
		||||
    "oauth3.js": "git+https://git.oauth3.org/OAuth3/oauth3.js.git#v1.2",
 | 
			
		||||
    "sqlite3-cluster": "git+https://git.coolaj86.com/coolaj86/sqlite3-cluster.git#v2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user