Repository: cloudant/python-cloudant

Issue:
Add Basic authentication support to setting up a client session
See #49 


Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index c683e44..d429d65 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,7 @@
 Unreleased
 ==========
+- [NEW] Added API for upcoming Bluemix Identity and Access Management support for Cloudant on Bluemix. Note: IAM API key support is not yet enabled in the service.
+- [NEW] Added HTTP basic authentication support.
 - [NEW] Added ``Result.all()`` convenience method.
 - [NEW] Allow ``service_name`` to be specified when instantiating from a Bluemix VCAP_SERVICES environment variable.
 - [IMPROVED] Updated ``posixpath.join`` references to use ``'/'.join`` when concatenating URL parts.
diff --git a/Jenkinsfile b/Jenkinsfile
index b89cb5c..2bc5d88 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,26 +1,48 @@
-// Define the test routine for different python versions
-def test_python(pythonVersion)
-{
+def getEnvForSuite(suiteName) {
+  // Base environment variables
+  def envVars = [
+    "CLOUDANT_ACCOUNT=$DB_USER",
+    "RUN_CLOUDANT_TESTS=1",
+    "SKIP_DB_UPDATES=1" // Disable pending resolution of case 71610
+  ]
+  // Add test suite specific environment variables
+  switch(suiteName) {
+    case 'basic':
+      envVars.add("RUN_BASIC_AUTH_TESTS=1")
+      break
+    case 'cookie':
+      break
+    case 'iam':
+      // Setting IAM_API_KEY forces tests to run using an IAM enabled client.
+      envVars.add("IAM_API_KEY=$DB_IAM_API_KEY")
+      break
+    default:
+      error("Unknown test suite environment ${suiteName}")
+  }
+  return envVars
+}
+
+def setupPythonAndTest(pythonVersion, testSuite) {
   node {
     // Unstash the source on this node
     unstash name: 'source'
     // Set up the environment and test
-    withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'clientlibs-test', usernameVariable: 'DB_USER', passwordVariable: 'DB_PASSWORD']]) {
-      try {
-        sh """  virtualenv tmp -p /usr/local/lib/python${pythonVersion}/bin/${pythonVersion.startsWith('3') ? "python3" : "python"}
-                . ./tmp/bin/activate
-                echo \$DB_USER
-                export RUN_CLOUDANT_TESTS=1
-                export CLOUDANT_ACCOUNT=\$DB_USER
-                # Temporarily disable the _db_updates tests pending resolution of case 71610
-                export SKIP_DB_UPDATES=1
-                pip install -r requirements.txt
-                pip install -r test-requirements.txt
-                pylint ./src/cloudant
-                nosetests -w ./tests/unit --with-xunit"""
-      } finally {
-        // Load the test results
-        junit 'nosetests.xml'
+    withCredentials([usernamePassword(credentialsId: 'clientlibs-test', usernameVariable: 'DB_USER', passwordVariable: 'DB_PASSWORD'),
+                     string(credentialsId: 'clientlibs-test-iam', variable: 'DB_IAM_API_KEY')]) {
+      withEnv(getEnvForSuite("${testSuite}")) {
+        try {
+          sh """
+            virtualenv tmp -p /usr/local/lib/python${pythonVersion}/bin/${pythonVersion.startsWith('3') ? "python3" : "python"}
+            . ./tmp/bin/activate
+            pip install -r requirements.txt
+            pip install -r test-requirements.txt
+            pylint ./src/cloudant
+            nosetests -w ./tests/unit --with-xunit
+          """
+        } finally {
+          // Load the test results
+          junit 'nosetests.xml'
+        }
       }
     }
   }
@@ -34,10 +56,14 @@ stage('Checkout'){
     stash name: 'source'
   }
 }
+
 stage('Test'){
-  // Run tests in parallel for multiple python versions
   parallel(
-    Python2: {test_python('2.7.12')},
-    Python3: {test_python('3.5.2')}
+    'Python2-BASIC':  { setupPythonAndTest('2.7.12', 'basic')  },
+    'Python3-BASIC':  { setupPythonAndTest('3.5.2',  'basic')  },
+    'Python2-COOKIE': { setupPythonAndTest('2.7.12', 'cookie') },
+    'Python3-COOKIE': { setupPythonAndTest('3.5.2',  'cookie') },
+    'Python2-IAM':    { setupPythonAndTest('2.7.12', 'iam')    },
+    'Python3-IAM':    { setupPythonAndTest('3.5.2',  'iam')    }
   )
 }
