diff --git a/configure.ac b/configure.ac
index ebb60a6c0dd3b7618b0971dd4f29e01bf865f7ef..764afdcc8237bdd9e7afb4282eb5b318beb7eb62 100644
--- a/configure.ac
+++ b/configure.ac
@@ -93,7 +93,7 @@ dnl Python-dev (actually only used for $PYTHON)
 AX_PYTHON_DEVEL
 
 dnl nosetests
-AC_PATH_PROG([NOSETESTS], [nosetests])
+AC_PATH_PROG([NOSETESTS], [nosetests${PYTHON_VERSION}])
 
 dnl GoogleTest (use the embedded version)
 GTEST_LIBS="\$(top_builddir)/lib/gtest/libgtest.la"
diff --git a/debian/compat b/debian/compat
index ec635144f60048986bc560c5576355344005e6e7..f599e28b8ab0d8c9c57a486c89c4a5132dcbd3b2 100644
--- a/debian/compat
+++ b/debian/compat
@@ -1 +1 @@
-9
+10
diff --git a/debian/control b/debian/control
index c27a7673075d5d0a3071329f26d64a638e4b5cf5..8d23064e2a89082ae83c67d56e719d416049f67f 100644
--- a/debian/control
+++ b/debian/control
@@ -3,9 +3,9 @@ Section: net
 Priority: extra
 Maintainer: Autistici/Inventati <debian@autistici.org>
 Build-Depends: debhelper (>= 10), apache2-dev | apache2-prefork-dev | apache2-threaded-dev,
- apache2, autoconf, automake, libtool, python-dev, dh-python, python-all,
- libpam-dev, libssl-dev, python-setuptools, python-flup, pkg-config, libz-dev,
- python-m2crypto, python-flask, python-nose, python-mox, python-beautifulsoup
+ apache2, autoconf, automake, libtool, python3-dev, dh-python, python3-all,
+ libpam-dev, libssl-dev, python3-setuptools, pkg-config, libz-dev,
+ python3-requests, python3-nose, python3-mox
 Standards-Version: 3.7.2
 
 Package: ai-sso
diff --git a/debian/rules b/debian/rules
index ce37677bfdd763473e45a44cbe90c50fb97d0a87..f606ef66890f1788e36aea4eccfc0210f15b8555 100755
--- a/debian/rules
+++ b/debian/rules
@@ -9,7 +9,7 @@
 
 override_dh_auto_configure:
 	sh autogen.sh
-	./configure --prefix=/usr --with-pam-dir=/lib/security --enable-pam-sso --enable-mod-sso --enable-shared
+	./configure --prefix=/usr --with-pam-dir=/lib/security --enable-pam-sso --enable-mod-sso --enable-shared PYTHON_VERSION=3
 
 override_dh_auto_install:
 	install -d $(CURDIR)/debian/tmp/etc/sso
diff --git a/src/Makefile.am b/src/Makefile.am
index 4e52d79e7a429fa5a9d89aea7e06d15d4dd38c19..8e6236b36d900a35445a19e51725b7f52e9265c0 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -7,7 +7,7 @@ if ENABLE_PAM_SSO
 PAM_SSO_SUBDIR = pam_sso
 endif
 
-PYTHON_SUBDIR = python sso_server
+PYTHON_SUBDIR = python #sso_server
 
 SUBDIRS = \
 	sso \
diff --git a/src/mod_sso/test/httpd_integration_test.py b/src/mod_sso/test/httpd_integration_test.py
index 38079f56219c3658f1e9bf68b6e615a9e2ba2b2c..996b667e0c9e3ca5e3a7b7ed01556653208c3f8d 100755
--- a/src/mod_sso/test/httpd_integration_test.py
+++ b/src/mod_sso/test/httpd_integration_test.py
@@ -1,15 +1,13 @@
 #!/usr/bin/python
 
-import Cookie
-import httplib
 import os
 import re
+import requests
 import subprocess
 import sys
 import time
 import unittest
-import urllib
-import urlparse
+from urllib.parse import urlencode, urlsplit, parse_qsl
 
 sys.path.append("../../python")
 import sso
@@ -31,7 +29,7 @@ devnull = open(os.devnull)
 
 
 def _start_httpd(public_key, config_file):
