diff --git a/docs/Gemfile b/docs/Gemfile index 3b2e3aec..d72001c4 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -7,12 +7,12 @@ source "https://rubygems.org" # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! -gem "jekyll", "~> 3.9.2" +gem "jekyll", "~> 4.2" # This is the default theme for new Jekyll sites. You may change this to anything you like. gem "minima", "~> 2.5" # If you want to use GitHub Pages, remove the "gem "jekyll"" above and # uncomment the line below. To upgrade, run `bundle update github-pages`. -gem "github-pages", "~> 227", group: :jekyll_plugins +#gem "github-pages", "~> 227", group: :jekyll_plugins # If you have any plugins, put them here! group :jekyll_plugins do gem "jekyll-feed", "~> 0.12" @@ -32,4 +32,4 @@ gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] # do not have a Java counterpart. gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] -gem "webrick", "~> 1.7" +gem "webrick", "~> 1.8" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 6c64cd12..b6680534 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,271 +1,97 @@ GEM remote: https://rubygems.org/ specs: - activesupport (6.0.6) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.11.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + bigdecimal (3.1.8) colorator (1.1.0) - commonmarker (0.23.6) - concurrent-ruby (1.1.10) - dnsruby (1.61.9) - simpleidn (~> 0.1) + concurrent-ruby (1.3.4) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - ethon (0.15.0) - ffi (>= 1.15.0) eventmachine (1.2.7) - execjs (2.8.1) - faraday (2.6.0) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.1) - ffi (1.15.5) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-darwin) forwardable-extended (2.6.0) - gemoji (3.0.1) - github-pages (227) - github-pages-health-check (= 1.17.9) - jekyll (= 3.9.2) - jekyll-avatar (= 0.7.0) - jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.2.0) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.15.1) - jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.13.0) - jekyll-include-cache (= 0.2.1) - jekyll-mentions (= 1.6.0) - jekyll-optional-front-matter (= 0.3.2) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.16.0) - jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.3) - jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.8.0) - jekyll-sitemap (= 1.4.0) - jekyll-swiss (= 1.0.0) - jekyll-theme-architect (= 0.2.0) - jekyll-theme-cayman (= 0.2.0) - jekyll-theme-dinky (= 0.2.0) - jekyll-theme-hacker (= 0.2.0) - jekyll-theme-leap-day (= 0.2.0) - jekyll-theme-merlot (= 0.2.0) - jekyll-theme-midnight (= 0.2.0) - jekyll-theme-minimal (= 0.2.0) - jekyll-theme-modernist (= 0.2.0) - jekyll-theme-primer (= 0.6.0) - jekyll-theme-slate (= 0.2.0) - jekyll-theme-tactile (= 0.2.0) - jekyll-theme-time-machine (= 0.2.0) - jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.12.0) - kramdown (= 2.3.2) - kramdown-parser-gfm (= 1.1.0) - liquid (= 4.0.3) - mercenary (~> 0.3) - minima (= 2.5.1) - nokogiri (>= 1.13.6, < 2.0) - rouge (= 3.26.0) - terminal-table (~> 1.4) - github-pages-health-check (1.17.9) - addressable (~> 2.3) - dnsruby (~> 1.60) - octokit (~> 4.0) - public_suffix (>= 3.0, < 5.0) - typhoeus (~> 1.3) - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) + google-protobuf (4.28.2-arm64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.28.2-x86_64-darwin) + bigdecimal + rake (>= 13) http_parser.rb (0.8.0) - i18n (0.9.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) - jekyll (3.9.2) + jekyll (4.3.4) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (~> 0.7) - jekyll-sass-converter (~> 1.0) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) jekyll-watch (~> 2.0) - kramdown (>= 1.17, < 3) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (~> 0.3.3) + mercenary (>= 0.3.6, < 0.5) pathutil (~> 0.9) - rouge (>= 1.7, < 4) + rouge (>= 3.0, < 5.0) safe_yaml (~> 1.0) - jekyll-avatar (0.7.0) - jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.1.1) - coffee-script (~> 2.2) - coffee-script-source (~> 1.11.1) - jekyll-commonmark (1.4.0) - commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.2.0) - commonmarker (~> 0.23.4) - jekyll (~> 3.9.0) - jekyll-commonmark (~> 1.4.0) - rouge (>= 2.0, < 4.0) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) jekyll-feed (0.15.1) jekyll (>= 3.7, < 5.0) - jekyll-gist (1.5.0) - octokit (~> 4.2) - jekyll-github-metadata (2.13.0) - jekyll (>= 3.4, < 5.0) - octokit (~> 4.0, != 4.4.0) - jekyll-include-cache (0.2.1) - jekyll (>= 3.7, < 5.0) - jekyll-mentions (1.6.0) - html-pipeline (~> 2.3) - jekyll (>= 3.7, < 5.0) - jekyll-optional-front-matter (0.3.2) - jekyll (>= 3.0, < 5.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.3.0) - jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.16.0) - jekyll (>= 3.3, < 5.0) - jekyll-relative-links (0.6.1) - jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.3) - addressable (~> 2.0) - jekyll (>= 3.5, < 5.0) - jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) - rubyzip (>= 1.3.0, < 3.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) jekyll-seo-tag (2.8.0) jekyll (>= 3.8, < 5.0) - jekyll-sitemap (1.4.0) - jekyll (>= 3.7, < 5.0) - jekyll-swiss (1.0.0) - jekyll-theme-architect (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-cayman (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-dinky (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-leap-day (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-merlot (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-midnight (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-minimal (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-modernist (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.6.0) - jekyll (> 3.5, < 5.0) - jekyll-github-metadata (~> 2.9) - jekyll-seo-tag (~> 2.0) - jekyll-theme-slate (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-tactile (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-time-machine (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.3) - jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.12.0) - gemoji (~> 3.0) - html-pipeline (~> 2.2) - jekyll (>= 3.0, < 5.0) - kramdown (2.3.2) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - liquid (4.0.3) - listen (3.7.1) + liquid (4.0.4) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - mercenary (0.3.6) + mercenary (0.4.0) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.16.3) - nokogiri (1.13.8-x86_64-darwin) - racc (~> 1.4) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (4.0.7) - racc (1.6.0) + public_suffix (6.0.1) + rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.2.5) - rouge (3.26.0) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rexml (3.3.8) + rouge (4.4.0) safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - simpleidn (0.2.1) - unf (~> 0.1.4) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - thread_safe (0.3.6) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (1.2.10) - thread_safe (~> 0.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (1.8.0) - webrick (1.7.0) - zeitwerk (2.6.1) + sass-embedded (1.79.4-arm64-darwin) + google-protobuf (~> 4.27) + sass-embedded (1.79.4-x86_64-darwin) + google-protobuf (~> 4.27) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.8.2) PLATFORMS + arm64-darwin-23 x86_64-darwin-21 x86_64-darwin-22 DEPENDENCIES - github-pages (~> 227) http_parser.rb (~> 0.6.0) - jekyll (~> 3.9.2) + jekyll (~> 4.2) jekyll-feed (~> 0.12) minima (~> 2.5) tzinfo (~> 1.2) tzinfo-data wdm (~> 0.1.1) - webrick (~> 1.7) + webrick (~> 1.8) BUNDLED WITH 2.4.19 diff --git a/docs/changeLog.html b/docs/changeLog.html index da1516ce..11c9c505 100644 --- a/docs/changeLog.html +++ b/docs/changeLog.html @@ -2,6 +2,17 @@ layout: page title: Change Log --- + +