diff --git a/src/cloudant/_client_session.py b/src/cloudant/_client_session.py
new file mode 100644
index 0000000..289b131
--- /dev/null
+++ b/src/cloudant/_client_session.py
@@ -0,0 +1,285 @@
+#!/usr/bin/env python
+# Copyright (c) 2015, 2017 IBM Corp. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Module containing client session classes.
+"""
+import base64
+import json
+import os
+
+from requests import RequestException, Session
+
+from ._2to3 import bytes_, unicode_, url_join
+from .error import CloudantException
+
+
+class ClientSession(Session):
+    """
+    This class extends Session and provides a default timeout.
+    """
+
+    def __init__(self, username=None, password=None, session_url=None, **kwargs):
+        super(ClientSession, self).__init__()
+
+        self._username = username
+        self._password = password
+        self._session_url = session_url
+
+        self._auto_renew = kwargs.get('auto_renew', False)
+        self._timeout = kwargs.get('timeout', None)
+
+    def base64_user_pass(self):
+        """
+        Composes a basic http auth string, suitable for use with the
+        _replicator database, and other places that need it.
+
+        :returns: Basic http authentication string
+        """
+        if self._username is None or self._password is None:
+            return None
+
+        hash_ = base64.urlsafe_b64encode(bytes_("{username}:{password}".format(
+            username=self._username,
+            password=self._password
+        )))
+        return "Basic {0}".format(unicode_(hash_))
+
+    # pylint: disable=arguments-differ
+    def request(self, method, url, **kwargs):
+        """
+        Overrides ``requests.Session.request`` to set the timeout.
+        """
+        resp = super(ClientSession, self).request(
+            method, url, timeout=self._timeout, **kwargs)
+
+        return resp
+
+    def info(self):
+        """
+        Get session information.
+        """
+        if self._session_url is None:
+            return None
+
+        resp = self.get(self._session_url)
+        resp.raise_for_status()
+        return resp.json()
+
+    def set_credentials(self, username, password):
+        """
+        Set a new username and password.
+
+        :param str username: New username.
+        :param str password: New password.
+        """
+        if username is not None:
+            self._username = username
+
+        if password is not None:
+            self._password = password
+
+    def login(self):
+        """
+        No-op method - not implemented here.
+        """
+        pass
+
+    def logout(self):
+        """
+        No-op method - not implemented here.
+        """
+        pass
+
+
+class BasicSession(ClientSession):
+    """
+    This class extends ClientSession to provide basic access authentication.
+    """
+
+    def __init__(self, username, password, server_url, **kwargs):
+        super(BasicSession, self).__init__(
+            username=username,
+            password=password,
+            session_url=url_join(server_url, '_session'),
+            **kwargs)
+
+    def request(self, method, url, **kwargs):
+        """
+        Overrides ``requests.Session.request`` to provide basic access
+        authentication.
+        """
+        auth = None
+        if self._username is not None and self._password is not None:
+            auth = (self._username, self._password)
+
+        return super(BasicSession, self).request(
+            method, url, auth=auth, **kwargs)
+
+
+class CookieSession(ClientSession):
+    """
+    This class extends ClientSession and provides cookie authentication.
+    """
+
+    def __init__(self, username, password, server_url, **kwargs):
+        super(CookieSession, self).__init__(
+            username=username,
+            password=password,
+            session_url=url_join(server_url, '_session'),
+            **kwargs)
+
+    def login(self):
+        """
+        Perform cookie based user login.
+        """
+        resp = super(CookieSession, self).request(
+            'POST',
+            self._session_url,
+            data={'name': self._username, 'password': self._password},
+        )
+        resp.raise_for_status()
+
+    def logout(self):
+        """
+        Logout cookie based user.
+        """
+        resp = super(CookieSession, self).request('DELETE', self._session_url)
+        resp.raise_for_status()
+
+    def request(self, method, url, **kwargs):
+        """
+        Overrides ``requests.Session.request`` to renew the cookie and then
+        retry the original request (if required).
+        """
+        resp = super(CookieSession, self).request(method, url, **kwargs)
+
+        if not self._auto_renew:
+            return resp
+
+        is_expired = any((
+            resp.status_code == 403 and
+            resp.json().get('error') == 'credentials_expired',
+            resp.status_code == 401
+        ))
+
+        if is_expired:
+            self.login()
+            resp = super(CookieSession, self).request(method, url, **kwargs)
+
+        return resp
+
+
+class IAMSession(ClientSession):
+    """
+    This class extends ClientSession and provides IAM authentication.
+    """
+
+    def __init__(self, api_key, server_url, **kwargs):
+        super(IAMSession, self).__init__(
+            session_url=url_join(server_url, '_iam_session'),
+            **kwargs)
+
+        self._api_key = api_key
+        self._token_url = os.environ.get(
+            'IAM_TOKEN_URL', 'https://iam.bluemix.net/oidc/token')
+
+    def login(self):
+        """
+        Perform IAM cookie based user login.
+        """
+        access_token = self._get_access_token()
+        try:
+            super(IAMSession, self).request(
+                'POST',
+                self._session_url,
+                headers={'Content-Type': 'application/json'},
+                data=json.dumps({'access_token': access_token})
+            ).raise_for_status()
+
+        except RequestException:
+            raise CloudantException(
+                'Failed to exchange IAM token with Cloudant')
+
+    def logout(self):
+        """
+        Logout IAM cookie based user.
+        """
+        self.cookies.clear()
+
+    def request(self, method, url, **kwargs):
+        """
+        Overrides ``requests.Session.request`` to renew the IAM cookie
+        and then retry the original request (if required).
+        """
+        # The CookieJar API prevents callers from getting an individual Cookie
+        # object by name.
+        # We are forced to use the only exposed method of discarding expired
+        # cookies from the CookieJar. Internally this involves iterating over
+        # the entire CookieJar and calling `.is_expired()` on each Cookie
+        # object.
+        self.cookies.clear_expired_cookies()
+
+        if self._auto_renew and 'IAMSession' not in self.cookies.keys():
+            self.login()
+
+        resp = super(IAMSession, self).request(method, url, **kwargs)
+
+        if not self._auto_renew:
+            return resp
+
+        if resp.status_code == 401:
+            self.login()
+            resp = super(IAMSession, self).request(method, url, **kwargs)
+
+        return resp
+
+    # pylint: disable=arguments-differ, unused-argument
+    def set_credentials(self, username, api_key):
+        """
+        Set a new IAM API key.
+
+        :param str username: Username parameter is unused.
+        :param str api_key: New IAM API key.
+        """
+        if api_key is not None:
+            self._api_key = api_key
+
+    def _get_access_token(self):
+        """
+        Get IAM access token using API key.
+        """
+        err = 'Failed to contact IAM token service'
+        try:
+            resp = super(IAMSession, self).request(
+                'POST',
+                self._token_url,
+                auth=('bx', 'bx'),  # required for user API keys
+                headers={'Accepts': 'application/json'},
+                data={
+                    'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
+                    'response_type': 'cloud_iam',
+                    'apikey': self._api_key
+                }
+            )
+            err = resp.json().get('errorMessage', err)
+            resp.raise_for_status()
+
+            return resp.json()['access_token']
+
+        except KeyError:
+            raise CloudantException('Invalid response from IAM token service')
+
+        except RequestException:
+            raise CloudantException(err)
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index 05e3bd3..e44df3d 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -17,14 +17,12 @@ Module containing miscellaneous classes, functions, and constants used
 throughout the library.
 """
 
