This is an automated email from the ASF dual-hosted git repository. glynnbird pushed a commit to branch fetch in repository https://gitbox.apache.org/repos/asf/couchdb-nano.git
commit fffb593c49fa4531c5eff46a7c10cba758c57e4e Author: Glynn Bird <[email protected]> AuthorDate: Mon Dec 12 14:03:45 2022 +0000 fix cookie renewal bug --- lib/cookiejar.js | 45 +++++++++++++++++++++++++++++++++++++++ lib/nano.js | 3 +-- test/nano.auth.test.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/lib/cookiejar.js b/lib/cookiejar.js new file mode 100644 index 0000000..270a669 --- /dev/null +++ b/lib/cookiejar.js @@ -0,0 +1,45 @@ +const tough = require('tough-cookie') +const cookieJar = new tough.CookieJar() + +// this is a monkey-patch of toughcookie's cookiejar, as it doesn't handle +// the refreshing of cookies from CouchDB properly +// see https://github.com/salesforce/tough-cookie/issues/154 +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) + } +} +module.exports = { + tough, + cookieJar +} diff --git a/lib/nano.js b/lib/nano.js index 1608cbd..7fc46c6 100644 --- a/lib/nano.js +++ b/lib/nano.js @@ -14,8 +14,7 @@ const { URL } = require('url') const { Readable } = require('node:stream') const assert = require('assert') -const tough = require('tough-cookie') -const cookieJar = new tough.CookieJar() +const { tough, cookieJar } = require('./cookiejar.js') const stream = require('stream') const pkg = require('../package.json') const undici = require('undici') diff --git a/test/nano.auth.test.js b/test/nano.auth.test.js index 817b232..8a24acb 100644 --- a/test/nano.auth.test.js +++ b/test/nano.auth.test.js @@ -54,3 +54,60 @@ test('should be able to authenticate - POST /_session - nano.auth', async () => assert.deepEqual(q, ['a']) mockAgent.assertNoPendingInterceptors() }) + +test('should be able to handle cookie refresh - POST /_session - nano.auth', async () => { + // mocks + const username = 'u' + const password = 'p' + const response = { ok: true, name: 'admin', roles: ['_admin', 'admin'] } + const c1 = 'AuthSession=YWRtaW46NUU0MTFBMDE6stHsxYnlDy4mYxwZEcnXHn4fm5w' + const cookie1 = `${c1}; Version=1; Expires=Mon, 10-Feb-2050 09:03:21 GMT; Max-Age=600; Path=/; HttpOnly` + const c2 = 'AuthSession=DE6stHsxYnlDy4YWRtaW46NUU0MTFBMmYxwZEcnXHn4fm5w' + const cookie2 = `${c2}; Version=1; Expires=Mon, 10-Feb-2050 09:05:21 GMT; Max-Age=600; Path=/; HttpOnly` + mockPool + .intercept({ + method: 'post', + path: '/_session', + body: 'name=u&password=p', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8' + } + }) + .reply(200, response, { + headers: { + 'content-type': 'application/json', + 'Set-Cookie': cookie1 + } + }) + mockPool + .intercept({ + path: '/_all_dbs', + headers: { + cookie: c1 + } + }) + .reply(200, ['a'], { + headers: { + 'content-type': 'application/json', + 'Set-Cookie': cookie2 + } + }) + mockPool + .intercept({ + path: '/_all_dbs', + headers: { + cookie: c2 + } + }) + .reply(200, ['a'], JSON_HEADERS) + + // test POST /_session + const p1 = await nano.auth(username, password) + assert.deepEqual(p1, response) + const p2 = await nano.db.list() + assert.deepEqual(p2, ['a']) + const p3 = await nano.db.list() + assert.deepEqual(p3, ['a']) + mockAgent.assertNoPendingInterceptors() +}) +
