Skip to content
This repository has been archived by the owner on Mar 11, 2022. It is now read-only.

Cookie persistence workaround #464

Merged
merged 1 commit into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
44 changes: 44 additions & 0 deletions lib/tokens/TokenManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean:

Suggested change
// This patch replaces 2 with an expiry check relative to the cloudantPatchUpdateTime if set instead of
// This patch replaces 2 with an expiry check relative to the cloudantPatchUpdateTime instead of

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it only compares it to the cloudantPatchUpdateTime if it is set.

// 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) {
Expand Down
44 changes: 42 additions & 2 deletions test/plugins/cookieauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) {
Expand Down Expand Up @@ -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();
});
});
});
});