-import os
 import sys
 import platform
 from collections import Sequence
 import json
-from requests import RequestException, Session
 
-from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_, url_join
+from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_
 from .error import CloudantArgumentError, CloudantException
 
 # Library Constants
@@ -290,211 +288,6 @@ class _Code(str):
         return str.__new__(cls, code)
 
 
-class ClientSession(Session):
-    """
-    This class extends Session and provides a default timeout.
-    """
-
-    def __init__(self, **kwargs):
-        super(ClientSession, self).__init__()
-        self._timeout = kwargs.get('timeout', None)
-
-    def request(self, method, url, **kwargs):  # pylint: disable=W0221
-        """
-        Overrides ``requests.Session.request`` to set the timeout.
-        """
-        resp = super(ClientSession, self).request(
-            method, url, timeout=self._timeout, **kwargs)
-
-        return resp
-
-
-class CookieSession(ClientSession):
-    """
-    This class extends ClientSession and provides cookie authentication.
-    """
-
-    def __init__(self, username, password, server_url, **kwargs):
-        super(CookieSession, self).__init__(**kwargs)
-        self._username = username
-        self._password = password
-        self._auto_renew = kwargs.get('auto_renew', False)
-        self._session_url = url_join(server_url, '_session')
-
-    def info(self):
-        """
-        Get cookie based login user information.
-        """
-        resp = self.get(self._session_url)
-        resp.raise_for_status()
-
-        return resp.json()
-
-    def login(self):
-        """
-        Perform cookie based user login.
-        """
-        resp = super(CookieSession, self).request(
-            'POST',
-            self._session_url,
-            data={'name': self._username, 'password': self._password},
-        )
-        resp.raise_for_status()
-
-    def logout(self):
-        """
-        Logout cookie based user.
-        """
-        resp = super(CookieSession, self).request('DELETE', self._session_url)
-        resp.raise_for_status()
-
-    def request(self, method, url, **kwargs):  # pylint: disable=W0221
-        """
-        Overrides ``requests.Session.request`` to renew the cookie and then
-        retry the original request (if required).
-        """
-        resp = super(CookieSession, self).request(method, url, **kwargs)
-
-        if not self._auto_renew:
-            return resp
-
-        is_expired = any((
-            resp.status_code == 403 and
-            resp.json().get('error') == 'credentials_expired',
-            resp.status_code == 401
-        ))
-
-        if is_expired:
-            self.login()
-            resp = super(CookieSession, self).request(method, url, **kwargs)
-
-        return resp
-
-    def set_credentials(self, username, password):
-        """
-        Set a new username and password.
-
-        :param str username: New username.
-        :param str password: New password.
-        """
-        if username is not None:
-            self._username = username
-
-        if password is not None:
-            self._password = password
-
-
-class IAMSession(ClientSession):
-    """
-    This class extends ClientSession and provides IAM authentication.
-    """
-
-    def __init__(self, api_key, server_url, **kwargs):
-        super(IAMSession, self).__init__(**kwargs)
-        self._api_key = api_key
-        self._auto_renew = kwargs.get('auto_renew', False)
-        self._session_url = url_join(server_url, '_iam_session')
-        self._token_url = os.environ.get(
-            'IAM_TOKEN_URL', 'https://iam.bluemix.net/oidc/token')
-
-    def info(self):
-        """
-        Get IAM cookie based login user information.
-        """
-        resp = self.get(self._session_url)
-        resp.raise_for_status()
-
-        return resp.json()
-
-    def login(self):
-        """
-        Perform IAM cookie based user login.
-        """
-        access_token = self._get_access_token()
-        try:
-            super(IAMSession, self).request(
-                'POST',
-                self._session_url,
-                headers={'Content-Type': 'application/json'},
-                data=json.dumps({'access_token': access_token})
-            ).raise_for_status()
-
-        except RequestException:
-            raise CloudantException(
-                'Failed to exchange IAM token with Cloudant')
-
-    def logout(self):
-        """
-        Logout IAM cookie based user.
-        """
-        self.cookies.clear()
-
-    def request(self, method, url, **kwargs):  # pylint: disable=W0221
-        """
-        Overrides ``requests.Session.request`` to renew the IAM cookie
-        and then retry the original request (if required).
-        """
-        # The CookieJar API prevents callers from getting an individual Cookie
-        # object by name.
-        # We are forced to use the only exposed method of discarding expired
-        # cookies from the CookieJar. Internally this involves iterating over
-        # the entire CookieJar and calling `.is_expired()` on each Cookie
-        # object.
-        self.cookies.clear_expired_cookies()
-
-        if self._auto_renew and 'IAMSession' not in self.cookies.keys():
-            self.login()
-
-        resp = super(IAMSession, self).request(method, url, **kwargs)
-
-        if not self._auto_renew:
-            return resp
-
-        if resp.status_code == 401:
-            self.login()
-            resp = super(IAMSession, self).request(method, url, **kwargs)
-
-        return resp
-
-    def set_credentials(self, username, api_key):
-        """
-        Set a new IAM API key.
-
-        :param str username: Username parameter is unused.
-        :param str api_key: New IAM API key.
-        """
-        if api_key is not None:
-            self._api_key = api_key
-
-    def _get_access_token(self):
-        """
-        Get IAM access token using API key.
-        """
-        err = 'Failed to contact IAM token service'
-        try:
-            resp = super(IAMSession, self).request(
-                'POST',
-                self._token_url,
-                auth=('bx', 'bx'),  # required for user API keys
-                headers={'Accepts': 'application/json'},
-                data={
-                    'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
-                    'response_type': 'cloud_iam',
-                    'apikey': self._api_key
-                }
-            )
-            err = resp.json().get('errorMessage', err)
-            resp.raise_for_status()
-
-            return resp.json()['access_token']
-
-        except KeyError:
-            raise CloudantException('Invalid response from IAM token service')
-
-        except RequestException:
-            raise CloudantException(err)
-
-
 class CloudFoundryService(object):
     """ Manages Cloud Foundry service configuration. """
 
