Merge branch 'v1.2' of ssh://git.oauth3.org/OAuth3/issuer.rest.walnut.js into v1.2

This commit is contained in:
AJ ONeal 2017-12-02 07:27:12 +00:00
commit 3fedb3d8ad
7 changed files with 134 additions and 53 deletions

9
CHANGELOG Normal file
View File

@ -0,0 +1,9 @@
v1.2.1 - Allow PPID reuse
* Now allows a single user to use the same PPID for multiple sites
v1.2.0 - Issuer Pub/Priv Key and Grant APIs for OAuth3
* Resource Owner Password token exchange
* Implicit Grant token exchange
* Public / Private Keypair generation
* Public key (remember device) syncing
* BUG: ppid rescoping is not handled correctly for 3rd parties

41
LICENSE Normal file
View File

@ -0,0 +1,41 @@
Copyright 2017 Daplie, Inc
This is open source software; you can redistribute it and/or modify it under the
terms of either:
a) the "MIT License"
b) the "Apache-2.0 License"
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,6 +1,12 @@
issuer@oauth3.org (js)
======================
| [oauth3.js](https://git.oauth3.org/OAuth3/oauth3.js)
| [issuer.html](https://git.oauth3.org/OAuth3/issuer.html)
| *issuer.rest.walnut.js*
| [issuer.srv](https://git.oauth3.org/OAuth3/issuer.srv)
| Sponsored by [Daplie](https://daplie.com)
Implementation of server-side RESTful OAuth3 issuer APIs.
These are the OAuth3 APIs that allow for creation and retrieval of public keys

View File

@ -40,5 +40,28 @@ function checkIssuerToken(req, expectedSub) {
});
}
async function getPrimarySub(grantStore, secondary) {
var results = await Promise.all([
grantStore.find({ sub: secondary }),
grantStore.find({ azpSub: secondary }),
]);
// Extract only the sub field from each row and reduce the duplicates so that each value
// can only appear in the final list once.
var subList = [].concat.apply([], results).map(grant => grant.sub);
subList = subList.filter((sub, ind, list) => list.indexOf(sub) === ind);
if (subList.length > 1) {
// This should not ever happen since there is a check for PPID collisions when saving
// grants, but it's probably better to have this check anyway just incase something
// happens that isn't currently accounted for.
console.error('acounts ' + JSON.stringify(subList) + ' are all associated with "'+secondary+'"');
throw new Error('PPID collision');
}
return subList[0];
}
module.exports.checkIssuerToken = checkIssuerToken;
module.exports.getPrimarySub = getPrimarySub;
module.exports.makeB64UrlSafe = makeB64UrlSafe;

View File

@ -56,14 +56,16 @@ function create(app) {
throw new OpErr("malformed request: 'sub' and 'scope' must be strings");
}
return req.Store.find({ azpSub: req.body.sub });
}).then(function (existing) {
if (existing.length) {
if (existing.length > 1) {
throw new OpErr("pre-existing PPID collision detected");
} else if (existing[0].sub !== req.params.sub || existing[0].azp !== req.params.azp) {
throw new OpErr("PPID collision detected, cannot save authorized party's sub");
return require('./common').getPrimarySub(req.Store, req.body.sub).catch(function (err) {
if (/collision/.test(err.message)) {
err.message = 'pre-existing PPID collision detected';
}
throw err;
});
}).then(function (primSub) {
if (primSub && primSub !== req.params.sub) {
console.log('account "'+req.params.sub+'" cannot use PPID "'+req.body.sub+'" already used by "'+primSub+'"');
throw new OpErr("PPID collision detected, cannot save authorized party's sub");
}
var grant = {

88
jwks.js
View File

@ -33,58 +33,58 @@ function thumbprint(jwk) {
return PromiseA.resolve(makeB64UrlSafe(hash));
}
function sanitizeJwk(jwk) {
// We need to sanitize the key to make sure we don't deliver any private keys fields if
// we were given a key we could use to sign tokens on behalf of the user. We also don't
// want to deliver the sub or any other PPIDs.
var whitelist = ['kty', 'alg', 'kid', 'use'];
if (jwk.kty === 'EC') {
whitelist = whitelist.concat(['crv', 'x', 'y']);
} else if (jwk.kty === 'RSA') {
whitelist = whitelist.concat(['e', 'n']);
}
var result = {};
whitelist.forEach(function (key) {
result[key] = jwk[key];
});
return result;
}
function create(app) {
var restful = {};
restful.get = function (req, res) {
// The sub in params is the 3rd party PPID, but the keys are stored by the issuer PPID, so
// we need to look up the issuer PPID using the 3rd party PPID.
var promise = req.getSiteStore().then(function (store) {
if (req.params.kid === req.experienceId) {
return store.IssuerOauth3OrgPrivateKeys.get(req.experienceId);
async function getRawKey(req) {
var store = await req.getSiteStore();
if (req.params.kid === req.experienceId) {
return store.IssuerOauth3OrgPrivateKeys.get(req.experienceId);
}
// The keys are stored by the issuer PPID, but the sub we have might be a different PPID
// for a 3rd party.
var issuerSub;
try {
issuerSub = await require('./common').getPrimarySub(store.IssuerOauth3OrgGrants, req.params.sub);
} catch (err) {
if (/collision/.test(err.message)) {
err.message = 'PPID collision - unable to safely retrieve keys';
}
throw err;
}
// First we check to see if the key is being requested by the `sub` that we as the issuer use
// to identify the user, and if not then we need to look up the specified `sub` to see if
// we can determine which (if any) account it's associated with.
return store.IssuerOauth3OrgJwks.get(req.params.sub+'/'+req.params.kid).then(function (jwk) {
if (jwk) {
return jwk;
}
if (!issuerSub) {
throw new OpErr("unknown PPID '" + req.params.sub + "'");
}
return store.IssuerOauth3OrgJwks.get(issuerSub + '/' + req.params.kid);
}
return store.IssuerOauth3OrgGrants.find({ azpSub: req.params.sub }).then(function (results) {
if (!results.length) {
throw new OpErr("unknown PPID '"+req.params.sub+"'");
}
if (results.length > 1) {
// This should not ever happen since there is a check for PPID collisions when saving
// grants, but it's probably better to have this check anyway just incase something
// happens that isn't currently accounted for.
throw new OpErr('PPID collision - unable to safely retrieve keys');
}
return store.IssuerOauth3OrgJwks.get(results[0].sub+'/'+req.params.kid);
});
});
}).then(function (jwk) {
restful.get = function (req, res) {
var promise = PromiseA.resolve(getRawKey(req)).then(function (jwk) {
if (!jwk) {
throw new OpErr("no keys stored with kid '"+req.params.kid+"' for PPID "+req.params.sub);
}
// We need to sanitize the key to make sure we don't deliver any private keys fields if
// we were given a key we could use to sign tokens on behalf of the user. We also don't
// want to deliver the sub or any other PPIDs.
var whitelist = [ 'kty', 'alg', 'kid', 'use' ];
if (jwk.kty === 'EC') {
whitelist = whitelist.concat([ 'crv', 'x', 'y' ]);
} else if (jwk.kty === 'RSA') {
whitelist = whitelist.concat([ 'e', 'n' ]);
}
var result = {};
whitelist.forEach(function (key) {
result[key] = jwk[key];
});
return result;
return sanitizeJwk(jwk);
});
app.handlePromise(req, res, promise, "[issuer@oauth3.org] retrieve JWK");

View File

@ -1,6 +1,6 @@
{
"name": "issuer_oauth3.org",
"version": "1.0.0-placeholder",
"version": "1.2.1",
"description": "Implementation of server-side RESTful OAuth3 issuer APIs",
"main": "rest.js",
"scripts": {
@ -8,7 +8,7 @@
},
"repository": {
"type": "git",
"url": "git+https://git.daplie.com/Oauth3/issuer_oauth3.org.git"
"url": "git+https://git.oauth3.org/OAuth3/issuer.rest.walnut.js.git"
},
"dependencies": {
"bluebird": "^3.5.0",