@webex/plugin-authorization-browser-first-party 1.144.2 → 1.146.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,9 +40,18 @@ var _webexCore = require("@webex/webex-core");
40
40
 
41
41
  var _uuid = _interopRequireDefault(require("uuid"));
42
42
 
43
+ var _encBase64url = _interopRequireDefault(require("crypto-js/enc-base64url"));
44
+
45
+ var _cryptoJs = _interopRequireDefault(require("crypto-js"));
46
+
43
47
  var _dec, _dec2, _obj;
44
48
 
49
+ // Necessary to require lodash this way in order to stub
50
+ // methods in the unit test
51
+ var lodash = require('lodash');
52
+
45
53
  var OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token';
54
+ var OAUTH2_CODE_VERIFIER = 'oauth2-code-verifier';
46
55
  /**
47
56
  * Browser support for OAuth2. Automatically parses the URL query for an
48
57
  * authorization code
@@ -121,6 +130,8 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
121
130
  location.query.state = {};
122
131
  }
123
132
 
133
+ var codeVerifier = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CODE_VERIFIER);
134
+ this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CODE_VERIFIER);
124
135
  var email = location.query.state.email;
125
136
 
126
137
  this._verifySecurityToken(location.query);
@@ -135,7 +146,8 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
135
146
  return _promise.default.resolve();
136
147
  }).then(function () {
137
148
  return _this.requestAuthorizationCodeGrant({
138
- code: code
149
+ code: code,
150
+ codeVerifier: codeVerifier
139
151
  });
140
152
  }).then(function () {
141
153
  _this.ready = true;
@@ -156,6 +168,8 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
156
168
  options.state = options.state || {};
157
169
  options.state.csrf_token = this._generateSecurityToken();
158
170
  options.state.email = options.email;
171
+ options.code_challenge = this._generateCodeChallenge();
172
+ options.code_challenge_method = 'S256';
159
173
  return this.initiateAuthorizationCodeGrant(options);
160
174
  },
161
175
 
@@ -209,15 +223,21 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
209
223
  return _promise.default.reject(new Error('`options.code` is required'));
210
224
  }
211
225
 
226
+ var form = {
227
+ grant_type: 'authorization_code',
228
+ redirect_uri: this.config.redirect_uri,
229
+ code: options.code,
230
+ self_contained_token: true
231
+ };
232
+
233
+ if (options.codeVerifier) {
234
+ form.code_verifier = options.codeVerifier;
235
+ }
236
+
212
237
  return this.webex.request({
213
238
  method: 'POST',
214
239
  uri: this.config.tokenUrl,
215
- form: {
216
- grant_type: 'authorization_code',
217
- redirect_uri: this.config.redirect_uri,
218
- code: options.code,
219
- self_contained_token: true
220
- },
240
+ form: form,
221
241
  auth: {
222
242
  user: this.config.client_id,
223
243
  pass: this.config.client_secret,
@@ -283,6 +303,27 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
283
303
  }
284
304
  },
285
305
 
306
+ /**
307
+ * Generates PKCE code verifier and code challenge and sets the the code verifier in sessionStorage
308
+ * @instance
309
+ * @memberof AuthorizationBrowserFirstParty
310
+ * @private
311
+ * @returns {string}
312
+ */
313
+ _generateCodeChallenge: function _generateCodeChallenge() {
314
+ this.logger.info('authorization: generating PKCE code challenge'); // eslint-disable-next-line no-underscore-dangle
315
+
316
+ var safeCharacterMap = _encBase64url.default._safe_map;
317
+ var codeVerifier = lodash.times(128, function () {
318
+ return safeCharacterMap[lodash.random(0, safeCharacterMap.length - 1)];
319
+ }).join('');
320
+
321
+ var codeChallenge = _cryptoJs.default.SHA256(codeVerifier).toString(_encBase64url.default);
322
+
323
+ this.webex.getWindow().sessionStorage.setItem(OAUTH2_CODE_VERIFIER, codeVerifier);
324
+ return codeChallenge;
325
+ },
326
+
286
327
  /**
287
328
  * Generates a CSRF token and sticks in in sessionStorage
288
329
  * @instance
@@ -330,7 +371,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
330
371
  throw new Error("CSRF token ".concat(token, " does not match stored token ").concat(sessionToken));
331
372
  }
332
373
  },
333
- version: "1.144.2"
374
+ version: "1.146.1"
334
375
  }, ((0, _applyDecoratedDescriptor2.default)(_obj, "initiateAuthorizationCodeGrant", [_dec], (0, _getOwnPropertyDescriptor.default)(_obj, "initiateAuthorizationCodeGrant"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "requestAuthorizationCodeGrant", [_dec2, _common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "requestAuthorizationCodeGrant"), _obj)), _obj)));
335
376
 
336
377
  var _default = Authorization;
@@ -1 +1 @@
1
- {"version":3,"sources":["authorization.js"],"names":["OAUTH2_CSRF_TOKEN","Authorization","WebexPlugin","extend","derived","isAuthenticating","deps","fn","isAuthorizing","session","default","type","ready","namespace","initialize","attrs","ret","prototype","location","url","parse","webex","getWindow","href","_checkForErrors","code","query","state","JSON","base64","decode","email","_verifySecurityToken","_cleanUrl","process","nextTick","internal","services","collectPreauthCatalog","catch","resolve","then","requestAuthorizationCodeGrant","initiateLogin","options","csrf_token","_generateSecurityToken","initiateAuthorizationCodeGrant","logger","info","credentials","buildLoginUrl","response_type","logout","noRedirect","buildLogoutUrl","reject","Error","request","method","uri","config","tokenUrl","form","grant_type","redirect_uri","self_contained_token","auth","user","client_id","pass","client_secret","sendImmediately","shouldRefreshAccessToken","res","set","supertoken","body","statusCode","ErrorConstructor","grantErrors","select","error","_res","history","replaceState","encode","search","querystring","stringify","format","token","uuid","v4","sessionStorage","setItem","sessionToken","getItem","removeItem","oneFlight"],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.\n */\n\n/* eslint camelcase: [0] */\n\nimport querystring from 'querystring';\nimport url from 'url';\n\nimport {base64, oneFlight, whileInFlight} from '@webex/common';\nimport {grantErrors, WebexPlugin} from '@webex/webex-core';\nimport {cloneDeep, isEmpty, omit} from 'lodash';\nimport uuid from 'uuid';\n\nconst OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token';\n\n/**\n * Browser support for OAuth2. Automatically parses the URL query for an\n * authorization code\n *\n * Use of this plugin for anything other than the Webex Web Client is strongly\n * discouraged and may be broken at any time\n * @class\n * @name AuthorizationBrowserFirstParty\n * @private\n */\nconst Authorization = WebexPlugin.extend({\n derived: {\n /**\n * Alias of {@link AuthorizationBrowserFirstParty#isAuthorizing}\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @type {boolean}\n */\n isAuthenticating: {\n deps: ['isAuthorizing'],\n fn() {\n return this.isAuthorizing;\n }\n }\n },\n\n session: {\n /**\n * Indicates if an Authorization Code exchange is inflight\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @type {boolean}\n */\n isAuthorizing: {\n default: false,\n type: 'boolean'\n },\n ready: {\n default: false,\n type: 'boolean'\n }\n },\n\n namespace: 'Credentials',\n\n /**\n * Initializer\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @private\n * @returns {Authorization}\n */\n // eslint-disable-next-line complexity\n initialize(...attrs) {\n const ret = Reflect.apply(WebexPlugin.prototype.initialize, this, attrs);\n const location = url.parse(this.webex.getWindow().location.href, true);\n\n this._checkForErrors(location);\n\n const {code} = location.query;\n\n if (!code) {\n this.ready = true;\n\n return ret;\n }\n\n if (location.query.state) {\n location.query.state = JSON.parse(base64.decode(location.query.state));\n }\n else {\n location.query.state = {};\n }\n\n const {email} = location.query.state;\n\n this._verifySecurityToken(location.query);\n this._cleanUrl(location);\n\n // Wait until nextTick in case `credentials` hasn't initialized yet\n process.nextTick(() => {\n this.webex.internal.services.collectPreauthCatalog({email})\n .catch(() => Promise.resolve())\n .then(() => this.requestAuthorizationCodeGrant({code}))\n .then(() => {\n this.ready = true;\n });\n });\n\n\n return ret;\n },\n\n /**\n * Kicks off an oauth flow\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @returns {Promise}\n */\n initiateLogin(options = {}) {\n options.state = options.state || {};\n options.state.csrf_token = this._generateSecurityToken();\n options.state.email = options.email;\n\n\n return this.initiateAuthorizationCodeGrant(options);\n },\n\n @whileInFlight('isAuthorizing')\n /**\n * Kicks off the Implicit Code grant flow. Typically called via\n * {@link AuthorizationBrowserFirstParty#initiateLogin}\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @returns {Promise}\n */\n initiateAuthorizationCodeGrant(options) {\n this.logger.info('authorization: initiating authorization code grant flow');\n this.webex.getWindow().location = this.webex.credentials.buildLoginUrl(Object.assign({response_type: 'code'}, options));\n\n return Promise.resolve();\n },\n\n /**\n * Called by {@link WebexCore#logout()}. Redirects to the logout page\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @param {boolean} options.noRedirect if true, does not redirect\n * @returns {Promise}\n */\n logout(options = {}) {\n if (!options.noRedirect) {\n this.webex.getWindow().location = this.webex.credentials.buildLogoutUrl(options);\n }\n },\n\n\n @whileInFlight('isAuthorizing')\n @oneFlight\n /**\n * Exchanges an authorization code for an access token\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @param {Object} options.code\n * @returns {Promise}\n */\n requestAuthorizationCodeGrant(options = {}) {\n this.logger.info('credentials: requesting authorization code grant');\n\n if (!options.code) {\n return Promise.reject(new Error('`options.code` is required'));\n }\n\n return this.webex.request({\n method: 'POST',\n uri: this.config.tokenUrl,\n form: {\n grant_type: 'authorization_code',\n redirect_uri: this.config.redirect_uri,\n code: options.code,\n self_contained_token: true\n },\n auth: {\n user: this.config.client_id,\n pass: this.config.client_secret,\n sendImmediately: true\n },\n shouldRefreshAccessToken: false\n })\n .then((res) => {\n this.webex.credentials.set({supertoken: res.body});\n })\n .catch((res) => {\n if (res.statusCode !== 400) {\n return Promise.reject(res);\n }\n\n const ErrorConstructor = grantErrors.select(res.body.error);\n\n return Promise.reject(new ErrorConstructor(res._res || res));\n });\n },\n\n /**\n * Checks if the result of the login redirect contains an error string\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} location\n * @private\n * @returns {Promise}\n */\n _checkForErrors(location) {\n const {query} = location;\n\n if (query && query.error) {\n const ErrorConstructor = grantErrors.select(query.error);\n\n throw new ErrorConstructor(query);\n }\n },\n\n /**\n * Removes no-longer needed values from the url (access token, csrf token, etc)\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} location\n * @private\n * @returns {Promise}\n */\n _cleanUrl(location) {\n location = cloneDeep(location);\n if (this.webex.getWindow().history && this.webex.getWindow().history.replaceState) {\n Reflect.deleteProperty(location.query, 'code');\n if (isEmpty(omit(location.query.state, 'csrf_token'))) {\n Reflect.deleteProperty(location.query, 'state');\n }\n else {\n location.query.state = base64.encode(JSON.stringify(omit(location.query.state, 'csrf_token')));\n }\n location.search = querystring.stringify(location.query);\n Reflect.deleteProperty(location, 'query');\n this.webex.getWindow().history.replaceState({}, null, url.format(location));\n }\n },\n\n /**\n * Generates a CSRF token and sticks in in sessionStorage\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @private\n * @returns {Promise}\n */\n _generateSecurityToken() {\n this.logger.info('authorization: generating csrf token');\n\n const token = uuid.v4();\n\n this.webex.getWindow().sessionStorage.setItem('oauth2-csrf-token', token);\n\n return token;\n },\n\n /**\n * Checks if the CSRF token in sessionStorage is the same as the one returned\n * in the url.\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} query\n * @private\n * @returns {Promise}\n */\n _verifySecurityToken(query) {\n const sessionToken = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CSRF_TOKEN);\n\n this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CSRF_TOKEN);\n if (!sessionToken) {\n return;\n }\n\n if (!query.state) {\n throw new Error(`Expected CSRF token ${sessionToken}, but not found in redirect query`);\n }\n\n if (!query.state.csrf_token) {\n throw new Error(`Expected CSRF token ${sessionToken}, but not found in redirect query`);\n }\n\n const token = query.state.csrf_token;\n\n if (token !== sessionToken) {\n throw new Error(`CSRF token ${token} does not match stored token ${sessionToken}`);\n }\n }\n});\n\nexport default Authorization;\n"]}
1
+ {"version":3,"sources":["authorization.js"],"names":["lodash","require","OAUTH2_CSRF_TOKEN","OAUTH2_CODE_VERIFIER","Authorization","WebexPlugin","extend","derived","isAuthenticating","deps","fn","isAuthorizing","session","default","type","ready","namespace","initialize","attrs","ret","prototype","location","url","parse","webex","getWindow","href","_checkForErrors","code","query","state","JSON","base64","decode","codeVerifier","sessionStorage","getItem","removeItem","email","_verifySecurityToken","_cleanUrl","process","nextTick","internal","services","collectPreauthCatalog","catch","resolve","then","requestAuthorizationCodeGrant","initiateLogin","options","csrf_token","_generateSecurityToken","code_challenge","_generateCodeChallenge","code_challenge_method","initiateAuthorizationCodeGrant","logger","info","credentials","buildLoginUrl","response_type","logout","noRedirect","buildLogoutUrl","reject","Error","form","grant_type","redirect_uri","config","self_contained_token","code_verifier","request","method","uri","tokenUrl","auth","user","client_id","pass","client_secret","sendImmediately","shouldRefreshAccessToken","res","set","supertoken","body","statusCode","ErrorConstructor","grantErrors","select","error","_res","history","replaceState","encode","search","querystring","stringify","format","safeCharacterMap","base64url","_safe_map","times","random","length","join","codeChallenge","CryptoJS","SHA256","toString","setItem","token","uuid","v4","sessionToken","oneFlight"],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.\n */\n\n/* eslint camelcase: [0] */\n\nimport querystring from 'querystring';\nimport url from 'url';\n\nimport {base64, oneFlight, whileInFlight} from '@webex/common';\nimport {grantErrors, WebexPlugin} from '@webex/webex-core';\nimport {cloneDeep, isEmpty, omit} from 'lodash';\nimport uuid from 'uuid';\nimport base64url from 'crypto-js/enc-base64url';\nimport CryptoJS from 'crypto-js';\n\n// Necessary to require lodash this way in order to stub\n// methods in the unit test\nconst lodash = require('lodash');\n\nconst OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token';\nconst OAUTH2_CODE_VERIFIER = 'oauth2-code-verifier';\n\n/**\n * Browser support for OAuth2. Automatically parses the URL query for an\n * authorization code\n *\n * Use of this plugin for anything other than the Webex Web Client is strongly\n * discouraged and may be broken at any time\n * @class\n * @name AuthorizationBrowserFirstParty\n * @private\n */\nconst Authorization = WebexPlugin.extend({\n derived: {\n /**\n * Alias of {@link AuthorizationBrowserFirstParty#isAuthorizing}\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @type {boolean}\n */\n isAuthenticating: {\n deps: ['isAuthorizing'],\n fn() {\n return this.isAuthorizing;\n }\n }\n },\n\n session: {\n /**\n * Indicates if an Authorization Code exchange is inflight\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @type {boolean}\n */\n isAuthorizing: {\n default: false,\n type: 'boolean'\n },\n ready: {\n default: false,\n type: 'boolean'\n }\n },\n\n namespace: 'Credentials',\n\n /**\n * Initializer\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @private\n * @returns {Authorization}\n */\n // eslint-disable-next-line complexity\n initialize(...attrs) {\n const ret = Reflect.apply(WebexPlugin.prototype.initialize, this, attrs);\n const location = url.parse(this.webex.getWindow().location.href, true);\n\n this._checkForErrors(location);\n\n const {code} = location.query;\n\n if (!code) {\n this.ready = true;\n\n return ret;\n }\n\n if (location.query.state) {\n location.query.state = JSON.parse(base64.decode(location.query.state));\n }\n else {\n location.query.state = {};\n }\n\n const codeVerifier = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CODE_VERIFIER);\n\n this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CODE_VERIFIER);\n\n\n const {email} = location.query.state;\n\n this._verifySecurityToken(location.query);\n this._cleanUrl(location);\n\n // Wait until nextTick in case `credentials` hasn't initialized yet\n process.nextTick(() => {\n this.webex.internal.services.collectPreauthCatalog({email})\n .catch(() => Promise.resolve())\n .then(() => this.requestAuthorizationCodeGrant({code, codeVerifier}))\n .then(() => {\n this.ready = true;\n });\n });\n\n\n return ret;\n },\n\n /**\n * Kicks off an oauth flow\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @returns {Promise}\n */\n initiateLogin(options = {}) {\n options.state = options.state || {};\n options.state.csrf_token = this._generateSecurityToken();\n options.state.email = options.email;\n\n options.code_challenge = this._generateCodeChallenge();\n options.code_challenge_method = 'S256';\n\n\n return this.initiateAuthorizationCodeGrant(options);\n },\n\n @whileInFlight('isAuthorizing')\n /**\n * Kicks off the Implicit Code grant flow. Typically called via\n * {@link AuthorizationBrowserFirstParty#initiateLogin}\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @returns {Promise}\n */\n initiateAuthorizationCodeGrant(options) {\n this.logger.info('authorization: initiating authorization code grant flow');\n this.webex.getWindow().location = this.webex.credentials.buildLoginUrl(Object.assign({response_type: 'code'}, options));\n\n return Promise.resolve();\n },\n\n /**\n * Called by {@link WebexCore#logout()}. Redirects to the logout page\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @param {boolean} options.noRedirect if true, does not redirect\n * @returns {Promise}\n */\n logout(options = {}) {\n if (!options.noRedirect) {\n this.webex.getWindow().location = this.webex.credentials.buildLogoutUrl(options);\n }\n },\n\n\n @whileInFlight('isAuthorizing')\n @oneFlight\n /**\n * Exchanges an authorization code for an access token\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} options\n * @param {Object} options.code\n * @returns {Promise}\n */\n requestAuthorizationCodeGrant(options = {}) {\n this.logger.info('credentials: requesting authorization code grant');\n\n if (!options.code) {\n return Promise.reject(new Error('`options.code` is required'));\n }\n\n const form = {\n grant_type: 'authorization_code',\n redirect_uri: this.config.redirect_uri,\n code: options.code,\n self_contained_token: true\n };\n\n if (options.codeVerifier) {\n form.code_verifier = options.codeVerifier;\n }\n\n return this.webex.request({\n method: 'POST',\n uri: this.config.tokenUrl,\n form,\n auth: {\n user: this.config.client_id,\n pass: this.config.client_secret,\n sendImmediately: true\n },\n shouldRefreshAccessToken: false\n })\n .then((res) => {\n this.webex.credentials.set({supertoken: res.body});\n })\n .catch((res) => {\n if (res.statusCode !== 400) {\n return Promise.reject(res);\n }\n\n const ErrorConstructor = grantErrors.select(res.body.error);\n\n return Promise.reject(new ErrorConstructor(res._res || res));\n });\n },\n\n /**\n * Checks if the result of the login redirect contains an error string\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} location\n * @private\n * @returns {Promise}\n */\n _checkForErrors(location) {\n const {query} = location;\n\n if (query && query.error) {\n const ErrorConstructor = grantErrors.select(query.error);\n\n throw new ErrorConstructor(query);\n }\n },\n\n /**\n * Removes no-longer needed values from the url (access token, csrf token, etc)\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} location\n * @private\n * @returns {Promise}\n */\n _cleanUrl(location) {\n location = cloneDeep(location);\n if (this.webex.getWindow().history && this.webex.getWindow().history.replaceState) {\n Reflect.deleteProperty(location.query, 'code');\n if (isEmpty(omit(location.query.state, 'csrf_token'))) {\n Reflect.deleteProperty(location.query, 'state');\n }\n else {\n location.query.state = base64.encode(JSON.stringify(omit(location.query.state, 'csrf_token')));\n }\n location.search = querystring.stringify(location.query);\n Reflect.deleteProperty(location, 'query');\n this.webex.getWindow().history.replaceState({}, null, url.format(location));\n }\n },\n\n /**\n * Generates PKCE code verifier and code challenge and sets the the code verifier in sessionStorage\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @private\n * @returns {string}\n */\n _generateCodeChallenge() {\n this.logger.info('authorization: generating PKCE code challenge');\n\n // eslint-disable-next-line no-underscore-dangle\n const safeCharacterMap = base64url._safe_map;\n\n const codeVerifier = lodash.times(\n 128,\n () => safeCharacterMap[lodash.random(0, safeCharacterMap.length - 1)]\n ).join('');\n\n const codeChallenge = CryptoJS.SHA256(codeVerifier).toString(base64url);\n\n this.webex.getWindow().sessionStorage.setItem(\n OAUTH2_CODE_VERIFIER, codeVerifier\n );\n\n return codeChallenge;\n },\n\n /**\n * Generates a CSRF token and sticks in in sessionStorage\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @private\n * @returns {Promise}\n */\n _generateSecurityToken() {\n this.logger.info('authorization: generating csrf token');\n\n const token = uuid.v4();\n\n this.webex.getWindow().sessionStorage.setItem('oauth2-csrf-token', token);\n\n return token;\n },\n\n /**\n * Checks if the CSRF token in sessionStorage is the same as the one returned\n * in the url.\n * @instance\n * @memberof AuthorizationBrowserFirstParty\n * @param {Object} query\n * @private\n * @returns {Promise}\n */\n _verifySecurityToken(query) {\n const sessionToken = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CSRF_TOKEN);\n\n this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CSRF_TOKEN);\n if (!sessionToken) {\n return;\n }\n\n if (!query.state) {\n throw new Error(`Expected CSRF token ${sessionToken}, but not found in redirect query`);\n }\n\n if (!query.state.csrf_token) {\n throw new Error(`Expected CSRF token ${sessionToken}, but not found in redirect query`);\n }\n\n const token = query.state.csrf_token;\n\n if (token !== sessionToken) {\n throw new Error(`CSRF token ${token} does not match stored token ${sessionToken}`);\n }\n }\n});\n\nexport default Authorization;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webex/plugin-authorization-browser-first-party",
3
- "version": "1.144.2",
3
+ "version": "1.146.1",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "Ian W. Remmel <iremmel@cisco.com>",
@@ -17,10 +17,11 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@babel/runtime-corejs2": "^7.14.8",
20
- "@webex/webex-core": "1.144.2",
20
+ "@webex/webex-core": "1.146.1",
21
21
  "lodash": "^4.17.21",
22
- "@webex/common": "1.144.2",
22
+ "@webex/common": "1.146.1",
23
23
  "uuid": "^3.3.2",
24
+ "crypto-js": "^4.1.1",
24
25
  "envify": "^4.1.0"
25
26
  }