diff --git a/src/cloudant/client.py b/src/cloudant/client.py
index ce7d493..abf3f5f 100755
--- a/src/cloudant/client.py
+++ b/src/cloudant/client.py
@@ -16,10 +16,14 @@
 Top level API module that maps to a Cloudant or CouchDB client connection
 instance.
 """
-import base64
 import json
 
-from ._2to3 import bytes_, unicode_
+from ._client_session import (
+    BasicSession,
+    ClientSession,
+    CookieSession,
+    IAMSession
+)
 from .database import CloudantDatabase, CouchDatabase
 from .feed import Feed, InfiniteFeed
 from .error import (
@@ -29,10 +33,9 @@ from .error import (
 from ._common_util import (
     USER_AGENT,
     append_response_error_content,
-    ClientSession,
     CloudFoundryService,
-    CookieSession,
-    IAMSession)
+    )
+
 
 class CouchDB(dict):
     """
@@ -67,6 +70,8 @@ class CouchDB(dict):
         `Requests library timeout argument
         <http://docs.python-requests.org/en/master/user/quickstart/#timeouts>`_.
         but will apply to every request made using this client.
+    :param bool use_basic_auth: Keyword argument, if set to True performs basic
+        access authentication with server. Default is False.
     :param bool use_iam: Keyword argument, if set to True performs
         IAM authentication with server. Default is False.
         Use :func:`~cloudant.client.CouchDB.iam` to construct an IAM
@@ -86,7 +91,9 @@ class CouchDB(dict):
         self._timeout = kwargs.get('timeout', None)
         self.r_session = None
         self._auto_renew = kwargs.get('auto_renew', False)
+        self._use_basic_auth = kwargs.get('use_basic_auth', False)
         self._use_iam = kwargs.get('use_iam', False)
+
         connect_to_couch = kwargs.get('connect', False)
         if connect_to_couch and self._DATABASE_CLASS == CouchDatabase:
             self.connect()
@@ -100,7 +107,16 @@ class CouchDB(dict):
             self.session_logout()
 
         if self.admin_party:
-            self.r_session = ClientSession(timeout=self._timeout)
+            self.r_session = ClientSession(
+                timeout=self._timeout
+            )
+        elif self._use_basic_auth:
+            self.r_session = BasicSession(
+                self._user,
+                self._auth_token,
+                self.server_url,
+                timeout=self._timeout
+            )
         elif self._use_iam:
             self.r_session = IAMSession(
                 self._auth_token,
@@ -146,9 +162,6 @@ class CouchDB(dict):
 
         :returns: Dictionary of session info for the current session.
         """
-        if self.admin_party:
-            return None
-
         return self.r_session.info()
 
     def session_cookie(self):
@@ -157,19 +170,26 @@ class CouchDB(dict):
 
         :returns: Session cookie for the current session
         """
-        if self.admin_party:
-            return None
         return self.r_session.cookies.get('AuthSession')
 
     def session_login(self, user=None, passwd=None):
         """
         Performs a session login by posting the auth information
         to the _session endpoint.
+
+        :param str user: Username used to connect to server.
+        :param str auth_token: Authentication token used to connect to server.
         """
-        if self.admin_party:
-            return
+        self.change_credentials(user=user, auth_token=passwd)
 
-        self.r_session.set_credentials(user, passwd)
+    def change_credentials(self, user=None, auth_token=None):
+        """
+        Change login credentials.
+
+        :param str user: Username used to connect to server.
+        :param str auth_token: Authentication token used to connect to server.
+        """
+        self.r_session.set_credentials(user, auth_token)
         self.r_session.login()
 
     def session_logout(self):
@@ -177,9 +197,6 @@ class CouchDB(dict):
         Performs a session logout and clears the current session by
         sending a delete request to the _session endpoint.
         """
-        if self.admin_party:
-            return
-
         self.r_session.logout()
 
     def basic_auth_str(self):
@@ -189,13 +206,7 @@ class CouchDB(dict):
 
         :returns: Basic http authentication string
         """
-        if self.admin_party:
-            return None
-        hash_ = base64.urlsafe_b64encode(bytes_("{username}:{password}".format(
-            username=self._user,
-            password=self._auth_token
-        )))
-        return "Basic {0}".format(unicode_(hash_))
+        return self.r_session.base64_user_pass()
 
     def all_dbs(self):
         """
diff --git a/src/cloudant/database.py b/src/cloudant/database.py
index 1992d61..cf53f6f 100644
--- a/src/cloudant/database.py
+++ b/src/cloudant/database.py
@@ -94,11 +94,13 @@ class CouchDatabase(dict):
 
         :returns: Dictionary containing authentication information
         """
-        if self.admin_party:
+        session = self.client.session()
+        if session is None:
             return None
+
         return {
             "basic_auth": self.client.basic_auth_str(),
-            "user_ctx": self.client.session()['userCtx']
+            "user_ctx": session.get('userCtx')
         }
 
     def exists(self):


Test patch:
diff --git a/tests/unit/auth_renewal_tests.py b/tests/unit/auth_renewal_tests.py
index 3d9b7cc..b5fb48b 100644
--- a/tests/unit/auth_renewal_tests.py
+++ b/tests/unit/auth_renewal_tests.py
@@ -23,9 +23,9 @@ import os
 import requests
 import time
 
-from cloudant._common_util import CookieSession
+from cloudant._client_session import CookieSession
 
-from .unit_t_db_base import UnitTestDbBase
+from .unit_t_db_base import skip_if_not_cookie_auth, UnitTestDbBase
 
 @unittest.skipIf(os.environ.get('ADMIN_PARTY') == 'true', 'Skipping - Admin Party mode')
 class AuthRenewalTests(UnitTestDbBase):
@@ -44,7 +44,8 @@ class AuthRenewalTests(UnitTestDbBase):
         Override UnitTestDbBase.tearDown() with no tear down
         """
         pass
-        
+
+    @skip_if_not_cookie_auth
     def test_client_db_doc_stack_success(self):
         """
         Ensure that auto renewal of cookie auth happens as expected and applies
@@ -109,6 +110,7 @@ class AuthRenewalTests(UnitTestDbBase):
             self.client.disconnect()
             del self.client
 
+    @skip_if_not_cookie_auth
     def test_client_db_doc_stack_failure(self):
         """
         Ensure that when the regular requests.Session is used that
diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py
index db78861..114a9e3 100644
--- a/tests/unit/client_tests.py
+++ b/tests/unit/client_tests.py
@@ -27,17 +27,19 @@ import base64
 import sys
 import os
 import datetime
+import mock
 
 from requests import ConnectTimeout, HTTPError
 from time import sleep
 
 from cloudant import cloudant, cloudant_bluemix, couchdb, couchdb_admin_party
 from cloudant.client import Cloudant, CouchDB
+from cloudant._client_session import BasicSession, CookieSession
+from cloudant.database import CloudantDatabase
 from cloudant.error import CloudantArgumentError, CloudantClientException
 from cloudant.feed import Feed, InfiniteFeed
-from cloudant._common_util import CookieSession
 
-from .unit_t_db_base import UnitTestDbBase
+from .unit_t_db_base import skip_if_not_cookie_auth, UnitTestDbBase
 from .. import bytes_, str_
 
 class CloudantClientExceptionTests(unittest.TestCase):
@@ -162,6 +164,7 @@ class ClientTests(UnitTestDbBase):
             self.client.disconnect()
             self.assertIsNone(self.client.r_session)
 
+    @skip_if_not_cookie_auth
     def test_auto_renew_enabled(self):
         """
         Test that CookieSession is used when auto_renew is enabled.
@@ -176,6 +179,7 @@ class ClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_auto_renew_enabled_with_auto_connect(self):
         """
         Test that CookieSession is used when auto_renew is enabled along with
@@ -190,6 +194,7 @@ class ClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_session(self):
         """
         Test getting session information.  
@@ -205,6 +210,7 @@ class ClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_session_cookie(self):
         """
         Test getting the session cookie.
@@ -219,6 +225,101 @@ class ClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @mock.patch('cloudant._client_session.Session.request')
+    def test_session_basic(self, m_req):
+        """
+        Test using basic access authentication.
+        """
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        m_response_ok.json.return_value = ['animaldb']
+        m_req.return_value = m_response_ok
+
+        client = Cloudant('foo', 'bar', url=self.url, use_basic_auth=True)
+        client.connect()
+        self.assertIsInstance(client.r_session, BasicSession)
+
+        all_dbs = client.all_dbs()
+
+        m_req.assert_called_once_with(
+            'GET',
+            self.url + '/_all_dbs',
+            allow_redirects=True,
+            auth=('foo', 'bar'),  # uses HTTP Basic Auth
+            timeout=None
+        )
+
+        self.assertEquals(all_dbs, ['animaldb'])
+
+    @mock.patch('cloudant._client_session.Session.request')
+    def test_session_basic_with_no_credentials(self, m_req):
+        """
+        Test using basic access authentication with no credentials.
+        """
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        m_req.return_value = m_response_ok
+
+        client = Cloudant(None, None, url=self.url, use_basic_auth=True)
+        client.connect()
+        self.assertIsInstance(client.r_session, BasicSession)
+
+        db = client['animaldb']
+
+        m_req.assert_called_once_with(
+            'HEAD',
+            self.url + '/animaldb',
+            allow_redirects=False,
+            auth=None,  # ensure no authentication specified
+            timeout=None
+        )
+
+        self.assertIsInstance(db, CloudantDatabase)
+
+    @mock.patch('cloudant._client_session.Session.request')
+    def test_change_credentials_basic(self, m_req):
+        """
+        Test changing credentials when using basic access authentication.
+        """
+        # mock 200
+        m_response_ok = mock.MagicMock()
+        m_response_ok.json.return_value = ['animaldb']
+
+        # mock 401
+        m_response_bad = mock.MagicMock()
+        m_response_bad.raise_for_status.side_effect = HTTPError('401 Unauthorized')
+
+        m_req.side_effect = [m_response_bad, m_response_ok]
+
+        client = Cloudant('foo', 'bar', url=self.url, use_basic_auth=True)
+        client.connect()
+        self.assertIsInstance(client.r_session, BasicSession)
+
+        with self.assertRaises(HTTPError):
+            client.all_dbs()  # expected 401
+
+        m_req.assert_called_with(
+            'GET',
+            self.url + '/_all_dbs',
+            allow_redirects=True,
+            auth=('foo', 'bar'),  # uses HTTP Basic Auth
+            timeout=None
+        )
+
+        # use valid credentials
+        client.change_credentials('baz', 'qux')
+        all_dbs = client.all_dbs()
+
+        m_req.assert_called_with(
+            'GET',
+            self.url + '/_all_dbs',
+            allow_redirects=True,
+            auth=('baz', 'qux'),  # uses HTTP Basic Auth
+            timeout=None
+        )
+        self.assertEquals(all_dbs, ['animaldb'])
+
+    @skip_if_not_cookie_auth
     def test_basic_auth_str(self):
         """
         Test getting the basic authentication string.
@@ -493,6 +594,7 @@ class CloudantClientTests(UnitTestDbBase):
     Cloudant specific client unit tests
     """
 
+    @skip_if_not_cookie_auth
     def test_cloudant_session_login(self):
         """
         Test that the Cloudant client session successfully authenticates.
@@ -505,6 +607,7 @@ class CloudantClientTests(UnitTestDbBase):
         self.client.session_login()
         self.assertNotEqual(self.client.session_cookie(), old_cookie)
 
+    @skip_if_not_cookie_auth
     def test_cloudant_session_login_with_new_credentials(self):
         """
         Test that the Cloudant client session fails to authenticate when
@@ -517,6 +620,7 @@ class CloudantClientTests(UnitTestDbBase):
 
         self.assertTrue(str(cm.exception).find('Name or password is incorrect'))
 
+    @skip_if_not_cookie_auth
     def test_cloudant_context_helper(self):
         """
         Test that the cloudant context helper works as expected.
@@ -528,6 +632,7 @@ class CloudantClientTests(UnitTestDbBase):
         except Exception as err:
             self.fail('Exception {0} was raised.'.format(str(err)))
 
+    @skip_if_not_cookie_auth
     def test_cloudant_bluemix_context_helper(self):
         """
         Test that the cloudant_bluemix context helper works as expected.
@@ -592,6 +697,7 @@ class CloudantClientTests(UnitTestDbBase):
             'https://{0}.cloudant.com'.format(self.account)
             )
 
+    @skip_if_not_cookie_auth
     def test_bluemix_constructor(self):
         """
         Test instantiating a client object using a VCAP_SERVICES environment
@@ -624,6 +730,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             c.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_bluemix_constructor_specify_instance_name(self):
         """
         Test instantiating a client object using a VCAP_SERVICES environment
@@ -656,6 +763,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             c.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_bluemix_constructor_with_multiple_services(self):
         """
         Test instantiating a client object using a VCAP_SERVICES environment
@@ -723,6 +831,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_connect_timeout(self):
         """
         Test that a connect timeout occurs when instantiating
@@ -749,6 +858,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_billing_data(self):
         """
         Test the retrieval of billing data
@@ -843,6 +953,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_volume_usage_data(self):
         """
         Test the retrieval of volume usage data
@@ -934,6 +1045,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_requests_usage_data(self):
         """
         Test the retrieval of requests usage data
@@ -1025,6 +1137,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_shared_databases(self):
         """
         Test the retrieval of shared database list
@@ -1035,6 +1148,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_generate_api_key(self):
         """
         Test the generation of an API key for this client account
@@ -1048,6 +1162,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_cors_configuration(self):
         """
         Test the retrieval of the current CORS configuration for this client
@@ -1061,6 +1176,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_cors_origins(self):
         """
         Test the retrieval of the CORS origins list
@@ -1072,6 +1188,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_disable_cors(self):
         """
         Test disabling CORS (assuming CORS is enabled)
@@ -1092,6 +1209,7 @@ class CloudantClientTests(UnitTestDbBase):
         finally:
             self.client.disconnect()
 
+    @skip_if_not_cookie_auth
     def test_update_cors_configuration(self):
         """
         Test updating CORS configuration
diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py
index 94e6fea..9a35c3e 100644
--- a/tests/unit/database_tests.py
+++ b/tests/unit/database_tests.py
@@ -38,7 +38,7 @@ from cloudant.index import Index, TextIndex, SpecialIndex
 from cloudant.feed import Feed, InfiniteFeed
 from tests.unit._test_util import LONG_NUMBER
 
-from .unit_t_db_base import UnitTestDbBase
+from .unit_t_db_base import skip_if_not_cookie_auth, UnitTestDbBase
 from .. import unicode_
 
 class CloudantDatabaseExceptionTests(unittest.TestCase):
@@ -151,6 +151,7 @@ class DatabaseTests(UnitTestDbBase):
             '/'.join((self.client.server_url, self.test_dbname))
         )
 