Version 1.1.54

+

2024/10/10

+
  • + Issue 108. IMS Token expiration is not handled properly. The SDK will now call the refreshClient callback when the IMS token expires. +
  • +
  • + Update dependencies to fix potential vulnerabilities +
  • +
    +

    Version 1.1.53

    2024/09/06

  • @@ -43,7 +54,6 @@

    2024/07/07

  • -

    Version 1.1.48

    2024/06/11

  • diff --git a/src/campaign.js b/src/campaign.js index 211fb1a4..060b21a1 100644 --- a/src/campaign.js +++ b/src/campaign.js @@ -144,6 +144,32 @@ const { Util } = require("./util.js"); faultString = faultString.trim(); } + // https://github.com/adobe/acc-js-sdk/issues/108 + // 401 error code is hidden in error message and incorrectly reported as HTTP 500 error causing the refresh token + // callback not to be called + // Extract specific error code if possible + if (errorCode == "SOP-330007" && errorMessage && errorMessage.indexOf("XSV-350114") != -1 && errorMessage.indexOf(" 401") != -1) { + statusCode = 401; + } + // Because Campaign security zones may hide the previous error, fallback by trying to decode + // the JWT token and check if it is expired + else if (call && call._bearerToken) { + try { + const jwt = Util.decodeJwtToken(call._bearerToken); + if (jwt) { + const createdAt = +jwt.created_at; + const expiresIn = +jwt.expires_in; + const expiredAt = createdAt + expiresIn; + const now = Date.now(); + const hasExpired = now > expiredAt; + statusCode = hasExpired ? 401 : statusCode; + } + } catch(ex) { + // Invalid JWT token + statusCode = 401; + } + } + /** * The type of exception, always "CampaignException" * @type {string} diff --git a/src/client.js b/src/client.js index 8e3babcd..2f164160 100644 --- a/src/client.js +++ b/src/client.js @@ -1483,7 +1483,9 @@ class Client { this._trackEvent('SOAP//failure', event, ex, pushDownOptions); // Call session expiration callback in case of 401 if (ex.statusCode == 401 && that._refreshClient && soapCall.retry) { - return this._retrySoapCall(soapCall); + return this._retrySoapCall(soapCall).catch((ex) => { + return Promise.reject(ex); + }); } else return Promise.reject(ex); diff --git a/src/util.js b/src/util.js index 191224ec..1626e11b 100644 --- a/src/util.js +++ b/src/util.js @@ -159,6 +159,26 @@ class Util { return object; return Promise.resolve(object); } + + static decodeJwtToken(token) { + if (!token) + throw new Error("Invalid JWT token, null or empty string"); + const parts = token.split('.'); + if (parts.length !== 3) + throw new Error("Invalid JWT token, should be made of 3 parts"); + + try { + const base64Url = parts[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + + return JSON.parse(jsonPayload); + } catch(ex) { + throw new Error("Invalid JWT token, " + ex); + } + } } /** diff --git a/test/imBearerToken.test.js b/test/imBearerToken.test.js index a1f99a61..b61eacc2 100644 --- a/test/imBearerToken.test.js +++ b/test/imBearerToken.test.js @@ -119,6 +119,38 @@ describe('IMS Bearer Toekn', function () { expect(lastCall[0].headers["X-Session-Token"]).toBeUndefined(); }); + it("Expired session refresh client callback (", async () => { + + const refreshClient = async (client) => { + const connectionParameters = sdk.ConnectionParameters.ofImsBearerToken("http://acc-sdk:8080", "ey2...", options); + client.reinit(connectionParameters); + await client.NLWS.xtkSession.logon(); + return client; + }; + + const transport = jest.fn(); + const options = { + transport: transport, + refreshClient: refreshClient, + }; + const connectionParameters = sdk.ConnectionParameters.ofImsBearerToken("http://acc-sdk:8080", "ey1...", options); + const client = await sdk.init(connectionParameters); + await client.NLWS.xtkSession.logon(); + + client._transport.mockReturnValueOnce(Promise.resolve(`XSV-350008 Session has expired or is invalid. Please reconnect.`)); + client._transport.mockReturnValueOnce(Mock.GET_XTK_SESSION_SCHEMA_RESPONSE); + client._transport.mockReturnValueOnce(Mock.GET_DATABASEID_RESPONSE); + var databaseId = await client.getOption("XtkDatabaseId"); + expect(databaseId).toBe("uFE80000000000000F1FA913DD7CC7C480041161C"); + const lastCall = client._transport.mock.calls[client._transport.mock.calls.length - 1]; + expect(lastCall[0].headers).toMatchObject({ + "ACC-SDK-Auth": "ImsBearerToken", + "Authorization": "Bearer ey2..." + }); + expect(lastCall[0].headers["X-Security-Token"]).toBeUndefined(); + expect(lastCall[0].headers["X-Session-Token"]).toBeUndefined(); + }); + it("Should call ping API", async () => { const client = await makeImsClient(); diff --git a/test/util.test.js b/test/util.test.js index eca2327b..e1de66c2 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -446,5 +446,36 @@ describe('Util', function() { await expect(Util.asPromise(Promise.resolve(3))).resolves.toBe(3); }); }); + + describe("Decode JWT token", () => { + it("Should decode valid expired token", () => { + const jsonObject = { "hello": "world" }; + const jsonString = JSON.stringify(jsonObject); + const base64String = btoa(jsonString); + const token = "ABC." + base64String + ".XYZ"; + expect(Util.decodeJwtToken(token)).toEqual(jsonObject); + }); + + it("Should not decode partial token (2 parts instead of 3)", () => { + const token = "ABCD.EFGH"; + expect(() => {Util.decodeJwtToken(token)}).toThrow("Invalid JWT token"); + }); + + it("Should not null or empty token", () => { + let token = ""; + expect(() => {Util.decodeJwtToken(token)}).toThrow("Invalid JWT token"); + token = null; + expect(() => {Util.decodeJwtToken(token)}).toThrow("Invalid JWT token"); + token = undefined; + expect(() => {Util.decodeJwtToken(token)}).toThrow("Invalid JWT token"); + }); + + it("Should not invalid token", () => { + const token = "Hel#lo.~!@#.!@$%$#"; + expect(() => {Util.decodeJwtToken(token)}).toThrow("Invalid JWT token"); + }); + + }); + });