26
27
  }
@@ -11,8 +11,15 @@ import {base64, oneFlight, whileInFlight} from '@webex/common';
11
11
  import {grantErrors, WebexPlugin} from '@webex/webex-core';
12
12
  import {cloneDeep, isEmpty, omit} from 'lodash';
13
13
  import uuid from 'uuid';
14
+ import base64url from 'crypto-js/enc-base64url';
15
+ import CryptoJS from 'crypto-js';
16
+
17
+ // Necessary to require lodash this way in order to stub
18
+ // methods in the unit test
19
+ const lodash = require('lodash');
14
20
 
15
21
  const OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token';
22
+ const OAUTH2_CODE_VERIFIER = 'oauth2-code-verifier';
16
23
 
17
24
  /**
18
25
  * Browser support for OAuth2. Automatically parses the URL query for an
@@ -88,6 +95,11 @@ const Authorization = WebexPlugin.extend({
88
95
  location.query.state = {};
89
96
  }
90
97
 
98
+ const codeVerifier = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CODE_VERIFIER);
99
+
100
+ this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CODE_VERIFIER);
101
+
102
+
91
103
  const {email} = location.query.state;
92
104
 
93
105
  this._verifySecurityToken(location.query);
@@ -97,7 +109,7 @@ const Authorization = WebexPlugin.extend({
97
109
  process.nextTick(() => {
98
110
  this.webex.internal.services.collectPreauthCatalog({email})
99
111
  .catch(() => Promise.resolve())
100
- .then(() => this.requestAuthorizationCodeGrant({code}))
112
+ .then(() => this.requestAuthorizationCodeGrant({code, codeVerifier}))
101
113
  .then(() => {
102
114
  this.ready = true;
103
115
  });
@@ -119,6 +131,9 @@ const Authorization = WebexPlugin.extend({
119
131
  options.state.csrf_token = this._generateSecurityToken();
120
132
  options.state.email = options.email;
121
133
 
134
+ options.code_challenge = this._generateCodeChallenge();
135
+ options.code_challenge_method = 'S256';
136
+
122
137
 
123
138
  return this.initiateAuthorizationCodeGrant(options);
124
139
  },
@@ -171,15 +186,21 @@ const Authorization = WebexPlugin.extend({
171
186
  return Promise.reject(new Error('`options.code` is required'));
172
187
  }
173
188
 
189
+ const form = {
190
+ grant_type: 'authorization_code',
191
+ redirect_uri: this.config.redirect_uri,
192
+ code: options.code,
193
+ self_contained_token: true
194
+ };
195
+
196
+ if (options.codeVerifier) {
197
+ form.code_verifier = options.codeVerifier;
198
+ }
199
+
174
200
  return this.webex.request({
175
201
  method: 'POST',
176
202
  uri: this.config.tokenUrl,
177
- form: {
178
- grant_type: 'authorization_code',
179
- redirect_uri: this.config.redirect_uri,
180
- code: options.code,
181
- self_contained_token: true
182
- },
203
+ form,
183
204
  auth: {
184
205
  user: this.config.client_id,
185
206
  pass: this.config.client_secret,
@@ -243,6 +264,33 @@ const Authorization = WebexPlugin.extend({
243
264
  }
244
265
  },
245
266
 
267
+ /**
268
+ * Generates PKCE code verifier and code challenge and sets the the code verifier in sessionStorage
269
+ * @instance
270
+ * @memberof AuthorizationBrowserFirstParty
271
+ * @private
272
+ * @returns {string}
273
+ */
274
+ _generateCodeChallenge() {
275
+ this.logger.info('authorization: generating PKCE code challenge');
276
+
277
+ // eslint-disable-next-line no-underscore-dangle
278
+ const safeCharacterMap = base64url._safe_map;
279
+
280
+ const codeVerifier = lodash.times(
281
+ 128,
282
+ () => safeCharacterMap[lodash.random(0, safeCharacterMap.length - 1)]
283
+ ).join('');
284
+
285
+ const codeChallenge = CryptoJS.SHA256(codeVerifier).toString(base64url);
286
+
287
+ this.webex.getWindow().sessionStorage.setItem(
288
+ OAUTH2_CODE_VERIFIER, codeVerifier
289
+ );
290
+
291
+ return codeChallenge;
292
+ },
293
+
246
294
  /**
247
295
  * Generates a CSRF token and sticks in in sessionStorage
248
296
  * @instance
@@ -11,13 +11,18 @@ import {browserOnly} from '@webex/test-helper-mocha';
11
11
  import sinon from 'sinon';
12
12
  import MockWebex from '@webex/test-helper-mock-webex';
13
13
  import {Credentials, Services} from '@webex/webex-core';
14
- import Authorization from '@webex/plugin-authorization-browser-first-party';
15
14
  import {base64, patterns} from '@webex/common';
16
- import {merge} from 'lodash';
15
+ import {merge, times} from 'lodash';
16
+ import CryptoJS from 'crypto-js';
17
+ import Authorization from '@webex/plugin-authorization-browser-first-party';
18
+
19
+ // Necessary to require lodash this way in order to stub the method
20
+ const lodash = require('lodash');
21
+
17
22
 
18
23
  browserOnly(describe)('plugin-authorization-browser-first-party', () => {
19
24
  describe('Authorization', () => {
20
- function makeWebex(href = 'https://example.com', csrfToken = undefined, config = {}) {
25
+ function makeWebex(href = 'https://example.com', csrfToken = undefined, pkceVerifier = undefined, config = {}) {
21
26
  const mockWindow = {
22
27
  history: {
23
28
  replaceState(a, b, location) {
@@ -28,7 +33,11 @@ browserOnly(describe)('plugin-authorization-browser-first-party', () => {
28
33
  href
29
34
  },
30
35
  sessionStorage: {
31
- getItem: sinon.stub().returns(csrfToken),
36
+ getItem: sinon.stub()
37
+ .onCall(0)
38
+ .returns(pkceVerifier)
39
+ .onCall(1)
40
+ .returns(csrfToken),
32
41
  removeItem: sinon.spy(),
33
42
  setItem: sinon.spy()
34
43
  }
@@ -86,7 +95,10 @@ browserOnly(describe)('plugin-authorization-browser-first-party', () => {
86
95
 
87
96
  return webex.authorization.when('change:ready')
88
97
  .then(() => {
89
- assert.calledOnce(webex.request);
98
+ // Webex request gets called twice:
99
+ // once for the pre-auth catalog
100
+ // once for auth token exchange
101
+ assert.calledTwice(webex.request);
90
102
  assert.isTrue(webex.authorization.ready);
91
103
  assert.isTrue(webex.credentials.canAuthorize);
92
104
  });
@@ -149,6 +161,35 @@ browserOnly(describe)('plugin-authorization-browser-first-party', () => {
149
161
  assert.isFalse(webex.credentials.canAuthorize);
150
162
  });
151
163
  });
164
+
165
+ describe('when code verifier is present in session storage', () => {
166
+ it('passes codeVerifier to requestAuthorizationCodeGrant', () => {
167
+ const expectedVerifier = 'test verifier';
168
+
169
+ const webex = makeWebex(
170
+ 'http://example.com?code=5',
171
+ undefined,
172
+ expectedVerifier
173
+ );
174
+
175
+ return webex.authorization.when('change:ready')
176
+ .then(() => {
177
+ assert.calledTwice(webex.request);
178
+ assert.calledWith(
179
+ webex.getWindow().sessionStorage.getItem,
180
+ 'oauth2-code-verifier'
181
+ );
182
+ assert.calledWith(
183
+ webex.getWindow().sessionStorage.removeItem,
184
+ 'oauth2-code-verifier'
185
+ );
186
+ assert.equal(
187
+ webex.request.getCall(1).args[0].form.code_verifier,
188
+ expectedVerifier
189
+ );
190
+ });
191
+ });
192
+ });
152
193
  });
153
194
 
154
195
  describe('#initiateLogin()', () => {
@@ -193,6 +234,31 @@ browserOnly(describe)('plugin-authorization-browser-first-party', () => {
193
234
  });
194
235
  });
195
236
 
237
+ it('adds a pkce code challenge', () => {
238
+ const webex = makeWebex(undefined, undefined, {
239
+ credentials: {
240
+ clientType: 'confidential'
241
+ }
242
+ });
243
+
244
+ const expectedCodeChallenge = 'test challenge';
245
+
246
+ sinon.spy(webex.authorization, 'initiateAuthorizationCodeGrant');
247
+ sinon.stub(webex.authorization, '_generateCodeChallenge')
248
+ .returns(expectedCodeChallenge);
249
+
250
+ return webex.authorization.initiateLogin()
251
+ .then(() => {
252
+ assert.called(webex.authorization.initiateAuthorizationCodeGrant);
253
+ const grantOptions = webex.authorization.initiateAuthorizationCodeGrant.getCall(0).args[0];
254
+
255
+ assert.equal(grantOptions.code_challenge, expectedCodeChallenge);
256
+ assert.equal(grantOptions.code_challenge_method, 'S256');
257
+ // eslint-disable-next-line no-underscore-dangle
258
+ assert.calledWith(webex.authorization._generateCodeChallenge);
259
+ });
260
+ });
261
+
196
262
  it('sets #isAuthorizing', () => {
197
263
  const webex = makeWebex(undefined, undefined, {
198
264
  credentials: {
@@ -242,6 +308,38 @@ browserOnly(describe)('plugin-authorization-browser-first-party', () => {
242
308
  });
243
309
  });
244
310
 
311
+ describe('#_generateCodeChallenge', () => {
312
+ const expectedCodeChallenge = 'code challenge';
313
+ // eslint-disable-next-line no-underscore-dangle
314
+ const safeCharacterMap = CryptoJS.enc.Base64url._safe_map;
315
+
316
+ const expectedVerifier = times(128, () => safeCharacterMap[0]).join('');
317
+
318
+ it('generates a challenge code and stores it in session storage', () => {
319
+ const webex = makeWebex('http://example.com');
320
+
321
+ const toStringStub = sinon.stub().returns(expectedCodeChallenge);
322
+ const randomStub = sinon.stub(lodash, 'random').returns(0);
323
+ const sha256Stub = sinon.stub(CryptoJS, 'SHA256').returns({
324
+ toString: toStringStub
325
+ });
326
+
327
+ // eslint-disable-next-line no-underscore-dangle
328
+ const codeChallenge = webex.authorization._generateCodeChallenge();
329
+
330
+ assert.equal(codeChallenge, expectedCodeChallenge);
331
+ assert.calledWith(sha256Stub, expectedVerifier);
332
+ assert.calledWith(toStringStub, CryptoJS.enc.Base64url);
333
+ assert.callCount(randomStub, 128);
334
+ assert.calledWith(randomStub, 0, safeCharacterMap.length - 1);
335
+ assert.calledWith(
336
+ webex.getWindow().sessionStorage.setItem,
337
+ 'oauth2-code-verifier',
338
+ expectedVerifier
339
+ );
340
+ });
341
+ });
342
+
245
343
  describe('#_cleanUrl()', () => {
246
344
  it('removes the state parameter when it has no keys', () => {
247
345
  const webex = makeWebex(undefined, undefined, {