+    @skip_if_not_cookie_auth
     def test_retrieve_creds(self):
         """
         Test retrieving client credentials. The client credentials are None if
@@ -370,6 +371,7 @@ class DatabaseTests(UnitTestDbBase):
         ddoc = self.db.get_design_document('_design/ddoc01')
         self.assertEqual(ddoc, local_ddoc)
 
+    @skip_if_not_cookie_auth
     def test_get_security_document(self):
         """
         Test retrieving the database security document
@@ -995,6 +997,7 @@ class CloudantDatabaseTests(UnitTestDbBase):
         with self.assertRaises(TypeError):
             database.unshare_database(share)
 
+    @skip_if_not_cookie_auth
     def test_security_document(self):
         """
         Test the retrieval of the security document.
@@ -1004,6 +1007,7 @@ class CloudantDatabaseTests(UnitTestDbBase):
         expected = {'cloudant': {share: ['_reader']}}
         self.assertDictEqual(self.db.security_document(), expected)
 
+    @skip_if_not_cookie_auth
     def test_share_database_default_permissions(self):
         """
         Test the sharing of a database applying default permissions.
@@ -1014,6 +1018,7 @@ class CloudantDatabaseTests(UnitTestDbBase):
         expected = {'cloudant': {share: ['_reader']}}
         self.assertDictEqual(self.db.security_document(), expected)
 