-    with open('public.key', 'w') as fd:
+    with open('public.key', 'wb') as fd:
         fd.write(public_key)
     env = dict(os.environ)
     env['TESTROOT'] = os.getcwd()
@@ -58,7 +56,7 @@ def _start_httpd(public_key, config_file):
     return httpd
 
 
-def _stop_httpd(httpd):
+def _stop_httpd(httpd, dump_log=False):
     httpd.terminate()
     time.sleep(1)
     try:
@@ -66,23 +64,25 @@ def _stop_httpd(httpd):
     except OSError:
         pass
 
-    dump_log = (os.getenv('DUMP_APACHE_LOG') == '1')
+    if os.getenv('DUMP_APACHE_LOG'):
+        dump_log = True
     status = httpd.wait()
     if os.WIFEXITED(status):
         if os.WEXITSTATUS(status) != 0:
-            print 'WARNING: httpd exited with status %d' % os.WEXITSTATUS(status)
+            print('WARNING: httpd exited with status %d' % os.WEXITSTATUS(status))
             dump_log = True
     elif os.WIFSIGNALED(status):
-        print 'WARNING: httpd exited due to signal %d' % os.WTERMSIG(status)
+        print('WARNING: httpd exited due to signal %d' % os.WTERMSIG(status))
         dump_log = True
     else:
-        print 'WARNING: httpd exited for unknown reason (returncode=%d)' % status
+        print('WARNING: httpd exited for unknown reason (returncode=%d)' % status)
         dump_log = True
 
     if dump_log:
+        print('*** APACHE LOG ***')
         with open(APACHE_LOG) as fd:
             for line in fd:
-                print line.rstrip('\n')
+                print(line.rstrip('\n'))
 
     for f in ('public.key', 'test-httpd.pid', '.apache_log'):
         try:
@@ -93,20 +93,20 @@ def _stop_httpd(httpd):
 
 def _query(url, host=None, cookie=None):
     """Make a simple request to url using httplib."""
-    conn = httplib.HTTPConnection("127.0.0.1", APACHE_PORT)
     headers = {"Host": host or "localhost"}
     if cookie:
-        headers["Cookie"] = cookie
-    conn.request("GET", url, headers=headers)
-    resp = conn.getresponse()
+        headers['Cookie'] = cookie
+    resp = requests.get(
+        f'http://127.0.0.1:{APACHE_PORT}{url}',
+        headers=headers,
+        allow_redirects=False)
     location = None
     body = None
-    if resp.status in (301, 302):
-        location = resp.getheader("Location")
-    elif resp.status == 200:
-        body = resp.read()
-    conn.close()
-    return (resp.status, body, location)
+    if resp.status_code in (301, 302):
+        location = resp.headers["Location"]
+    elif resp.status_code == 200:
+        body = resp.text
+    return (resp.status_code, body, location)
 
 
 def mkcookie(tkt):
@@ -116,9 +116,9 @@ def mkcookie(tkt):
 class HttpdKeyLoadingTest(unittest.TestCase):
 
     def test_key_with_null_byte(self):
-        has_null_byte = lambda s: '\0' in s
+        has_null_byte = lambda s: 0 in s
 
-        public = ''
+        public = b''
         while not has_null_byte(public):
             public, secret = sso.generate_keys()
 
@@ -138,6 +138,7 @@ class HttpdKeyLoadingTest(unittest.TestCase):
 class HttpdIntegrationTestBase(unittest.TestCase):
 
     CONFIG = None
+    DUMP_LOG = False
 
     def setUp(self):
         self.public, self.secret = sso.generate_keys()
@@ -145,7 +146,7 @@ class HttpdIntegrationTestBase(unittest.TestCase):
         self.httpd = _start_httpd(self.public, self.CONFIG)
 
     def tearDown(self):
-        _stop_httpd(self.httpd)
+        _stop_httpd(self.httpd, self.DUMP_LOG)
 
     def _ticket(self, user="testuser", group="group1", service="service.example.com/",
                 nonce=None):
@@ -238,8 +239,8 @@ class HttpdIntegrationTestBase(unittest.TestCase):
               "status": 401 }),
             ]
         for name, check in checks:
