diff --git a/models.js b/models.js index 4b96fb9..7dc3c54 100644 --- a/models.js +++ b/models.js @@ -4,6 +4,12 @@ var apiname = 'issuer_oauth3_org'; var baseFields = [ 'createdAt', 'updatedAt', 'deletedAt' ]; module.exports = [ + { + tablename: apiname + '_private_keys', + idname: 'id', + unique: ['id'], + indices: baseFields.concat([ 'kty', 'kid' ]), + }, { tablename: apiname + '_jwks', idname: 'id', diff --git a/package.json b/package.json index 306abf5..9329c98 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "url": "git+https://git.daplie.com/Oauth3/issuer_oauth3.org.git" }, "dependencies": { - "bluebird": "^3.5.0" + "bluebird": "^3.5.0", + "elliptic": "^6.4.0", + "jsonwebtoken": "^7.4.1", + "jwk-to-pem": "^1.2.6" } } diff --git a/rest.js b/rest.js index b4c9408..472b623 100644 --- a/rest.js +++ b/rest.js @@ -3,9 +3,14 @@ var PromiseA = require('bluebird'); var crypto = require('crypto'); +function makeB64UrlSafe(b64) { + return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); +} + module.exports.create = function (bigconf, deps, app) { var Jwks = { restful: {} }; var Grants = { restful: {} }; + var Tokens = { restful: {} }; // This tablename is based on the tablename found in the objects in model.js. // Instead of the snake_case the name with be UpperCammelCase, converted by masterquest-sqlite3. @@ -27,11 +32,9 @@ module.exports.create = function (bigconf, deps, app) { // catch the error thrown when a token isn't provided and verifyAsync isn't a function. return req.oauth3.verifyAsync(); }).then(function (token) { - // Now that we've confirmed the token is valid we also need to make sure the authorized party - // is us. - // TODO: For the time being the only verify-able tokens are the ones we issued, but this - // will not always be the case. We will need a better way to verify the authorized party. - if (token.iss !== token.azp || token.iss !== token.aud) { + // Now that we've confirmed the token is valid we also need to make sure the issuer, audience, + // and authorized party are all us, because no other app should be managing user identity. + if (token.iss !== req.experienceId || token.aud !== token.iss || token.azp !== token.iss) { throw new Error("token does not allow access to requested resource"); } @@ -85,7 +88,7 @@ module.exports.create = function (bigconf, deps, app) { // as the replacer argument the keys are always in the order they appeared in the array. var jwkStr = JSON.stringify(jwk, keys); var hash = crypto.createHash('sha256').update(jwkStr).digest('base64'); - return PromiseA.resolve(hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+/g, '')); + return PromiseA.resolve(makeB64UrlSafe(hash)); }; Jwks.restful.get = function (req, res) { @@ -207,6 +210,67 @@ module.exports.create = function (bigconf, deps, app) { app.handlePromise(req, res, promise, '[issuer@oauth3.org] save grants'); }; + Tokens.getPrivKey = function (store, experienceId) { + return store.IssuerOauth3OrgPrivateKeys.get(experienceId).then(function (jwk) { + if (jwk) { + return jwk; + } + + var keyPair = require('elliptic').ec('p256').genKeyPair(); + jwk = { + kty: 'EC', + crv: 'P-256', + alg: 'ES256', + kid: experienceId, + x: makeB64UrlSafe(keyPair.getPublic().getX().toArrayLike(Buffer).toString('base64')), + y: makeB64UrlSafe(keyPair.getPublic().getY().toArrayLike(Buffer).toString('base64')), + d: makeB64UrlSafe(keyPair.getPrivate().toArrayLike(Buffer).toString('base64')), + }; + + return store.IssuerOauth3OrgPrivateKeys.upsert(experienceId, jwk).then(function () { + return jwk; + }); + }); + }; + + Tokens.restful.create = function (req, res) { + var store; + var promise = req.getSiteStore().then(function (_store) { + store = _store; + return store.IssuerOauth3OrgGrants.get(req.params.sub+'/'+req.params.azp); + }).then(function (grant) { + if (!grant) { + throw new Error("'"+req.params.azp+"' not given any grants from '"+req.params.sub+"'"); + } + return Tokens.getPrivKey(store, req.experienceId).then(function (jwk) { + var pem = require('jwk-to-pem')(jwk, { private: true }); + var payload = { + // standard + iss: req.experienceId, // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.1 + aud: req.params.aud, // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.3 + azp: grant.azp, + sub: grant.azpSub, // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.2 + // extended + scp: grant.scope, + }; + var opts = { + algorithm: jwk.alg, + header: { + kid: jwk.kid + } + }; + + return { + access_token: require('jsonwebtoken').sign(payload, pem, Object.assign({expiresIn: '1d'}, opts)), + refresh_token: require('jsonwebtoken').sign(payload, pem, opts), + scope: grant.scope, + }; + }); + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] create tokens'); + }; + 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')); @@ -218,5 +282,8 @@ module.exports.create = function (bigconf, deps, app) { app.get( '/grants/:sub/:azp', Grants.restful.getOne); app.post( '/grants/:sub/:azp', Grants.restful.saveNew); + app.use( '/access_token/:sub', authorizeIssuer); + app.post( '/access_token/:sub/:aud/:azp', Tokens.restful.create); + app.use(detachSiteStore); };