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");
+ });
+
+ });
+
});