+    @skip_if_not_cookie_auth
     def test_share_database(self):
         """
         Test the sharing of a database.
@@ -1024,6 +1029,7 @@ class CloudantDatabaseTests(UnitTestDbBase):
         expected = {'cloudant': {share: ['_writer']}}
         self.assertDictEqual(self.db.security_document(), expected)
 
+    @skip_if_not_cookie_auth
     def test_share_database_with_redundant_role_entries(self):
         """
         Test the sharing of a database works when the list of roles contains
@@ -1066,6 +1072,7 @@ class CloudantDatabaseTests(UnitTestDbBase):
             '\'_db_updates\', \'_design\', \'_shards\', \'_security\']'
         )
 
+    @skip_if_not_cookie_auth
     def test_unshare_database(self):
         """
         Test the un-sharing of a database from a specified user.
diff --git a/tests/unit/iam_auth_tests.py b/tests/unit/iam_auth_tests.py
index a65d3f0..d6b3c04 100644
--- a/tests/unit/iam_auth_tests.py
+++ b/tests/unit/iam_auth_tests.py
@@ -19,8 +19,8 @@ import json
 import mock
 
 from cloudant._2to3 import Cookie
-from cloudant._common_util import IAMSession
 from cloudant.client import Cloudant
+from cloudant._client_session import IAMSession
 
 MOCK_API_KEY = 'CqbrIYzdO3btWV-5t4teJLY_etfT_dkccq-vO-5vCXSo'
 
@@ -97,7 +97,7 @@ class IAMAuthTests(unittest.TestCase):
 
         self.assertEquals(iam._api_key, new_api_key)
 
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_get_access_token(self, m_req):
         m_response = mock.MagicMock()
         m_response.json.return_value = MOCK_OIDC_TOKEN_RESPONSE
@@ -122,8 +122,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertTrue(m_response.raise_for_status.called)
         self.assertTrue(m_response.json.called)
 
-    @mock.patch('cloudant._common_util.ClientSession.request')
-    @mock.patch('cloudant._common_util.IAMSession._get_access_token')
+    @mock.patch('cloudant._client_session.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession._get_access_token')
     def test_iam_login(self, m_token, m_req):
         m_token.return_value = MOCK_ACCESS_TOKEN
         m_response = mock.MagicMock()
@@ -150,7 +150,7 @@ class IAMAuthTests(unittest.TestCase):
         iam.logout()
         self.assertEqual(len(iam.cookies.keys()), 0)
 
-    @mock.patch('cloudant._common_util.ClientSession.get')
+    @mock.patch('cloudant._client_session.ClientSession.get')
     def test_iam_get_session_info(self, m_get):
         m_info = {'ok': True, 'info': {'authentication_db': '_users'}}
 
@@ -166,8 +166,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(info, m_info)
         self.assertTrue(m_response.raise_for_status.called)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_first_request(self, m_req, m_login):
         # mock 200
         m_response_ok = mock.MagicMock()
@@ -191,8 +191,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_req.call_count, 1)
         self.assertEqual(resp.status_code, 200)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_renew_cookie_on_expiry(self, m_req, m_login):
         # mock 200
         m_response_ok = mock.MagicMock()
@@ -213,8 +213,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_req.call_count, 1)
         self.assertEqual(resp.status_code, 200)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_renew_cookie_on_401_success(self, m_req, m_login):
         # mock 200
         m_response_ok = mock.MagicMock()
@@ -243,8 +243,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_login.call_count, 2)
         self.assertEqual(m_req.call_count, 3)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_renew_cookie_on_401_failure(self, m_req, m_login):
         # mock 401
         m_response_bad = mock.MagicMock()
@@ -269,8 +269,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_login.call_count, 3)
         self.assertEqual(m_req.call_count, 4)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_renew_cookie_disabled(self, m_req, m_login):
         # mock 401
         m_response_bad = mock.MagicMock()
@@ -292,8 +292,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_login.call_count, 1)  # no attempt to renew
         self.assertEqual(m_req.call_count, 2)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_client_create(self, m_req, m_login):
         # mock 200
         m_response_ok = mock.MagicMock()
@@ -315,8 +315,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_req.call_count, 1)
         self.assertEqual(dbs, ['animaldb'])
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.IAMSession.set_credentials')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.IAMSession.set_credentials')
     def test_iam_client_session_login(self, m_set, m_login):
         # create IAM client
         client = Cloudant.iam('foo', MOCK_API_KEY)
@@ -331,8 +331,8 @@ class IAMAuthTests(unittest.TestCase):
         self.assertEqual(m_login.call_count, 2)
         self.assertEqual(m_set.call_count, 2)
 
-    @mock.patch('cloudant._common_util.IAMSession.login')
-    @mock.patch('cloudant._common_util.IAMSession.set_credentials')
+    @mock.patch('cloudant._client_session.IAMSession.login')
+    @mock.patch('cloudant._client_session.IAMSession.set_credentials')
     def test_iam_client_session_login_with_new_credentials(self, m_set, m_login):
         # create IAM client
         client = Cloudant.iam('foo', MOCK_API_KEY)
diff --git a/tests/unit/replicator_tests.py b/tests/unit/replicator_tests.py
index 74d0846..5c23140 100644
--- a/tests/unit/replicator_tests.py
+++ b/tests/unit/replicator_tests.py
@@ -34,7 +34,7 @@ from cloudant.replicator import Replicator
 from cloudant.document import Document
 from cloudant.error import CloudantReplicatorException, CloudantClientException
 
-from .unit_t_db_base import UnitTestDbBase
+from .unit_t_db_base import skip_if_not_cookie_auth, UnitTestDbBase
 from .. import unicode_
 
 class CloudantReplicatorExceptionTests(unittest.TestCase):
@@ -157,6 +157,7 @@ class ReplicatorTests(UnitTestDbBase):
         clone = Replicator(self.client)
         clone.create_replication(self.db, self.target_db)
 
+    @skip_if_not_cookie_auth
     @flaky(max_runs=3)
     def test_create_replication(self):
         """
