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()
+})
+

Reply via email to