-            for i in xrange(repeats):
-                print 'CHECKING %s (%d of 10)' % (name, i), check
+            for i in range(repeats):
+                print('CHECKING %s (%d of 10)' % (name, i), check)
                 status, body, location = _query(check["url"],
                                                 host=check.get("http_host"),
                                                 cookie=check.get("cookie"))
@@ -268,48 +269,41 @@ class HttpdIntegrationTestBase(unittest.TestCase):
         if not sso_login_url:
             sso_login_url = '/%s/sso_login' % sso_service.split('/', 1)[1]
 
-        cookies = Cookie.SimpleCookie()
+        sess = requests.Session()
 
         # Make a request to a protected URL.
-        conn = httplib.HTTPConnection("127.0.0.1", APACHE_PORT)
-        conn.request("GET", url, headers={'Host': host_header})
-        resp = conn.getresponse()
-        self.assertEquals(302, resp.status)
-        set_cookie_hdr = resp.getheader("Set-Cookie")
-        self.assertTrue(set_cookie_hdr)
-        cookies.load(set_cookie_hdr)
-        location = resp.getheader("Location")
-        location_u = urlparse.urlsplit(location)
-        location_args = dict(urlparse.parse_qsl(location_u.query))
+        resp = sess.get(
+            f'http://127.0.0.1:{APACHE_PORT}{url}',
+            headers={'Host': host_header},
+            allow_redirects=False)
+        self.assertEquals(302, resp.status_code)
+        location = resp.headers["Location"]
+        location_u = urlsplit(location)
+        location_args = dict(parse_qsl(location_u.query))
         nonce = location_args.get('n')
         self.assertTrue(nonce)
-        conn.close()
+        session_cookie = resp.cookies['_sso_local_session']
 
         # Now call the /sso_login endpoint.
-        conn = httplib.HTTPConnection("127.0.0.1", APACHE_PORT)
         tkt = self._ticket(nonce=nonce, service=sso_service)
-        conn.request("GET", sso_login_url + "?" + urllib.urlencode(
-            {"t": tkt, "d": "https://" + host_header + url}), headers={
-                "Cookie": cookies.output(attrs=[], header='', sep='; '),
-                "Host": host_header,
-            })
-        resp = conn.getresponse()
-        self.assertEquals(302, resp.status)
-        set_cookie_hdr = resp.getheader("Set-Cookie")
-        self.assertTrue(set_cookie_hdr)
-        cookies.load(set_cookie_hdr)
-        self.assertEquals(tkt, cookies['SSO_test'].value)
-        conn.close()
+        query_args = urlencode({"t": tkt, "d": "https://" + host_header + url})
+        resp = sess.get(
+            f'http://127.0.0.1:{APACHE_PORT}{sso_login_url}?{query_args}',
+            headers={'Host': host_header},
+            cookies={'_sso_local_session': session_cookie},
+            allow_redirects=False)
+        self.assertEquals(302, resp.status_code)
+        self.assertTrue('SSO_test' in resp.cookies,
+                        'missing cookie: %s\n%s' % (resp.cookies, resp.headers))
+        self.assertEquals(tkt, resp.cookies['SSO_test'])
 
         # Make the original request again.
-        conn = httplib.HTTPConnection("127.0.0.1", APACHE_PORT)
-        conn.request("GET", url, headers={
-            "Cookie": cookies.output(attrs=[], header='', sep='; '),
-            'Host': host_header,
-        })
-        resp = conn.getresponse()
-        self.assertEquals(200, resp.status)
-        conn.close()
+        resp = sess.get(
+            f'http://127.0.0.1:{APACHE_PORT}{url}',
+            headers={'Host': host_header},
+            cookies={'SSO_test': tkt},
+            allow_redirects=False)
+        self.assertEquals(200, resp.status_code)
 
 
 class HttpdIntegrationTest(HttpdIntegrationTestBase):
@@ -324,8 +318,7 @@ class HttpdIntegrationTest(HttpdIntegrationTestBase):
         # to spot issues where we are not cleaning up state properly
         # between requests.
         n = 100