@@ -300,6 +301,7 @@ class ReplicatorTests(UnitTestDbBase):
         match = [repl_id for repl_id in all_repl_ids if repl_id in repl_ids]
         self.assertEqual(set(repl_ids), set(match))
 
+    @skip_if_not_cookie_auth
     def test_retrieve_replication_state(self):
         """
         Test that the replication state can be retrieved for a replication
@@ -341,6 +343,7 @@ class ReplicatorTests(UnitTestDbBase):
             )
             self.assertIsNone(repl_state)
 
+    @skip_if_not_cookie_auth
     def test_stop_replication(self):
         """
         Test that a replication can be stopped.
@@ -376,6 +379,7 @@ class ReplicatorTests(UnitTestDbBase):
                 'Replication with id {} not found.'.format(repl_id)
             )
 
+    @skip_if_not_cookie_auth
     def test_follow_replication(self):
         """
         Test that follow_replication(...) properly iterates updated
diff --git a/tests/unit/unit_t_db_base.py b/tests/unit/unit_t_db_base.py
index c0b62b7..ccec69a 100644
--- a/tests/unit/unit_t_db_base.py
+++ b/tests/unit/unit_t_db_base.py
@@ -68,6 +68,15 @@ from cloudant.design_document import DesignDocument
 
 from .. import unicode_
 
+
+def skip_if_not_cookie_auth(f):
+    def wrapper(*args):
+        if not args[0].use_cookie_auth:
+            raise unittest.SkipTest('Test only supports cookie authentication')
+        return f(*args)
+    return wrapper
+
+
 class UnitTestDbBase(unittest.TestCase):
     """
     The base class for all unit tests targeting a database
@@ -133,14 +142,19 @@ class UnitTestDbBase(unittest.TestCase):
 
     def set_up_client(self, auto_connect=False, auto_renew=False, encoder=None,
                       timeout=(30,300)):
+        self.user = os.environ.get('DB_USER', None)
+        self.pwd = os.environ.get('DB_PASSWORD', None)
+        self.use_cookie_auth = True
+
         if os.environ.get('RUN_CLOUDANT_TESTS') is None:
+            self.url = os.environ['DB_URL']
+
             admin_party = False
-            if (os.environ.get('ADMIN_PARTY') and
-                os.environ.get('ADMIN_PARTY') == 'true'):
+            if os.environ.get('ADMIN_PARTY') == 'true':
                 admin_party = True
-            self.user = os.environ.get('DB_USER', None)
-            self.pwd = os.environ.get('DB_PASSWORD', None)
-            self.url = os.environ['DB_URL']
+
+            self.use_cookie_auth = False
+            # construct Cloudant client (using admin party mode)
             self.client = CouchDB(
                 self.user,
                 self.pwd,
@@ -153,22 +167,50 @@ class UnitTestDbBase(unittest.TestCase):
             )
         else:
             self.account = os.environ.get('CLOUDANT_ACCOUNT')
-            self.user = os.environ.get('DB_USER')
-            self.pwd = os.environ.get('DB_PASSWORD')
             self.url = os.environ.get(
                 'DB_URL',
                 'https://{0}.cloudant.com'.format(self.account))
-            self.client = Cloudant(
-                self.user,
-                self.pwd,
-                url=self.url,
-                x_cloudant_user=self.account,
-                connect=auto_connect,
-                auto_renew=auto_renew,
-                encoder=encoder,
-                timeout=timeout
-            )
 
+            if os.environ.get('RUN_BASIC_AUTH_TESTS'):
+                self.use_cookie_auth = False
+                # construct Cloudant client (using basic access authentication)
+                self.client = Cloudant(
+                    self.user,
+                    self.pwd,
+                    url=self.url,
+                    x_cloudant_user=self.account,
+                    connect=auto_connect,
+                    auto_renew=auto_renew,
+                    encoder=encoder,
+                    timeout=timeout,
+                    use_basic_auth=True,
+                )
+            elif os.environ.get('IAM_API_KEY'):
+                self.use_cookie_auth = False
+                # construct Cloudant client (using IAM authentication)
+                self.client = Cloudant(
+                    None,  # username is not required
+                    os.environ.get('IAM_API_KEY'),
+                    url=self.url,
+                    x_cloudant_user=self.account,
+                    connect=auto_connect,
+                    auto_renew=auto_renew,
+                    encoder=encoder,
+                    timeout=timeout,
+                    use_iam=True,
+                )
+            else:
+                # construct Cloudant client (using cookie authentication)
+                self.client = Cloudant(
+                    self.user,
+                    self.pwd,
+                    url=self.url,
+                    x_cloudant_user=self.account,
+                    connect=auto_connect,
+                    auto_renew=auto_renew,
+                    encoder=encoder,
+                    timeout=timeout
+                )
 
     def tearDown(self):
         """
@@ -285,17 +327,9 @@ class UnitTestDbBase(unittest.TestCase):
                     'bar2': ['_reader']
                 }
             }
-        if os.environ.get('ADMIN_PARTY') == 'true':
-            resp = requests.put(
-                '/'.join([self.db.database_url, '_security']),
-                data=json.dumps(self.sdoc),
-                headers={'Content-Type': 'application/json'}
-            )
-        else:
-            resp = requests.put(
-                '/'.join([self.db.database_url, '_security']),
-                auth=(self.user, self.pwd),
-                data=json.dumps(self.sdoc),
-                headers={'Content-Type': 'application/json'}
-            )
+        resp = self.client.r_session.put(
+            '/'.join([self.db.database_url, '_security']),
+            data=json.dumps(self.sdoc),
+            headers={'Content-Type': 'application/json'}
+        )
         self.assertEqual(resp.status_code, 200)