From a44d7fb69d7cf94d3ebc0ee6eb74d13cfc79de04 Mon Sep 17 00:00:00 2001 From: Priya Date: Thu, 6 Aug 2026 23:14:36 +0530 Subject: [PATCH 1/2] fix(js-sdk): small improvements --- .../src/authorization.js | 17 ++++---- .../test/unit/spec/authorization.js | 41 +++++++++++++++---- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js index e3c6cb56f13..d58d047b9f1 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js @@ -21,10 +21,6 @@ import uuid from 'uuid'; import base64url from 'crypto-js/enc-base64url'; import CryptoJS from 'crypto-js'; -// Necessary to require lodash this way in order to stub -// methods in the unit test -const lodash = require('lodash'); - const OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token'; const OAUTH2_CODE_VERIFIER = 'oauth2-code-verifier'; @@ -825,7 +821,8 @@ const Authorization = WebexPlugin.extend({ * during authorization code exchange; removes it once consumed. * * Implementation details: - * - Creates a 128 character string using base64url safe alphabet. + * - Creates a 128 character string using a cryptographically secure random + * source and the base64url safe alphabet. * - Computes SHA256 hash, encodes to base64url (no padding). * * @instance @@ -838,10 +835,14 @@ const Authorization = WebexPlugin.extend({ // eslint-disable-next-line no-underscore-dangle const safeCharacterMap = base64url._safe_map; + const randomValues = new Uint8Array(128); + + this.webex.getWindow().crypto.getRandomValues(randomValues); - const codeVerifier = lodash - .times(128, () => safeCharacterMap[lodash.random(0, safeCharacterMap.length - 1)]) - .join(''); + const codeVerifier = Array.from( + randomValues, + (randomValue) => safeCharacterMap[randomValue & (safeCharacterMap.length - 1)] + ).join(''); const codeChallenge = CryptoJS.SHA256(codeVerifier).toString(base64url); diff --git a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js index 71cbc57a35a..05d99ad6a43 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js @@ -16,18 +16,19 @@ import CryptoJS from 'crypto-js'; import Authorization from '@webex/plugin-authorization-browser-first-party'; import {Events} from '../../../src'; -// Necessary to require lodash this way in order to stub the method -const lodash = require('lodash'); - describe('plugin-authorization-browser-first-party', () => { describe('Authorization', () => { function makeWebex( href = 'https://example.com', csrfToken = undefined, pkceVerifier = undefined, - config = {} + config = {}, + getRandomValues = sinon.stub().callsFake((randomValues) => randomValues.fill(0)) ) { const mockWindow = { + crypto: { + getRandomValues, + }, history: { replaceState(a, b, location) { mockWindow.location.href = location; @@ -1225,25 +1226,47 @@ describe('plugin-authorization-browser-first-party', () => { // eslint-disable-next-line no-underscore-dangle const safeCharacterMap = CryptoJS.enc.Base64url._safe_map; - const expectedVerifier = times(128, () => safeCharacterMap[0]).join(''); + const expectedVerifier = times( + 128, + (index) => safeCharacterMap[index & (safeCharacterMap.length - 1)] + ).join(''); it('generates a challenge code and stores it in session storage', () => { - const webex = makeWebex('http://example.com'); + let generatedRandomValues; + let getRandomValuesCallCount = 0; + const getRandomValuesStub = sinon.stub().callsFake((randomValues) => { + getRandomValuesCallCount += 1; + generatedRandomValues = randomValues; + randomValues.set(times(128, (index) => index)); + + return randomValues; + }); + const webex = makeWebex( + 'http://example.com', + undefined, + undefined, + {}, + getRandomValuesStub + ); const toStringStub = sinon.stub().returns(expectedCodeChallenge); - const randomStub = sinon.stub(lodash, 'random').returns(0); + const mathRandomStub = sinon.stub(Math, 'random'); const sha256Stub = sinon.stub(CryptoJS, 'SHA256').returns({ toString: toStringStub, }); + getRandomValuesStub.resetHistory(); + getRandomValuesCallCount = 0; + // eslint-disable-next-line no-underscore-dangle const codeChallenge = webex.authorization._generateCodeChallenge(); assert.equal(codeChallenge, expectedCodeChallenge); + assert.equal(getRandomValuesCallCount, 1); + assert.instanceOf(generatedRandomValues, Uint8Array); + assert.notCalled(mathRandomStub); assert.calledWith(sha256Stub, expectedVerifier); assert.calledWith(toStringStub, CryptoJS.enc.Base64url); - assert.callCount(randomStub, 128); - assert.calledWith(randomStub, 0, safeCharacterMap.length - 1); assert.calledWith( webex.getWindow().sessionStorage.setItem, 'oauth2-code-verifier', From eac820f79430832b2107018fbc9541645cae655d Mon Sep 17 00:00:00 2001 From: Priya Date: Thu, 6 Aug 2026 23:19:47 +0530 Subject: [PATCH 2/2] fix(js-sdk): test coverage --- .../test/unit/spec/authorization.js | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js index 05d99ad6a43..e3fb6bf7fd0 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js @@ -16,6 +16,9 @@ import CryptoJS from 'crypto-js'; import Authorization from '@webex/plugin-authorization-browser-first-party'; import {Events} from '../../../src'; +// Necessary to require lodash this way in order to stub the method +const lodash = require('lodash'); + describe('plugin-authorization-browser-first-party', () => { describe('Authorization', () => { function makeWebex( @@ -1222,22 +1225,12 @@ describe('plugin-authorization-browser-first-party', () => { }); describe('#_generateCodeChallenge', () => { - const expectedCodeChallenge = 'code challenge'; // eslint-disable-next-line no-underscore-dangle const safeCharacterMap = CryptoJS.enc.Base64url._safe_map; - const expectedVerifier = times( - 128, - (index) => safeCharacterMap[index & (safeCharacterMap.length - 1)] - ).join(''); - - it('generates a challenge code and stores it in session storage', () => { - let generatedRandomValues; - let getRandomValuesCallCount = 0; + function makeWebexWithRandomValues(fillRandomValues) { const getRandomValuesStub = sinon.stub().callsFake((randomValues) => { - getRandomValuesCallCount += 1; - generatedRandomValues = randomValues; - randomValues.set(times(128, (index) => index)); + fillRandomValues(randomValues); return randomValues; }); @@ -1249,29 +1242,67 @@ describe('plugin-authorization-browser-first-party', () => { getRandomValuesStub ); - const toStringStub = sinon.stub().returns(expectedCodeChallenge); + getRandomValuesStub.resetHistory(); + webex.getWindow().sessionStorage.setItem.resetHistory(); + + return {getRandomValuesStub, webex}; + } + + it('uses a 128-byte CSPRNG and does not use insecure random generators', () => { + const {getRandomValuesStub, webex} = makeWebexWithRandomValues((randomValues) => { + randomValues.fill(0); + }); const mathRandomStub = sinon.stub(Math, 'random'); - const sha256Stub = sinon.stub(CryptoJS, 'SHA256').returns({ - toString: toStringStub, + const lodashRandomStub = sinon.stub(lodash, 'random'); + + // eslint-disable-next-line no-underscore-dangle + webex.authorization._generateCodeChallenge(); + + const generatedRandomValues = getRandomValuesStub.firstCall.args[0]; + + assert.calledOnceWithExactly(getRandomValuesStub, generatedRandomValues); + assert.instanceOf(generatedRandomValues, Uint8Array); + assert.lengthOf(generatedRandomValues, 128); + assert.notCalled(mathRandomStub); + assert.notCalled(lodashRandomStub); + }); + + it('generates a 128-character verifier from the base64url-safe alphabet', () => { + const {webex} = makeWebexWithRandomValues((randomValues) => { + randomValues.set(times(128, (index) => index)); }); + const expectedVerifier = times( + 128, + (index) => safeCharacterMap[index & (safeCharacterMap.length - 1)] + ).join(''); - getRandomValuesStub.resetHistory(); - getRandomValuesCallCount = 0; + // eslint-disable-next-line no-underscore-dangle + webex.authorization._generateCodeChallenge(); + + const storedVerifier = webex.getWindow().sessionStorage.setItem.firstCall.args[1]; + + assert.match(storedVerifier, /^[A-Za-z0-9_-]{128}$/); + assert.equal(storedVerifier, expectedVerifier); + }); + + it('stores the verifier and returns its SHA-256 base64url challenge', () => { + const {webex} = makeWebexWithRandomValues((randomValues) => { + randomValues.fill(42); + }); + const expectedVerifier = safeCharacterMap[42 & (safeCharacterMap.length - 1)].repeat(128); + const expectedChallenge = CryptoJS.SHA256(expectedVerifier).toString( + CryptoJS.enc.Base64url + ); // eslint-disable-next-line no-underscore-dangle const codeChallenge = webex.authorization._generateCodeChallenge(); - assert.equal(codeChallenge, expectedCodeChallenge); - assert.equal(getRandomValuesCallCount, 1); - assert.instanceOf(generatedRandomValues, Uint8Array); - assert.notCalled(mathRandomStub); - assert.calledWith(sha256Stub, expectedVerifier); - assert.calledWith(toStringStub, CryptoJS.enc.Base64url); - assert.calledWith( + assert.calledOnceWithExactly( webex.getWindow().sessionStorage.setItem, 'oauth2-code-verifier', expectedVerifier ); + assert.equal(codeChallenge, expectedChallenge); }); });