-        errors = 0
-        for i in xrange(n):
+        for i in range(n):
             cookie = 'SSO_test=%s' % self._ticket()
             status, body, location = _query("/index.html", cookie=cookie)
             self.assertEquals(200, status)
@@ -345,29 +338,28 @@ class HttpdIntegrationTest(HttpdIntegrationTestBase):
     def test_sso_login(self):
         # Call the /sso_login endpoint, verify that it sets the local
         # SSO cookie to whatever the 't' parameter says.
-        cookies = Cookie.SimpleCookie()
-        conn = httplib.HTTPConnection("127.0.0.1", APACHE_PORT)
         tkt = self._ticket()
-        conn.request("GET", "/sso_login?%s" % urllib.urlencode(
-            {"t": tkt, "d": "https://service.example.com/index.html"}))
-        resp = conn.getresponse()
-        self.assertEquals(302, resp.status)
-        set_cookie_hdr = resp.getheader("Set-Cookie")
+        query_args = urlencode(
+            {"t": tkt, "d": "https://service.example.com/index.html"})
+        resp = requests.get(
+            f'http://127.0.0.1:{APACHE_PORT}/sso_login?{query_args}',
+            allow_redirects=False)
+        self.assertEquals(302, resp.status_code)
+        set_cookie_hdr = resp.headers["Set-Cookie"]
         self.assertTrue(set_cookie_hdr)
-        cookies.load(set_cookie_hdr)
-        self.assertEquals(tkt, cookies['SSO_test'].value)
-        conn.close()
+        self.assertTrue('SSO_test' in resp.cookies,
+                        'missing cookie: %s' % resp.cookies)
+        self.assertEquals(tkt, resp.cookies['SSO_test'])
 
     def test_sso_logout(self):
         # test the /sso_logout endpoint
-        conn = httplib.HTTPConnection("127.0.0.1", APACHE_PORT)
-        conn.request("GET", "/sso_logout", headers={
-            "Cookie": mkcookie(self._ticket())})
-        resp = conn.getresponse()
-        set_cookie_hdr = resp.getheader("Set-Cookie")
+        resp = requests.get(
+            f'http://127.0.0.1:{APACHE_PORT}/sso_logout',
+            headers={"Cookie": mkcookie(self._ticket())},
+            allow_redirects=False)
+        set_cookie_hdr = resp.headers["Set-Cookie"]
         self.assertTrue(set_cookie_hdr)
         self.assertTrue("SSO_test=;" in set_cookie_hdr)
-        conn.close()
 
 
 class HttpdIntegrationTestWithNonces(HttpdIntegrationTestBase):
@@ -375,7 +367,7 @@ class HttpdIntegrationTestWithNonces(HttpdIntegrationTestBase):
     CONFIG = 'test-httpd-2.4-nonces.conf'
 
     def setUp(self):
-        with open('session.key', 'w') as fd:
+        with open('session.key', 'wb') as fd:
             fd.write(os.urandom(32))
         HttpdIntegrationTestBase.setUp(self)
 
@@ -409,9 +401,10 @@ class HttpdIntegrationTestWithNonces(HttpdIntegrationTestBase):
 class WebmailIntegrationTestWithNonces(HttpdIntegrationTestBase):
 
     CONFIG = 'test-httpd-2.4-webmail.conf'
+    DUMP_LOG = True
 
     def setUp(self):
-        with open('session.key', 'w') as fd:
+        with open('session.key', 'wb') as fd:
             fd.write(os.urandom(32))
         HttpdIntegrationTestBase.setUp(self)
 
diff --git a/src/python/sso/urllib2_handler.py b/src/python/sso/urllib2_handler.py
index 4f60ee9fa6e29c6a195cf623f52811089c701964..73f698bd230ed1707b13fb2cbc5434a73fc0b1b4 100644
--- a/src/python/sso/urllib2_handler.py
+++ b/src/python/sso/urllib2_handler.py
@@ -87,9 +87,3 @@ def install_handler(jar=None, **kwargs):
         urllib2.build_opener(urllib2.HTTPCookieProcessor(jar),
                              SSOProcessor(**kwargs)))
 
-
-if __name__ == '__main__':
-    install_handler()
-    resp = urllib2.urlopen('https://wiki.autistici.org/')
-    print resp.read()
-