From 13f92ec6b3409439650d59e8c1925263c116adf5 Mon Sep 17 00:00:00 2001 From: anjuthomas Date: Thu, 17 May 2018 18:41:16 -0700 Subject: [PATCH 1/6] ServiceContext files --- src/.DS_Store | Bin 0 -> 6148 bytes src/serviceContext.js | 177 +++++++++++++++++++ test/.DS_Store | Bin 0 -> 6148 bytes test/serviceContext-test.js | 329 ++++++++++++++++++++++++++++++++++++ 4 files changed, 506 insertions(+) create mode 100644 src/.DS_Store create mode 100644 src/serviceContext.js create mode 100644 test/.DS_Store create mode 100644 test/serviceContext-test.js diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..8fcb782953b502f146f1eff733c202cd8b0fa0c4 GIT binary patch literal 6148 zcmeHKu};H441IS{OhTk!V4T+4$`8Y#)dyATcqZXUTqd zj$bZ$LmUIJ`Sa~LFat29JL1j5*!;Tt$PQY@h;-iL9&6m;5*;2#)!!$adyFkvPk0&d zcg($Fhci52d$sA-t4+6JwLj_w{d6D3l}rjq0VyB_q<|Foy#n5QY15-bMJXT!q`;>F z{(UHP$J($@j86wci~z(5(_vi4EI}-uAl8O`A~Q5gDlw_nMhr_j^QG0*hJ9kvVR16= z(@wTFp;(;Ge2H>cO;nTuQeddSVQy#M|CjV1=Kmo{J1HOq{*?kYSzIsXe5KY~M=$5S vw$N|rAI91!XNXozj8@Ezx8jp8b;Z}bUK{p_L1#YbMEwZ3E;1?b7Ye)s`L-RR literal 0 HcmV?d00001 diff --git a/src/serviceContext.js b/src/serviceContext.js new file mode 100644 index 0000000..d57c458 --- /dev/null +++ b/src/serviceContext.js @@ -0,0 +1,177 @@ +const State = require('./state.js').State; +const crypto = require('crypto'); +const KeyJar = require('../nodeOIDCMsg/src/oicMsg/keystore/KeyJar').KeyJar; +const assert = require('chai').assert; + +/** + * @fileoverview This class keeps information that a client needs to be + * able to talk to a server. Some of this information comes from + * configuration and some from dynamic provider info discovery or client + * registration. But information is also picked up during the + * conversation with a server. + */ + +/** + * serviceContext + * @class + * @constructor + */ +class ServiceContext { + /** + * This class keeps information that a client needs to be able to talk + * to a server. Some of this information comes from configuration and some + * from dynamic provider info discovery or client registration. + * But information is also picked up during the conversation with a server. + * @param {KeyJar} keyjar OIDCMsg KeyJar instance that contains the RP signing and encyrpting keys + * @param {Object} config Client configuration + * @param {Object} params Other attributes that might be needed + */ + constructor(keyjar, config, params) { + this.clientSecret = [this.getClientSecret, this.setClientSecret]; + keyjar = keyjar || null; + config = config || null; + this.keyjar = keyjar || new KeyJar(); + this.providerInfo = {}; + this.registrationResponse = {}; + this.kid = {'sig': {}, 'enc': {}}; + + this.config = config || {}; + + this.baseUrl = ''; + this.requestDir = ''; + this.allow = {}; + this.behavior = {}; + this.clientPreferences = {}; + this.cId = ''; + this.cSecret = ''; + this.issuer = ''; + + let serviceContext = + ['client_id', 'issuer', 'client_secret', 'base_url', 'requests_dir']; + let defaultVal = ''; + + if (params) { + for (var i = 0; i < Object.keys(params).length; i++) { + let key = Object.keys(params)[i]; + let val = params[key]; + this[key] = val; + } + } + + for (let i = 0; i < serviceContext.length; i++) { + let attr = serviceContext[i]; + if (attr === 'client_id') { + this.client_id = this.config[attr] || defaultVal; + } else if (attr === 'issuer') { + this.issuer = this.config[attr] || defaultVal; + } else if (attr === 'client_secret') { + this.client_secret = this.config[attr] || defaultVal; + this.setClientSecret(this.client_secret); + } else if (attr === 'base_url') { + this.base_url = this.config[attr] || defaultVal; + } else if (attr === 'requests_dir') { + this.request_dir = this.config[attr] || defaultVal; + } + }; + + let providerInfo = + ['allow', 'client_preferences', 'behaviour', 'provider_info']; + defaultVal = {}; + for (let i = 0; i < providerInfo.length; i++) { + let attr = providerInfo[i]; + if (attr === 'allow') { + this.allow = this.config[attr] || defaultVal; + } else if (attr === 'client_preferences') { + this.client_prefs = this.config[attr] || defaultVal; + } else if (attr === 'behaviour') { + this.behavior = this.config[attr] || defaultVal; + } else if (attr === 'provider_info') { + this.provider_info = this.config[attr] || defaultVal; + } + }; + + try { + this.redirectUris = this.config['redirect_uris']; + } catch (err) { + this.redirectUris = [null]; + } + + try { + this.callback = this.config['callback']; + } catch (err) { + this.callback = {} + } + + if (config && Object.keys(config).indexOf('keydefs') !== -1) { + this.keyjar = this.buildKeyJar(config['keydefs'], this.keyjar)[1]; + } + + return this; + } + + getClientSecret() { + return this.client_secret; + } + + setClientSecret(val) { + if (!val) { + this.client_secret; + } else { + this.client_secret = val; + // client uses it for signing + // Server might also use it for signing which means the + // client uses it for verifying server signatures + if (this.keyjar == null) { + this.keyjar = new KeyJar(); + } + this.keyjar.addSymmetric('', val.toString()); + } + } + + /** + * Need to generate a redirect_uri path that is unique for a OP/RP combo + This is to counter the mix-up attack. + * @param {string} path Leading path + * @return A list of one unique URL + */ + generateRequestUris(path) { + let m = crypto.createHmac('sha256', ''); + try { + m.update(this.providerInfo['issuer']); + } catch (error) { + m.update(this.issuer); + } + m.update(this.baseUrl); + if (!path.startsWith('/')) { + return [this.baseUrl + '/' + path + '/' + m.digest('hex')]; + } else { + return [this.baseUrl + path + '/' + m.digest('hex')]; + } + } + + /** + * A 1<->1 map is maintained between a URL pointing to a file and + * the name of the file in the file system. + * + * As an example if the base_url is 'https://example.com' and a jwks_uri + * is 'https://example.com/jwks_uri.json' then the filename of the + * corresponding file on the local filesystem would be 'jwks_uri'. + * Relative to the directory from which the RP instance is run. + * + * @param {*} webName + */ + filenameFromWebName(webName) { + if (webName.startsWith(this.baseUrl) == false) { + console.log('ValueError'); + } + let name = webName.substring(this.baseUrl.length, webName.length); + if (name.startsWith('/')) { + return name.substring(1, name.length); + } else { + let splitName = name.split('/'); + return splitName[splitName.length - 1]; + } + } +} + +module.exports.ServiceContext = ServiceContext; \ No newline at end of file diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c039af10f0b52e000b34b02107636e0cdcf55fde GIT binary patch literal 6148 zcmeHKu};HK3_L@JN-Uiau9lm2NuEFSCH>+pj4PQGkOERb3P=Gd@S6hOduh|7L`5ke z1*E{I0{(p{bjPM*pBSGGh8O{e6Q;wsj#+|OJV9(K_KD2UEUCn#S{*Sg>CBf}*Hr8i zlMaiMd7pZ+)d|JobmmKx!t8|4hqiiy#Rx$#zf@};i$n%A3(ePYm=4?0ml01E* Aq5uE@ literal 0 HcmV?d00001 diff --git a/test/serviceContext-test.js b/test/serviceContext-test.js new file mode 100644 index 0000000..96b0ce3 --- /dev/null +++ b/test/serviceContext-test.js @@ -0,0 +1,329 @@ +const assert = require('chai').assert; +const ServiceContext = require('../src/serviceContext.js').ServiceContext; +const urlParse = require('url-parse'); + +const ATTRMAP = { + 'userinfo': { + 'sign': 'userinfo_signed_response_alg', + 'alg': 'userinfo_encrypted_response_alg', + 'enc': 'userinfo_encrypted_response_enc' + }, + 'id_token': { + 'sign': 'id_token_signed_response_alg', + 'alg': 'id_token_encrypted_response_alg', + 'enc': 'id_token_encrypted_response_enc' + }, + 'request': { + 'sign': 'request_object_signing_alg', + 'alg': 'request_object_encryption_alg', + 'enc': 'request_object_encryption_enc' + } +}; + +const DEFAULT_SIGN_ALG = { + 'userinfo': 'RS256', + 'request': 'RS384', + 'id_token': 'ES384', +}; + +/** + * Reformat the crypto algorithm information gathered from a + * client registration response into something more palatable. + * + * @param {string} typ: 'id_token', 'userinfo' or 'request_object' + */ +function signEncAlgs(serviceContext, typ) { + let resp = {}; + for (let i = 0; i < Object.keys(ATTRMAP[typ]).length; i++) { + let key = Object.keys(ATTRMAP[typ])[i]; + let val = ATTRMAP[typ][key]; + if (serviceContext.registrationResponse && serviceContext.registrationResponse[val]){ + resp[key] = serviceContext.registrationResponse[val]; + }else if (key === 'sign') { + try { + resp[key] = DEFAULT_SIGN_ALG[typ]; + } catch (err) { + return; + } + } + } + return resp; +} + +/** + * Verifies that the algorithm to be used are supported by the other side. + * This will look at provider information either statically configured or + * obtained through dynamic provider info discovery. + * + * @param {string} alg The algorithm specification + * @param {string} usage In which context the 'alg' will be used. + * The following contexts are supported: + * - userinfo + * - id_token + * - request_object + * - token_endpoint_auth + * @param {string} typ Type of alg + * - signing_alg + * - encryption_alg + * - encryption_enc + */ +function verifyAlgSupport(serviceContext, alg, usage, typ) { + let supported = serviceContext.providerInfo[usage + '_' + typ + '_values_supported']; + if (supported.indexOf(alg) !== -1) { + return true; + } else { + return false; + } +} + +describe('', function() { + let config = { + 'client_id': 'client_id', + 'issuer': 'issuer', + 'client_secret': 'client_secret', + 'base_url': 'https://example.com', + 'requests_dir': 'requests' + }; + + let ci = new ServiceContext(null, config); + + it('create serviceContext instance', function() { + assert.isNotNull(ci); + }); + + ci.registrationResponse = { + 'application_type': 'web', + 'redirect_uris': [ + 'https://client.example.org/callback', + 'https://client.example.org/callback2' + ], + 'token_endpoint_auth_method': 'client_secret_basic', + 'jwks_uri': 'https://client.example.org/my_public_keys.jwks', + 'userinfo_encrypted_response_alg': 'RSA1_5', + 'userinfo_encrypted_response_enc': 'A128CBC-HS256', + }; + + let res = signEncAlgs(ci, 'userinfo'); + it('registration userInfo signEncAlgs', function() { + assert.deepEqual( + res, {'sign': 'RS256', 'alg': 'RSA1_5', 'enc': 'A128CBC-HS256'}); + }); + + ci.registrationResponse = { + 'application_type': 'web', + 'redirect_uris': [ + 'https://client.example.org/callback', + 'https://client.example.org/callback2' + ], + 'token_endpoint_auth_method': 'client_secret_basic', + 'jwks_uri': 'https://client.example.org/my_public_keys.jwks', + 'userinfo_encrypted_response_alg': 'RSA1_5', + 'userinfo_encrypted_response_enc': 'A128CBC-HS256', + 'request_object_signing_alg': 'RS384' + }; + + res = signEncAlgs(ci, 'userinfo'); + it('registration request object signEncAlgs typ userinfo', function() { + assert.deepEqual( + res, {'sign': 'RS256', 'alg': 'RSA1_5', 'enc': 'A128CBC-HS256'}); + }); + + let res2 = signEncAlgs(ci, 'request'); + it('registration request object signEncAlgs typ request', function() { + assert.deepEqual(res2, {'sign': 'RS384'}); + }); + + ci.registrationResponse = { + 'application_type': 'web', + 'redirect_uris': [ + 'https://client.example.org/callback', + 'https://client.example.org/callback2' + ], + 'token_endpoint_auth_method': 'client_secret_basic', + 'jwks_uri': 'https://client.example.org/my_public_keys.jwks', + 'userinfo_encrypted_response_alg': 'RSA1_5', + 'userinfo_encrypted_response_enc': 'A128CBC-HS256', + 'request_object_signing_alg': 'RS384', + 'id_token_encrypted_response_alg': 'ECDH-ES', + 'id_token_encrypted_response_enc': 'A128GCM', + 'id_token_signed_response_alg': 'ES384', + }; + + let res3 = signEncAlgs(ci, 'userinfo'); + it('registration request object signEncAlgs typ userinfo', function() { + assert.deepEqual( + res3, {'sign': 'RS256', 'alg': 'RSA1_5', 'enc': 'A128CBC-HS256'}); + }); + + let res4 = signEncAlgs(ci, 'request'); + it('registration request object signEncAlgs typ request', function() { + assert.deepEqual(res4, {'sign': 'RS384'}); + }); + + let res5 = signEncAlgs(ci, 'id_token'); + it('registration request object signEncAlgs typ id_token', function() { + assert.deepEqual( + res5, {'sign': 'ES384', 'alg': 'ECDH-ES', 'enc': 'A128GCM'}); + }); + + ci.providerInfo = { + 'version': '3.0', + 'issuer': 'https://server.example.com', + 'authorization_endpoint': 'https://server.example.com/connect/authorize', + 'token_endpoint': 'https://server.example.com/connect/token', + 'token_endpoint_auth_methods_supported': + ['client_secret_basic', 'private_key_jwt'], + 'token_endpoint_auth_signing_alg_values_supported': ['RS256', 'ES256'], + 'userinfo_endpoint': 'https://server.example.com/connect/userinfo', + 'check_session_iframe': 'https://server.example.com/connect/check_session', + 'end_session_endpoint': 'https://server.example.com/connect/end_session', + 'jwks_uri': 'https://server.example.com/jwks.json', + 'registration_endpoint': 'https://server.example.com/connect/register', + 'scopes_supported': + ['openid', 'profile', 'email', 'address', 'phone', 'offline_access'], + 'response_types_supported': + ['code', 'code id_token', 'id_token', 'token id_token'], + 'acr_values_supported': + ['urn:mace:incommon:iap:silver', 'urn:mace:incommon:iap:bronze'], + 'subject_types_supported': ['public', 'pairwise'], + 'userinfo_signing_alg_values_supported': ['RS256', 'ES256', 'HS256'], + 'userinfo_encryption_alg_values_supported': ['RSA1_5', 'A128KW'], + 'userinfo_encryption_enc_values_supported': ['A128CBC+HS256', 'A128GCM'], + 'id_token_signing_alg_values_supported': ['RS256', 'ES256', 'HS256'], + 'id_token_encryption_alg_values_supported': ['RSA1_5', 'A128KW'], + 'id_token_encryption_enc_values_supported': ['A128CBC+HS256', 'A128GCM'], + 'request_object_signing_alg_values_supported': ['none', 'RS256', 'ES256'], + 'display_values_supported': ['page', 'popup'], + 'claim_types_supported': ['normal', 'distributed'], + 'claims_supported': [ + 'sub', 'iss', 'auth_time', 'acr', 'name', 'given_name', 'family_name', + 'nickname', 'profile', 'picture', 'website', 'email', 'email_verified', + 'locale', 'zoneinfo', 'http://example.info/claims/groups' + ], + 'claims_parameter_supported': true, + 'service_documentation': + 'http://server.example.com/connect/service_documentation.html', + 'ui_locales_supported': ['en-US', 'en-GB', 'en-CA', 'fr-FR', 'fr-CA'] + }; + + let res6 = verifyAlgSupport(ci, 'RS256', 'id_token', 'signing_alg'); + it('verify_alg_support', function() { + assert.isTrue(res6); + }); + + let res7 = verifyAlgSupport(ci, 'RS512', 'id_token', 'signing_alg'); + it('verify_alg_support', function() { + assert.isFalse(res7); + }); + + let res8 = verifyAlgSupport(ci, 'RSA1_5', 'userinfo', 'encryption_alg'); + it('verify_alg_support', function() { + assert.isTrue(res8); + }); + + let res9 = verifyAlgSupport(ci, 'ES256', 'token_endpoint_auth', 'signing_alg'); + it('verify_alg_support', function() { + assert.isTrue(res9); + }); + + ci.providerInfo['issuer'] = 'https://example.com/'; + let url_list = ci.generateRequestUris('/leading'); + let sp = urlParse(url_list[0]); + let p = sp.pathname.split('/'); + + it('verify_requests_uri', function() { + assert.deepEqual(p[0], ''); + assert.deepEqual(p[1], 'leading'); + assert.deepEqual(p.length, 3); + }); + + ci.providerInfo['issuer'] = 'https://op.example.org/'; + url_list = ci.generateRequestUris('/leading'); + sp = urlParse(url_list[0]); + let np = sp.pathname.split('/'); + + it('verify_requests_uri test2', function() { + assert.deepEqual(np[0], ''); + assert.deepEqual(np[1], 'leading'); + assert.deepEqual(np.length, 3); + assert.notDeepEqual(np[2], p[2]); + }); +}); + +describe('client info tests', function() { + let config; + let ci; + beforeEach(function() { + config = { + 'client_id': 'client_id', + 'issuer': 'issuer', + 'client_secret': 'client_secret', + 'base_url': 'https://example.com', + 'requests_dir': 'requests' + }; + + ci = new ServiceContext(null, config); + }); + + it('client info init', function() { + for (let i = 0; i < Object.keys(config); i++) { + let attr = Object.keys(config)[i]; + if (attr === 'client_id') { + assert.deepEqual(ci.clientId, config[attr]); + } else if (attr === 'issuer') { + assert.deepEqual(ci.issuer, config[attr]); + } else if (attr === 'client_secret') { + assert.deepEqual(ci.clientSecret, config[attr]); + } else if (attr === 'base_url') { + assert.deepEqual(ci.base_url, config[attr]); + } else if (attr === 'requests_dir') { + assert.deepEqual(ci.base_url, config[attr]); + } + } + assert.isNotNull(ci); + }); +}); + +describe('set and get client secret', function() { + let ci; + beforeEach(function() { + ci = new ServiceContext(); + ci.clientSecret = 'supersecret'; + }); + + it('client info init', function() { + assert.deepEqual(ci.clientSecret, 'supersecret'); + }); +}); + +describe('set and get client id', function() { + let ci; + beforeEach(function() { + ci = new ServiceContext(); + ci.clientId = 'myself'; + }); + + it('client info init clientId', function() { + assert.deepEqual(ci.clientId, 'myself'); + }); +}); + +describe('client filename', function() { + let config; + let ci; + let fname; + beforeEach(function() { + config = { + 'client_id': 'client_id', + 'issuer': 'issuer', + 'client_secret': 'client_secret', + 'base_url': 'https://example.com', + 'requests_dir': 'requests' + }; + ci = new ServiceContext(null, config); + fname = ci.filenameFromWebName('https://example.com/rq12345'); + }); + it('client filename', function() { + assert.deepEqual(fname, 'rq12345'); + }); +}); \ No newline at end of file From db46883027d12240371ee77524edb2098669d1fe Mon Sep 17 00:00:00 2001 From: anjuthomas Date: Thu, 17 May 2018 18:42:15 -0700 Subject: [PATCH 2/6] ServiceContext files --- src/.DS_Store | Bin 6148 -> 0 bytes test/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/.DS_Store delete mode 100644 test/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 8fcb782953b502f146f1eff733c202cd8b0fa0c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu};H441IS{OhTk!V4T+4$`8Y#)dyATcqZXUTqd zj$bZ$LmUIJ`Sa~LFat29JL1j5*!;Tt$PQY@h;-iL9&6m;5*;2#)!!$adyFkvPk0&d zcg($Fhci52d$sA-t4+6JwLj_w{d6D3l}rjq0VyB_q<|Foy#n5QY15-bMJXT!q`;>F z{(UHP$J($@j86wci~z(5(_vi4EI}-uAl8O`A~Q5gDlw_nMhr_j^QG0*hJ9kvVR16= z(@wTFp;(;Ge2H>cO;nTuQeddSVQy#M|CjV1=Kmo{J1HOq{*?kYSzIsXe5KY~M=$5S vw$N|rAI91!XNXozj8@Ezx8jp8b;Z}bUK{p_L1#YbMEwZ3E;1?b7Ye)s`L-RR diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index c039af10f0b52e000b34b02107636e0cdcf55fde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu};HK3_L@JN-Uiau9lm2NuEFSCH>+pj4PQGkOERb3P=Gd@S6hOduh|7L`5ke z1*E{I0{(p{bjPM*pBSGGh8O{e6Q;wsj#+|OJV9(K_KD2UEUCn#S{*Sg>CBf}*Hr8i zlMaiMd7pZ+)d|JobmmKx!t8|4hqiiy#Rx$#zf@};i$n%A3(ePYm=4?0ml01E* Aq5uE@ From e89cd378b5768dddd02f197d265fab82610bf8bc Mon Sep 17 00:00:00 2001 From: anjuthomas Date: Tue, 22 May 2018 17:24:45 -0700 Subject: [PATCH 3/6] Revised ServiceContext --- src/serviceContext.js | 58 ++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/src/serviceContext.js b/src/serviceContext.js index d57c458..de1e31d 100644 --- a/src/serviceContext.js +++ b/src/serviceContext.js @@ -1,27 +1,16 @@ -const State = require('./state.js').State; const crypto = require('crypto'); const KeyJar = require('../nodeOIDCMsg/src/oicMsg/keystore/KeyJar').KeyJar; -const assert = require('chai').assert; -/** - * @fileoverview This class keeps information that a client needs to be - * able to talk to a server. Some of this information comes from - * configuration and some from dynamic provider info discovery or client - * registration. But information is also picked up during the - * conversation with a server. - */ - -/** - * serviceContext - * @class - * @constructor +/** + * ServiceContext + * This class keeps information that a client needs to be able to talk + * to a server. Some of this information comes from configuration and some + * from dynamic provider info discovery or client registration. + * But information is also picked up during the conversation with a server. + * @class */ class ServiceContext { /** - * This class keeps information that a client needs to be able to talk - * to a server. Some of this information comes from configuration and some - * from dynamic provider info discovery or client registration. - * But information is also picked up during the conversation with a server. * @param {KeyJar} keyjar OIDCMsg KeyJar instance that contains the RP signing and encyrpting keys * @param {Object} config Client configuration * @param {Object} params Other attributes that might be needed @@ -34,9 +23,7 @@ class ServiceContext { this.providerInfo = {}; this.registrationResponse = {}; this.kid = {'sig': {}, 'enc': {}}; - this.config = config || {}; - this.baseUrl = ''; this.requestDir = ''; this.allow = {}; @@ -45,13 +32,11 @@ class ServiceContext { this.cId = ''; this.cSecret = ''; this.issuer = ''; - - let serviceContext = - ['client_id', 'issuer', 'client_secret', 'base_url', 'requests_dir']; + let serviceContext = ['client_id', 'issuer', 'client_secret', 'base_url', 'requests_dir']; let defaultVal = ''; - if (params) { - for (var i = 0; i < Object.keys(params).length; i++) { + if (params){ + for (var i = 0; i < Object.keys(params).length; i++){ let key = Object.keys(params)[i]; let val = params[key]; this[key] = val; @@ -72,10 +57,9 @@ class ServiceContext { } else if (attr === 'requests_dir') { this.request_dir = this.config[attr] || defaultVal; } - }; + } - let providerInfo = - ['allow', 'client_preferences', 'behaviour', 'provider_info']; + let providerInfo = ['allow', 'client_preferences', 'behaviour', 'provider_info']; defaultVal = {}; for (let i = 0; i < providerInfo.length; i++) { let attr = providerInfo[i]; @@ -88,7 +72,7 @@ class ServiceContext { } else if (attr === 'provider_info') { this.provider_info = this.config[attr] || defaultVal; } - }; + } try { this.redirectUris = this.config['redirect_uris']; @@ -99,7 +83,7 @@ class ServiceContext { try { this.callback = this.config['callback']; } catch (err) { - this.callback = {} + this.callback = {}; } if (config && Object.keys(config).indexOf('keydefs') !== -1) { @@ -142,9 +126,9 @@ class ServiceContext { m.update(this.issuer); } m.update(this.baseUrl); - if (!path.startsWith('/')) { - return [this.baseUrl + '/' + path + '/' + m.digest('hex')]; - } else { + if (!path.startsWith('/')){ + return [this.baseUrl + '/' + path+ '/' + m.digest('hex')]; + }else{ return [this.baseUrl + path + '/' + m.digest('hex')]; } } @@ -152,16 +136,16 @@ class ServiceContext { /** * A 1<->1 map is maintained between a URL pointing to a file and * the name of the file in the file system. - * + * * As an example if the base_url is 'https://example.com' and a jwks_uri * is 'https://example.com/jwks_uri.json' then the filename of the * corresponding file on the local filesystem would be 'jwks_uri'. * Relative to the directory from which the RP instance is run. - * - * @param {*} webName + * + * @param {*} webName */ filenameFromWebName(webName) { - if (webName.startsWith(this.baseUrl) == false) { + if (webName.startsWith(this.baseUrl) == false){ console.log('ValueError'); } let name = webName.substring(this.baseUrl.length, webName.length); From 1d068143eef9296d17515ce3e24b7cb06f9395f0 Mon Sep 17 00:00:00 2001 From: anjuthomas Date: Wed, 23 May 2018 22:01:53 -0700 Subject: [PATCH 4/6] Revised ServiceContext 2 --- src/serviceContext.js | 74 ++++++++++++------------------------------- 1 file changed, 21 insertions(+), 53 deletions(-) diff --git a/src/serviceContext.js b/src/serviceContext.js index de1e31d..cc2bdf8 100644 --- a/src/serviceContext.js +++ b/src/serviceContext.js @@ -2,12 +2,10 @@ const crypto = require('crypto'); const KeyJar = require('../nodeOIDCMsg/src/oicMsg/keystore/KeyJar').KeyJar; /** - * ServiceContext * This class keeps information that a client needs to be able to talk * to a server. Some of this information comes from configuration and some * from dynamic provider info discovery or client registration. * But information is also picked up during the conversation with a server. - * @class */ class ServiceContext { /** @@ -16,26 +14,14 @@ class ServiceContext { * @param {Object} params Other attributes that might be needed */ constructor(keyjar, config, params) { - this.clientSecret = [this.getClientSecret, this.setClientSecret]; - keyjar = keyjar || null; - config = config || null; this.keyjar = keyjar || new KeyJar(); this.providerInfo = {}; this.registrationResponse = {}; this.kid = {'sig': {}, 'enc': {}}; this.config = config || {}; - this.baseUrl = ''; - this.requestDir = ''; - this.allow = {}; - this.behavior = {}; - this.clientPreferences = {}; - this.cId = ''; - this.cSecret = ''; - this.issuer = ''; - let serviceContext = ['client_id', 'issuer', 'client_secret', 'base_url', 'requests_dir']; let defaultVal = ''; - if (params){ + if (params) { for (var i = 0; i < Object.keys(params).length; i++){ let key = Object.keys(params)[i]; let val = params[key]; @@ -43,36 +29,18 @@ class ServiceContext { } } - for (let i = 0; i < serviceContext.length; i++) { - let attr = serviceContext[i]; - if (attr === 'client_id') { - this.client_id = this.config[attr] || defaultVal; - } else if (attr === 'issuer') { - this.issuer = this.config[attr] || defaultVal; - } else if (attr === 'client_secret') { - this.client_secret = this.config[attr] || defaultVal; - this.setClientSecret(this.client_secret); - } else if (attr === 'base_url') { - this.base_url = this.config[attr] || defaultVal; - } else if (attr === 'requests_dir') { - this.request_dir = this.config[attr] || defaultVal; - } - } + this.client_id = this.config['client_id'] || defaultVal; + this.issuer = this.config['issuer'] || defaultVal; + this.client_secret = this.config['client_secret'] || defaultVal; + this.setclient_secret(this.client_secret); + this.base_url = this.config['base_url'] || defaultVal; + this.request_dir = this.config['requests_dir'] || defaultVal; - let providerInfo = ['allow', 'client_preferences', 'behaviour', 'provider_info']; - defaultVal = {}; - for (let i = 0; i < providerInfo.length; i++) { - let attr = providerInfo[i]; - if (attr === 'allow') { - this.allow = this.config[attr] || defaultVal; - } else if (attr === 'client_preferences') { - this.client_prefs = this.config[attr] || defaultVal; - } else if (attr === 'behaviour') { - this.behavior = this.config[attr] || defaultVal; - } else if (attr === 'provider_info') { - this.provider_info = this.config[attr] || defaultVal; - } - } + defaultVal = {} + this.allow = this.config['allow'] || defaultVal; + this.client_prefs = this.config['client_preferences'] || defaultVal; + this.behavior = this.config['behaviour'] || defaultVal; + this.provider_info = this.config['provider_info'] || defaultVal; try { this.redirectUris = this.config['redirect_uris']; @@ -93,13 +61,13 @@ class ServiceContext { return this; } - getClientSecret() { + getclient_secret() { return this.client_secret; } - setClientSecret(val) { + setclient_secret(val) { if (!val) { - this.client_secret; + this.client_secret = ''; } else { this.client_secret = val; // client uses it for signing @@ -114,7 +82,7 @@ class ServiceContext { /** * Need to generate a redirect_uri path that is unique for a OP/RP combo - This is to counter the mix-up attack. + * This is to counter the mix-up attack. * @param {string} path Leading path * @return A list of one unique URL */ @@ -125,11 +93,11 @@ class ServiceContext { } catch (error) { m.update(this.issuer); } - m.update(this.baseUrl); + m.update(this.base_url); if (!path.startsWith('/')){ - return [this.baseUrl + '/' + path+ '/' + m.digest('hex')]; + return [this.base_url + '/' + path+ '/' + m.digest('hex')]; }else{ - return [this.baseUrl + path + '/' + m.digest('hex')]; + return [this.base_url + path + '/' + m.digest('hex')]; } } @@ -145,10 +113,10 @@ class ServiceContext { * @param {*} webName */ filenameFromWebName(webName) { - if (webName.startsWith(this.baseUrl) == false){ + if (webName.startsWith(this.base_url) == false){ console.log('ValueError'); } - let name = webName.substring(this.baseUrl.length, webName.length); + let name = webName.substring(this.base_url.length, webName.length); if (name.startsWith('/')) { return name.substring(1, name.length); } else { From 62f0214da2778bf8756162a4858a3bfd236092ba Mon Sep 17 00:00:00 2001 From: anjuthomas Date: Mon, 28 May 2018 23:01:44 -0700 Subject: [PATCH 5/6] Revised ServiceContext 3 --- src/serviceContext.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/serviceContext.js b/src/serviceContext.js index cc2bdf8..41ec189 100644 --- a/src/serviceContext.js +++ b/src/serviceContext.js @@ -28,11 +28,11 @@ class ServiceContext { this[key] = val; } } - + this.client_id = this.config['client_id'] || defaultVal; this.issuer = this.config['issuer'] || defaultVal; this.client_secret = this.config['client_secret'] || defaultVal; - this.setclient_secret(this.client_secret); + this.setClientSecret(this.client_secret); this.base_url = this.config['base_url'] || defaultVal; this.request_dir = this.config['requests_dir'] || defaultVal; @@ -48,24 +48,20 @@ class ServiceContext { this.redirectUris = [null]; } - try { - this.callback = this.config['callback']; - } catch (err) { - this.callback = {}; - } + this.callback = this.config['callback'] || {}; if (config && Object.keys(config).indexOf('keydefs') !== -1) { - this.keyjar = this.buildKeyJar(config['keydefs'], this.keyjar)[1]; + this.keyjar = this.keyjar.buildKeyJar(config['keydefs'], this.keyjar)[1]; } return this; } - getclient_secret() { + getClientSecret() { return this.client_secret; } - setclient_secret(val) { + setClientSecret(val) { if (!val) { this.client_secret = ''; } else { @@ -107,10 +103,10 @@ class ServiceContext { * * As an example if the base_url is 'https://example.com' and a jwks_uri * is 'https://example.com/jwks_uri.json' then the filename of the - * corresponding file on the local filesystem would be 'jwks_uri'. + * corresponding file on the local filesystem would be 'jwks_uri.json. * Relative to the directory from which the RP instance is run. * - * @param {*} webName + * @param {string} webName */ filenameFromWebName(webName) { if (webName.startsWith(this.base_url) == false){ From 91504ed6d231879e8be477fd022146be23174389 Mon Sep 17 00:00:00 2001 From: anjuthomas Date: Fri, 1 Jun 2018 17:48:15 -0700 Subject: [PATCH 6/6] Revised ServiceContext 4 --- src/serviceContext.js | 79 +++++++++++++++++++++++- test/serviceContext-test.js | 119 +++++++----------------------------- 2 files changed, 99 insertions(+), 99 deletions(-) diff --git a/src/serviceContext.js b/src/serviceContext.js index 41ec189..580d71b 100644 --- a/src/serviceContext.js +++ b/src/serviceContext.js @@ -1,6 +1,30 @@ const crypto = require('crypto'); const KeyJar = require('../nodeOIDCMsg/src/oicMsg/keystore/KeyJar').KeyJar; +const ATTRMAP = { + 'userinfo': { + 'sign': 'userinfo_signed_response_alg', + 'alg': 'userinfo_encrypted_response_alg', + 'enc': 'userinfo_encrypted_response_enc' + }, + 'id_token': { + 'sign': 'id_token_signed_response_alg', + 'alg': 'id_token_encrypted_response_alg', + 'enc': 'id_token_encrypted_response_enc' + }, + 'request': { + 'sign': 'request_object_signing_alg', + 'alg': 'request_object_encryption_alg', + 'enc': 'request_object_encryption_enc' + } +}; + +const DEFAULT_SIGN_ALG = { + 'userinfo': 'RS256', + 'request': 'RS384', + 'id_token': 'ES384', +}; + /** * This class keeps information that a client needs to be able to talk * to a server. Some of this information comes from configuration and some @@ -120,6 +144,59 @@ class ServiceContext { return splitName[splitName.length - 1]; } } + + /** + * Reformat the crypto algorithm information gathered from a + * client registration response into something more palatable. + * + * @param {string} typ: 'id_token', 'userinfo' or 'request_object' + */ + signEncAlgs(typ) { + let serviceContext = this; + let resp = {}; + for (let i = 0; i < Object.keys(ATTRMAP[typ]).length; i++) { + let key = Object.keys(ATTRMAP[typ])[i]; + let val = ATTRMAP[typ][key]; + if (serviceContext.registrationResponse && serviceContext.registrationResponse[val]){ + resp[key] = serviceContext.registrationResponse[val]; + }else if (key === 'sign') { + try { + resp[key] = DEFAULT_SIGN_ALG[typ]; + } catch (err) { + return; + } + } + } + return resp; + } + + /** + * Verifies that the algorithm to be used are supported by the other side. + * This will look at provider information either statically configured or + * obtained through dynamic provider info discovery. + * + * @param {string} alg The algorithm specification + * @param {string} usage In which context the 'alg' will be used. + * The following contexts are supported: + * - userinfo + * - id_token + * - request_object + * - token_endpoint_auth + * @param {string} typ Type of alg + * - signing_alg + * - encryption_alg + * - encryption_enc + */ + verifyAlgSupport(alg, usage, typ) { + let serviceContext = this; + let supported = serviceContext.providerInfo[usage + '_' + typ + '_values_supported']; + if (supported.indexOf(alg) !== -1) { + return true; + } else { + return false; + } + } } -module.exports.ServiceContext = ServiceContext; \ No newline at end of file + +module.exports.ServiceContext = ServiceContext; diff --git a/test/serviceContext-test.js b/test/serviceContext-test.js index 96b0ce3..4b72501 100644 --- a/test/serviceContext-test.js +++ b/test/serviceContext-test.js @@ -2,80 +2,6 @@ const assert = require('chai').assert; const ServiceContext = require('../src/serviceContext.js').ServiceContext; const urlParse = require('url-parse'); -const ATTRMAP = { - 'userinfo': { - 'sign': 'userinfo_signed_response_alg', - 'alg': 'userinfo_encrypted_response_alg', - 'enc': 'userinfo_encrypted_response_enc' - }, - 'id_token': { - 'sign': 'id_token_signed_response_alg', - 'alg': 'id_token_encrypted_response_alg', - 'enc': 'id_token_encrypted_response_enc' - }, - 'request': { - 'sign': 'request_object_signing_alg', - 'alg': 'request_object_encryption_alg', - 'enc': 'request_object_encryption_enc' - } -}; - -const DEFAULT_SIGN_ALG = { - 'userinfo': 'RS256', - 'request': 'RS384', - 'id_token': 'ES384', -}; - -/** - * Reformat the crypto algorithm information gathered from a - * client registration response into something more palatable. - * - * @param {string} typ: 'id_token', 'userinfo' or 'request_object' - */ -function signEncAlgs(serviceContext, typ) { - let resp = {}; - for (let i = 0; i < Object.keys(ATTRMAP[typ]).length; i++) { - let key = Object.keys(ATTRMAP[typ])[i]; - let val = ATTRMAP[typ][key]; - if (serviceContext.registrationResponse && serviceContext.registrationResponse[val]){ - resp[key] = serviceContext.registrationResponse[val]; - }else if (key === 'sign') { - try { - resp[key] = DEFAULT_SIGN_ALG[typ]; - } catch (err) { - return; - } - } - } - return resp; -} - -/** - * Verifies that the algorithm to be used are supported by the other side. - * This will look at provider information either statically configured or - * obtained through dynamic provider info discovery. - * - * @param {string} alg The algorithm specification - * @param {string} usage In which context the 'alg' will be used. - * The following contexts are supported: - * - userinfo - * - id_token - * - request_object - * - token_endpoint_auth - * @param {string} typ Type of alg - * - signing_alg - * - encryption_alg - * - encryption_enc - */ -function verifyAlgSupport(serviceContext, alg, usage, typ) { - let supported = serviceContext.providerInfo[usage + '_' + typ + '_values_supported']; - if (supported.indexOf(alg) !== -1) { - return true; - } else { - return false; - } -} - describe('', function() { let config = { 'client_id': 'client_id', @@ -103,7 +29,7 @@ describe('', function() { 'userinfo_encrypted_response_enc': 'A128CBC-HS256', }; - let res = signEncAlgs(ci, 'userinfo'); + let res = ci.signEncAlgs('userinfo'); it('registration userInfo signEncAlgs', function() { assert.deepEqual( res, {'sign': 'RS256', 'alg': 'RSA1_5', 'enc': 'A128CBC-HS256'}); @@ -122,13 +48,13 @@ describe('', function() { 'request_object_signing_alg': 'RS384' }; - res = signEncAlgs(ci, 'userinfo'); + res = ci.signEncAlgs('userinfo'); it('registration request object signEncAlgs typ userinfo', function() { assert.deepEqual( res, {'sign': 'RS256', 'alg': 'RSA1_5', 'enc': 'A128CBC-HS256'}); }); - let res2 = signEncAlgs(ci, 'request'); + let res2 = ci.signEncAlgs('request'); it('registration request object signEncAlgs typ request', function() { assert.deepEqual(res2, {'sign': 'RS384'}); }); @@ -149,18 +75,18 @@ describe('', function() { 'id_token_signed_response_alg': 'ES384', }; - let res3 = signEncAlgs(ci, 'userinfo'); + let res3 = ci.signEncAlgs('userinfo'); it('registration request object signEncAlgs typ userinfo', function() { assert.deepEqual( res3, {'sign': 'RS256', 'alg': 'RSA1_5', 'enc': 'A128CBC-HS256'}); }); - let res4 = signEncAlgs(ci, 'request'); + let res4 = ci.signEncAlgs('request'); it('registration request object signEncAlgs typ request', function() { assert.deepEqual(res4, {'sign': 'RS384'}); }); - let res5 = signEncAlgs(ci, 'id_token'); + let res5 = ci.signEncAlgs('id_token'); it('registration request object signEncAlgs typ id_token', function() { assert.deepEqual( res5, {'sign': 'ES384', 'alg': 'ECDH-ES', 'enc': 'A128GCM'}); @@ -206,22 +132,22 @@ describe('', function() { 'ui_locales_supported': ['en-US', 'en-GB', 'en-CA', 'fr-FR', 'fr-CA'] }; - let res6 = verifyAlgSupport(ci, 'RS256', 'id_token', 'signing_alg'); + let res6 = ci.verifyAlgSupport('RS256', 'id_token', 'signing_alg'); it('verify_alg_support', function() { assert.isTrue(res6); }); - let res7 = verifyAlgSupport(ci, 'RS512', 'id_token', 'signing_alg'); + let res7 = ci.verifyAlgSupport('RS512', 'id_token', 'signing_alg'); it('verify_alg_support', function() { assert.isFalse(res7); }); - let res8 = verifyAlgSupport(ci, 'RSA1_5', 'userinfo', 'encryption_alg'); + let res8 = ci.verifyAlgSupport('RSA1_5', 'userinfo', 'encryption_alg'); it('verify_alg_support', function() { assert.isTrue(res8); }); - let res9 = verifyAlgSupport(ci, 'ES256', 'token_endpoint_auth', 'signing_alg'); + let res9 = ci.verifyAlgSupport('ES256', 'token_endpoint_auth', 'signing_alg'); it('verify_alg_support', function() { assert.isTrue(res9); }); @@ -266,21 +192,18 @@ describe('client info tests', function() { }); it('client info init', function() { - for (let i = 0; i < Object.keys(config); i++) { - let attr = Object.keys(config)[i]; - if (attr === 'client_id') { - assert.deepEqual(ci.clientId, config[attr]); - } else if (attr === 'issuer') { - assert.deepEqual(ci.issuer, config[attr]); - } else if (attr === 'client_secret') { - assert.deepEqual(ci.clientSecret, config[attr]); - } else if (attr === 'base_url') { - assert.deepEqual(ci.base_url, config[attr]); - } else if (attr === 'requests_dir') { - assert.deepEqual(ci.base_url, config[attr]); - } - } assert.isNotNull(ci); + if (Object.keys(config).indexOf('client_id') > -1) { + assert.deepEqual(ci.config['client_id'], config['client_id']); + } else if (Object.keys(config).indexOf('issuer') > -1) { + assert.deepEqual(ci.config['issuer'], config['issuer']); + } else if (Object.keys(config).indexOf('client_secret') > -1) { + assert.deepEqual(ci.config['client_secret'], config['client_secret']); + } else if (Object.keys(config).indexOf('base_url') > -1) { + assert.deepEqual(ci.config['base_url'], config['base_url']); + } else if (Object.keys(config).indexOf('request_dir') > -1) { + assert.deepEqual(ci.config['request_dir'], config['request_dir']); + } }); });