diff --git a/CHANGES.md b/CHANGES.md index eb09fe14..ebbd1a57 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +# Unreleased +- [FIXED] Issue where new session cookies from pre-emptive renewal would not persist beyond the original session + lifetime. + # 4.5.0 (2021-08-26) - [IMPROVED] - Document IDs and attachment names are now rejected if they could cause an unexpected Cloudant request. We have seen that some applications pass unsantized document IDs to SDK functions diff --git a/lib/tokens/TokenManager.js b/lib/tokens/TokenManager.js index c3aa3649..2d399eb5 100644 --- a/lib/tokens/TokenManager.js +++ b/lib/tokens/TokenManager.js @@ -27,6 +27,50 @@ class TokenManager { this._isTokenRenewing = false; this._tokenExchangeEE = new EventEmitter().setMaxListeners(Infinity); + + // START monkey patch for https://github.com/salesforce/tough-cookie/issues/154 + // Use the tough-cookie CookieJar from the RequestJar + const cookieJar = this._jar ? this._jar._jar : false; + // Check if we've already patched the jar + if (cookieJar && !cookieJar.cloudantPatch) { + // Set the patching flag + cookieJar.cloudantPatch = true; + // Replace the store's updateCookie function with one that applies a patch to newCookie + const originalUpdateCookieFn = cookieJar.store.updateCookie; + cookieJar.store.updateCookie = function(oldCookie, newCookie, cb) { + // Add current time as an update timestamp to the newCookie + newCookie.cloudantPatchUpdateTime = new Date(); + // Replace the cookie's expiryTime function with one that uses cloudantPatchUpdateTime + // in place of creation time to check the expiry. + const originalExpiryTimeFn = newCookie.expiryTime; + newCookie.expiryTime = function(now) { + // The original expiryTime check is relative to a time in this order: + // 1. supplied now argument + // 2. this.creation (original cookie creation time) + // 3. current time + // This patch replaces 2 with an expiry check relative to the cloudantPatchUpdateTime if set instead of + // the creation time by passing it as the now argument. + return originalExpiryTimeFn.call( + newCookie, + newCookie.cloudantPatchUpdateTime || now + ); + }; + // Finally delegate back to the original update function or the fallback put (which is set by Cookie + // when an update function is not present on the store). Since we always set an update function for our + // patch we need to also provide that fallback. + if (originalUpdateCookieFn) { + originalUpdateCookieFn.call( + cookieJar.store, + oldCookie, + newCookie, + cb + ); + } else { + cookieJar.store.putCookie(newCookie, cb); + } + }; + } + // END cookie jar monkey patch } _autoRenew(defaultMaxAgeSecs) { diff --git a/test/plugins/cookieauth.js b/test/plugins/cookieauth.js index d73510e5..954a495b 100644 --- a/test/plugins/cookieauth.js +++ b/test/plugins/cookieauth.js @@ -17,6 +17,7 @@ const assert = require('assert'); const Client = require('../../lib/client.js'); +const Cloudant = require('../../cloudant.js'); const nock = require('../nock.js'); const uuidv4 = require('uuid/v4'); // random @@ -31,10 +32,10 @@ const COOKIEAUTH_PLUGIN = [ { cookieauth: { autoRenew: false } } ]; // mock cookies const MOCK_COOKIE = 'AuthSession=Y2xbZWr0bQlpcc19ZQN8OeU4OWFCNYcZOxgdhy-QRDp4i6JQrfkForX5OU5P'; -const MOCK_SET_COOKIE_HEADER = { 'set-cookie': `${MOCK_COOKIE}; Version=1; Max-Age=86400; Path=/; HttpOnly` }; +const MOCK_SET_COOKIE_HEADER = { 'set-cookie': `${MOCK_COOKIE}; Version=1; Max-Age=1; Path=/; HttpOnly` }; const MOCK_COOKIE_2 = 'AuthSession=Q2fbIWc0kQspdc39OQL89eS4PWECcYEZDxgdgy-0RCp2i0dcrDkfoWX7OI5A'; -const MOCK_SET_COOKIE_HEADER_2 = { 'set-cookie': `${MOCK_COOKIE_2}; Version=1; Max-Age=86400; Path=/; HttpOnly` }; +const MOCK_SET_COOKIE_HEADER_2 = { 'set-cookie': `${MOCK_COOKIE_2}; Version=1; Max-Age=1; Path=/; HttpOnly` }; describe('#db CookieAuth Plugin', function() { before(function(done) { @@ -358,5 +359,44 @@ describe('#db CookieAuth Plugin', function() { done(); }); }); + + it('pre-emptive renewal outlasts original session', function() { + if (process.env.NOCK_OFF) { + this.skip(); + } + + var mocks = nock(SERVER) + .post('/_session', {name: ME, password: PASSWORD}) + .reply(200, {ok: true}, MOCK_SET_COOKIE_HEADER) + .get(DBNAME) + .matchHeader('cookie', MOCK_COOKIE) + .reply(200, {doc_count: 0}) + // pre-emptive renewals every 500 ms + .post('/_session', {name: ME, password: PASSWORD}) + .times(2) + .reply(200, {ok: true}, MOCK_SET_COOKIE_HEADER_2) + .get(DBNAME) + .matchHeader('cookie', MOCK_COOKIE_2) + .reply(200, {doc_count: 0}); + + var cloudantClient = new Cloudant({ + url: SERVER, + username: ME, + password: PASSWORD, + maxAttempt: 1 + // Note using default cookieauth plugin + }); + return cloudantClient.db.get(DBNAME.substring(1)) /* Remove leading slash */ + .then(() => { + // Wait long enough for a pre-emptive renewal and orignal session to lapse + return new Promise(resolve => setTimeout(resolve, 1000)); + }) + .then(() => { + return cloudantClient.db.get(DBNAME.substring(1)); + }) + .finally(() => { + mocks.done(); + }); + }); }); });