diff --git a/README.md b/README.md index 8e03724..bedd237 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,13 @@ issuer components are these: ``` api: api.:hostname +authorization_dialog #/authorization_dialog +logout #/logout create_jwk: :scheme//:hostname/api/issuer@oauth3.org/jwks/:sub -jwks: :scheme//:hostname/api/issuer@oauth3.org/jwks/:thumbprint.json +jwks: :scheme//:hostname/api/issuer@oauth3.org/jwks/:sub/:kid.json grants: :scheme//:hostname/api/issuer@oauth3.org/grants/:sub/:azp? credential_meta: :scheme//:hostname/api/issuer@oauth3.org/logins/meta/:type/:id credential_otp: :scheme//:hostname/api/issuer@oauth3.org/otp -authorization_decision :scheme//:hostname/api/issuer@oauth3.org/authorization_decision -authorization_dialog :scheme//:hostname/api/issuer@oauth3.org/authorization_dialog -logout :scheme//:hostname/api/issuer@oauth3.org/#/logout ``` No `access_token` endpoint is strictly necessary. Since clients can create and @@ -64,11 +63,7 @@ var sub = sha256.digest('hex'); This way any issuer can transfer ownership of identity to any other issuer and deterministically reproduce the ppid by virtue of the secret identity of the -subject and the public identity of the authorized party and the key is known to -be good if the issuer "iss" can supply the public key that verifies the token, -identified by its thumbprint "kid" (which the issuer knows without revealing -its ppid of the subject and without the authorized party needing to reveal its -ppid of the subject. +subject and the public identity of the authorized party. JWKs ---- @@ -86,9 +81,10 @@ signature verification. [JWK](https://tools.ietf.org/html/rfc7517#section-4). ### Retrieving a JWK ### - * **URL** `:scheme//:hostname/api/issuer@oauth3.org/jwks/:kid.json` + * **URL** `:scheme//:hostname/api/issuer@oauth3.org/jwks/:sub/:kid.json` * **Method** `GET` * **Url Params** + * `sub`: The [subject](#subject) for the 3rd party needing to verify a token * `kid`: The [JWK thumbprint](https://tools.ietf.org/html/rfc7638) of the key Currently only `EC` and `RSA` key storage is supported. All provided parameters @@ -98,16 +94,13 @@ specified as part of the public key for the `kty` by the GET request. This is to avoid compromising a key if the private portion or any other potentially sensitive fields are given to us. -TODO: we need to somehow associate a key with a particular user without needing -the issuer's subject. Resources providers will not have that subject but will -need to be able to retrieve only public keys that actually belong to the user -that are trying to validate. - Grants ------ Grants represent the list of resources the user has allowed a party to access. We store those permissions on the server so that users will not have to grant -the same privileges multiple times on different machines. +the same privileges multiple times on different machines. We also store the +[subject](#subject) between the user and the `azp` to allow us to only serve +public keys associated with the correct user when retrieving JWKs. ### Saving/Modifying Grants ### * **URL** `:scheme//:hostname/api/issuer@oauth3.org/grants/:sub/:azp` @@ -116,6 +109,7 @@ the same privileges multiple times on different machines. * `sub`: The [subject](#subject) using the issuer hostname as the `azp` * `azp`: The authorized party the grants are for * **Body Params** + * `sub`: The [subject](#subject) using `azp` from the url * `scope`: A comma separated list of the permissions granted ### Retrieving Grants ### @@ -130,10 +124,10 @@ the same privileges multiple times on different machines. * `scope`: A comma separated list of the permissions granted * `updatedAt`: The ms timestamp for the most recent change to the grants -### Retrieving All Grants ### +### Retrieving All Grants For a User ### * **URL** `:scheme//:hostname/api/issuer@oauth3.org/grants/:sub` * **Method** `GET` * **Url Params** * `sub`: The [subject](#subject) using the issuer hostname as the `azp` - * **Response**: An array of objects with the same values as the simple grant + * **Response**: An array of objects with the same values as the single grant get response. diff --git a/models.js b/models.js index 9eba768..4b96fb9 100644 --- a/models.js +++ b/models.js @@ -14,6 +14,6 @@ module.exports = [ tablename: apiname + '_grants', idname: 'id', unique: ['id'], - indices: baseFields.concat([ 'sub', 'azp', 'scope' ]), + indices: baseFields.concat([ 'sub', 'azp', 'azpSub', 'scope' ]), }, ]; diff --git a/rest.js b/rest.js index 17fe3c9..68be741 100644 --- a/rest.js +++ b/rest.js @@ -89,11 +89,28 @@ module.exports.create = function (bigconf, deps, app) { }; Jwks.restful.get = function (req, res) { - var promise = req.Store.find({ kid: req.params.kid }, { limit: 1 }).then(function (results) { + var store; + // 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) { + store = _store; + return store.IssuerOauth3OrgGrants.find({ azpSub: req.params.sub }); + }).then(function (results) { if (!results.length) { - throw new Error('no keys stored with kid "'+req.params.kid+'"'); + throw new Error("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 Error('PPID collision - unable to safely retrieve keys'); + } + + return store.IssuerOauth3OrgJwks.get(results[0].sub+'/'+req.params.kid); + }).then(function (jwk) { + if (!jwk) { + throw new Error("no keys stored with kid '"+req.params.kid+"' for PPID "+req.params.sub); } - var jwk = results[0]; // 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 @@ -132,31 +149,29 @@ module.exports.create = function (bigconf, deps, app) { }; + Grants.trim = function (grant) { + return { + sub: grant.sub, + azp: grant.azp, + // azpSub: grant.azpSub, + scope: grant.scope, + updatedAt: parseInt(grant.updatedAt, 10), + }; + }; + Grants.restful.getOne = function (req, res) { var promise = req.Store.get(req.params.sub+'/'+req.params.azp).then(function (grant) { if (!grant) { throw new Error('no grants found'); } - return { - sub: grant.sub, - azp: grant.azp, - scope: grant.scope, - updatedAt: parseInt(grant.updatedAt, 10), - }; + return Grants.trim(grant); }); app.handlePromise(req, res, promise, "[issuer@oauth3.org] retrieve grants"); }; Grants.restful.getAll = function (req, res) { var promise = req.Store.find({ sub: req.params.sub }).then(function (results) { - return results.map(function (grant) { - return { - sub: grant.sub, - azp: grant.azp, - scope: grant.scope, - updatedAt: parseInt(grant.updatedAt, 10), - }; - }).sort(function (grantA, grantB) { + return results.map(Grants.trim).sort(function (grantA, grantB) { return (grantA.azp < grantB.azp) ? -1 : 1; }); }); @@ -165,15 +180,20 @@ module.exports.create = function (bigconf, deps, app) { }; Grants.restful.saveNew = function (req, res) { var promise = PromiseA.resolve().then(function () { - if (typeof req.body.scope !== 'string') { - throw new Error("malformed request: 'scope' should be a string"); + if (typeof req.body.scope !== 'string' || typeof req.body.sub !== 'string') { + throw new Error("malformed request: 'sub' and 'scope' must be strings"); + } + return req.Store.find({ azpSub: req.body.sub }); + }).then(function (existing) { + if (existing.length) { + throw new Error("PPID collision detected, cannot save authorized party's sub"); } - var scope = req.body.scope.split(/[+ ,]+/g).join(','); var grant = { - sub: req.params.sub, - azp: req.params.azp, - scope: scope, + sub: req.params.sub, + azp: req.params.azp, + azpSub: req.body.sub, + scope: req.body.scope.split(/[+ ,]+/g).join(','), }; return req.Store.upsert(grant.sub+'/'+grant.azp, grant); }).then(function () { @@ -183,9 +203,9 @@ module.exports.create = function (bigconf, deps, app) { app.handlePromise(req, res, promise, '[issuer@oauth3.org] save grants'); }; - app.use( '/jwks', attachSiteStore.bind(null, 'IssuerOauth3OrgJwks')); - app.get( '/jwks/:kid.json', Jwks.restful.get); - app.use( '/jwks/:sub', authorizeIssuer); // Everything but getting keys is only for the issuer + app.get( '/jwks/:sub/:kid.json', Jwks.restful.get); + // Everything but getting keys is only for the issuer + app.use( '/jwks/:sub', authorizeIssuer, attachSiteStore.bind(null, 'IssuerOauth3OrgJwks')); app.post( '/jwks/:sub', Jwks.restful.saveNew); // Everything regarding grants is only for the issuer