implemented creating tokens for other apps
requires user to already have a token from/for the issuer
This commit is contained in:
parent
ef43b56f1f
commit
c03f380e8d
@ -4,6 +4,12 @@ var apiname = 'issuer_oauth3_org';
|
|||||||
var baseFields = [ 'createdAt', 'updatedAt', 'deletedAt' ];
|
var baseFields = [ 'createdAt', 'updatedAt', 'deletedAt' ];
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
{
|
||||||
|
tablename: apiname + '_private_keys',
|
||||||
|
idname: 'id',
|
||||||
|
unique: ['id'],
|
||||||
|
indices: baseFields.concat([ 'kty', 'kid' ]),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
tablename: apiname + '_jwks',
|
tablename: apiname + '_jwks',
|
||||||
idname: 'id',
|
idname: 'id',
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
"url": "git+https://git.daplie.com/Oauth3/issuer_oauth3.org.git"
|
"url": "git+https://git.daplie.com/Oauth3/issuer_oauth3.org.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bluebird": "^3.5.0"
|
"bluebird": "^3.5.0",
|
||||||
|
"elliptic": "^6.4.0",
|
||||||
|
"jsonwebtoken": "^7.4.1",
|
||||||
|
"jwk-to-pem": "^1.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
rest.js
79
rest.js
@ -3,9 +3,14 @@
|
|||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
var crypto = require('crypto');
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
function makeB64UrlSafe(b64) {
|
||||||
|
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.create = function (bigconf, deps, app) {
|
module.exports.create = function (bigconf, deps, app) {
|
||||||
var Jwks = { restful: {} };
|
var Jwks = { restful: {} };
|
||||||
var Grants = { restful: {} };
|
var Grants = { restful: {} };
|
||||||
|
var Tokens = { restful: {} };
|
||||||
|
|
||||||
// This tablename is based on the tablename found in the objects in model.js.
|
// 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.
|
// 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.
|
// catch the error thrown when a token isn't provided and verifyAsync isn't a function.
|
||||||
return req.oauth3.verifyAsync();
|
return req.oauth3.verifyAsync();
|
||||||
}).then(function (token) {
|
}).then(function (token) {
|
||||||
// Now that we've confirmed the token is valid we also need to make sure the authorized party
|
// Now that we've confirmed the token is valid we also need to make sure the issuer, audience,
|
||||||
// is us.
|
// and authorized party are all us, because no other app should be managing user identity.
|
||||||
// TODO: For the time being the only verify-able tokens are the ones we issued, but this
|
if (token.iss !== req.experienceId || token.aud !== token.iss || token.azp !== token.iss) {
|
||||||
// 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) {
|
|
||||||
throw new Error("token does not allow access to requested resource");
|
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.
|
// as the replacer argument the keys are always in the order they appeared in the array.
|
||||||
var jwkStr = JSON.stringify(jwk, keys);
|
var jwkStr = JSON.stringify(jwk, keys);
|
||||||
var hash = crypto.createHash('sha256').update(jwkStr).digest('base64');
|
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) {
|
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');
|
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);
|
app.get( '/jwks/:sub/:kid.json', Jwks.restful.get);
|
||||||
// Everything but getting keys is only for the issuer
|
// Everything but getting keys is only for the issuer
|
||||||
app.use( '/jwks/:sub', authorizeIssuer, attachSiteStore.bind(null, 'IssuerOauth3OrgJwks'));
|
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.get( '/grants/:sub/:azp', Grants.restful.getOne);
|
||||||
app.post( '/grants/:sub/:azp', Grants.restful.saveNew);
|
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);
|
app.use(detachSiteStore);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user