diff --git a/configure.ac b/configure.ac
index 4443904c65ab5850953040e744ccd8c61a841fa5..b60a036f6d77bc8452c506d0f4a340d75ad3953d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -84,13 +84,11 @@ if test "$build_pam_sso" != "no" ; then
 fi
 AM_CONDITIONAL(ENABLE_PAM_SSO, [ test "$build_pam_sso" != "no" ])
 
-dnl SWIG
-dnl AC_PROG_SWIG(1.3.17)
-AX_PKG_SWIG(1.3.17)
-AX_SWIG_ENABLE_CXX
-AX_SWIG_MULTI_MODULE_SUPPORT
-AX_SWIG_PYTHON
-AM_CONDITIONAL(SWIG, [ test -n "$SWIG" && $SWIG -version >/dev/null 2>&1 ])
+dnl Python-dev (actually only used for $PYTHON)
+AX_PYTHON_DEVEL
+
+dnl nosetests
+AC_PATH_PROG([NOSETESTS], [nosetests])
 
 dnl GoogleTest (use the embedded version)
 GTEST_LIBS="\$(top_builddir)/lib/gtest/libgtest.la"
diff --git a/m4/ax_pkg_swig.m4 b/m4/ax_pkg_swig.m4
deleted file mode 100644
index e112f3d3fa5d1a2c34315249270863a9fdd0c7f9..0000000000000000000000000000000000000000
--- a/m4/ax_pkg_swig.m4
+++ /dev/null
@@ -1,135 +0,0 @@
-# ===========================================================================
-#        http://www.gnu.org/software/autoconf-archive/ax_pkg_swig.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-#   AX_PKG_SWIG([major.minor.micro], [action-if-found], [action-if-not-found])
-#
-# DESCRIPTION
-#
-#   This macro searches for a SWIG installation on your system. If found,
-#   then SWIG is AC_SUBST'd; if not found, then $SWIG is empty.  If SWIG is
-#   found, then SWIG_LIB is set to the SWIG library path, and AC_SUBST'd.
-#
-#   You can use the optional first argument to check if the version of the
-#   available SWIG is greater than or equal to the value of the argument. It
-#   should have the format: N[.N[.N]] (N is a number between 0 and 999. Only
-#   the first N is mandatory.) If the version argument is given (e.g.
-#   1.3.17), AX_PKG_SWIG checks that the swig package is this version number
-#   or higher.
-#
-#   As usual, action-if-found is executed if SWIG is found, otherwise
-#   action-if-not-found is executed.
-#
-#   In configure.in, use as:
-#
-#     AX_PKG_SWIG(1.3.17, [], [ AC_MSG_ERROR([SWIG is required to build..]) ])
-#     AX_SWIG_ENABLE_CXX
-#     AX_SWIG_MULTI_MODULE_SUPPORT
-#     AX_SWIG_PYTHON
-#
-# LICENSE
-#
-#   Copyright (c) 2008 Sebastian Huber <sebastian-huber@web.de>
-#   Copyright (c) 2008 Alan W. Irwin <irwin@beluga.phys.uvic.ca>
-#   Copyright (c) 2008 Rafael Laboissiere <rafael@laboissiere.net>
-#   Copyright (c) 2008 Andrew Collier <colliera@ukzn.ac.za>
-#   Copyright (c) 2011 Murray Cumming <murrayc@openismus.com>
-#
-#   This program is free software; you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by the
-#   Free Software Foundation; either version 2 of the License, or (at your
-#   option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-#   Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License along
-#   with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#   As a special exception, the respective Autoconf Macro's copyright owner
-#   gives unlimited permission to copy, distribute and modify the configure
-#   scripts that are the output of Autoconf when processing the Macro. You
-#   need not follow the terms of the GNU General Public License when using
-#   or distributing such scripts, even though portions of the text of the
-#   Macro appear in them. The GNU General Public License (GPL) does govern
-#   all other use of the material that constitutes the Autoconf Macro.
-#
-#   This special exception to the GPL applies to versions of the Autoconf
-#   Macro released by the Autoconf Archive. When you make and distribute a
-#   modified version of the Autoconf Macro, you may extend this special
-#   exception to the GPL to apply to your modified version as well.
-
-#serial 8
-
-AC_DEFUN([AX_PKG_SWIG],[
-        # Ubuntu has swig 2.0 as /usr/bin/swig2.0
-        AC_PATH_PROGS([SWIG],[swig swig2.0])
-        if test -z "$SWIG" ; then
-                m4_ifval([$3],[$3],[:])
-        elif test -n "$1" ; then
-                AC_MSG_CHECKING([SWIG version])
-                [swig_version=`$SWIG -version 2>&1 | grep 'SWIG Version' | sed 's/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/g'`]
-                AC_MSG_RESULT([$swig_version])
-                if test -n "$swig_version" ; then
-                        # Calculate the required version number components
-                        [required=$1]
-                        [required_major=`echo $required | sed 's/[^0-9].*//'`]
-                        if test -z "$required_major" ; then
-                                [required_major=0]
-                        fi
-                        [required=`echo $required | sed 's/[0-9]*[^0-9]//'`]
-                        [required_minor=`echo $required | sed 's/[^0-9].*//'`]
-                        if test -z "$required_minor" ; then
-                                [required_minor=0]
-                        fi
-                        [required=`echo $required | sed 's/[0-9]*[^0-9]//'`]
-                        [required_patch=`echo $required | sed 's/[^0-9].*//'`]
-                        if test -z "$required_patch" ; then
-                                [required_patch=0]
-                        fi
-                        # Calculate the available version number components
-                        [available=$swig_version]
-                        [available_major=`echo $available | sed 's/[^0-9].*//'`]
-                        if test -z "$available_major" ; then
-                                [available_major=0]
-                        fi
-                        [available=`echo $available | sed 's/[0-9]*[^0-9]//'`]
-                        [available_minor=`echo $available | sed 's/[^0-9].*//'`]
-                        if test -z "$available_minor" ; then
-                                [available_minor=0]
-                        fi
-                        [available=`echo $available | sed 's/[0-9]*[^0-9]//'`]
-                        [available_patch=`echo $available | sed 's/[^0-9].*//'`]
-                        if test -z "$available_patch" ; then
-                                [available_patch=0]
-                        fi
-                        # Convert the version tuple into a single number for easier comparison.
-                        # Using base 100 should be safe since SWIG internally uses BCD values
-                        # to encode its version number.
-                        required_swig_vernum=`expr $required_major \* 10000 \
-                            \+ $required_minor \* 100 \+ $required_patch`
-                        available_swig_vernum=`expr $available_major \* 10000 \
-                            \+ $available_minor \* 100 \+ $available_patch`
-
-                        if test $available_swig_vernum -lt $required_swig_vernum; then
-                                AC_MSG_WARN([SWIG version >= $1 is required.  You have $swig_version.])
-                                SWIG=''
-                                m4_ifval([$3],[$3],[])
-                        else
-                                AC_MSG_CHECKING([for SWIG library])
-                                SWIG_LIB=`$SWIG -swiglib`
-                                AC_MSG_RESULT([$SWIG_LIB])
-                                m4_ifval([$2],[$2],[])
-                        fi
-                else
-                        AC_MSG_WARN([cannot determine SWIG version])
-                        SWIG=''
-                        m4_ifval([$3],[$3],[])
-                fi
-        fi
-        AC_SUBST([SWIG_LIB])
-])
diff --git a/m4/ax_swig_enable_cxx.m4 b/m4/ax_swig_enable_cxx.m4
deleted file mode 100644
index 348c15d42cbaf019dfaea81e51dbdbfa68082879..0000000000000000000000000000000000000000
--- a/m4/ax_swig_enable_cxx.m4
+++ /dev/null
@@ -1,53 +0,0 @@
-# ===========================================================================
-#    http://www.gnu.org/software/autoconf-archive/ax_swig_enable_cxx.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-#   AX_SWIG_ENABLE_CXX
-#
-# DESCRIPTION
-#
-#   Enable SWIG C++ support. This affects all invocations of $(SWIG).
-#
-# LICENSE
-#
-#   Copyright (c) 2008 Sebastian Huber <sebastian-huber@web.de>
-#   Copyright (c) 2008 Alan W. Irwin <irwin@beluga.phys.uvic.ca>
-#   Copyright (c) 2008 Rafael Laboissiere <rafael@laboissiere.net>
-#   Copyright (c) 2008 Andrew Collier <colliera@ukzn.ac.za>
-#
-#   This program is free software; you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by the
-#   Free Software Foundation; either version 2 of the License, or (at your
-#   option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-#   Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License along
-#   with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#   As a special exception, the respective Autoconf Macro's copyright owner
-#   gives unlimited permission to copy, distribute and modify the configure
-#   scripts that are the output of Autoconf when processing the Macro. You
-#   need not follow the terms of the GNU General Public License when using
-#   or distributing such scripts, even though portions of the text of the
-#   Macro appear in them. The GNU General Public License (GPL) does govern
-#   all other use of the material that constitutes the Autoconf Macro.
-#
-#   This special exception to the GPL applies to versions of the Autoconf
-#   Macro released by the Autoconf Archive. When you make and distribute a
-#   modified version of the Autoconf Macro, you may extend this special
-#   exception to the GPL to apply to your modified version as well.
-
-#serial 6
-
-AU_ALIAS([SWIG_ENABLE_CXX], [AX_SWIG_ENABLE_CXX])
-AC_DEFUN([AX_SWIG_ENABLE_CXX],[
-        AC_REQUIRE([AX_PKG_SWIG])
-        AC_REQUIRE([AC_PROG_CXX])
-        SWIG="$SWIG -c++"
-])
diff --git a/m4/ax_swig_multi_module_support.m4 b/m4/ax_swig_multi_module_support.m4
deleted file mode 100644
index f9c38427c9c6a8f43fcb200eaa9c0b714ea65d75..0000000000000000000000000000000000000000
--- a/m4/ax_swig_multi_module_support.m4
+++ /dev/null
@@ -1,56 +0,0 @@
-# ================================================================================
-#  http://www.gnu.org/software/autoconf-archive/ax_swig_multi_module_support.html
-# ================================================================================
-#
-# SYNOPSIS
-#
-#   AX_SWIG_MULTI_MODULE_SUPPORT
-#
-# DESCRIPTION
-#
-#   Enable support for multiple modules. This effects all invocations of
-#   $(SWIG). You have to link all generated modules against the appropriate
-#   SWIG runtime library. If you want to build Python modules for example,
-#   use the AX_SWIG_PYTHON macro and link the modules against
-#   $(AX_SWIG_PYTHON_LIBS).
-#
-# LICENSE
-#
-#   Copyright (c) 2008 Sebastian Huber <sebastian-huber@web.de>
-#   Copyright (c) 2008 Alan W. Irwin <irwin@beluga.phys.uvic.ca>
-#   Copyright (c) 2008 Rafael Laboissiere <rafael@laboissiere.net>
-#   Copyright (c) 2008 Andrew Collier <colliera@ukzn.ac.za>
-#
-#   This program is free software; you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by the
-#   Free Software Foundation; either version 2 of the License, or (at your
-#   option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-#   Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License along
-#   with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#   As a special exception, the respective Autoconf Macro's copyright owner
-#   gives unlimited permission to copy, distribute and modify the configure
-#   scripts that are the output of Autoconf when processing the Macro. You
-#   need not follow the terms of the GNU General Public License when using
-#   or distributing such scripts, even though portions of the text of the
-#   Macro appear in them. The GNU General Public License (GPL) does govern
-#   all other use of the material that constitutes the Autoconf Macro.
-#
-#   This special exception to the GPL applies to versions of the Autoconf
-#   Macro released by the Autoconf Archive. When you make and distribute a
-#   modified version of the Autoconf Macro, you may extend this special
-#   exception to the GPL to apply to your modified version as well.
-
-#serial 7
-
-AU_ALIAS([SWIG_MULTI_MODULE_SUPPORT], [AX_SWIG_MULTI_MODULE_SUPPORT])
-AC_DEFUN([AX_SWIG_MULTI_MODULE_SUPPORT],[
-        AC_REQUIRE([AX_PKG_SWIG])
-        SWIG="$SWIG -noruntime"
-])
diff --git a/m4/ax_swig_python.m4 b/m4/ax_swig_python.m4
deleted file mode 100644
index 8fd3df5a8027c51416fac5333f96bc7f4f99a1ab..0000000000000000000000000000000000000000
--- a/m4/ax_swig_python.m4
+++ /dev/null
@@ -1,64 +0,0 @@
-# ===========================================================================
-#      http://www.gnu.org/software/autoconf-archive/ax_swig_python.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-#   AX_SWIG_PYTHON([use-shadow-classes = {no, yes}])
-#
-# DESCRIPTION
-#
-#   Checks for Python and provides the $(AX_SWIG_PYTHON_CPPFLAGS), and
-#   $(AX_SWIG_PYTHON_OPT) output variables.
-#
-#   $(AX_SWIG_PYTHON_OPT) contains all necessary SWIG options to generate
-#   code for Python. Shadow classes are enabled unless the value of the
-#   optional first argument is exactly 'no'. If you need multi module
-#   support (provided by the AX_SWIG_MULTI_MODULE_SUPPORT macro) use
-#   $(AX_SWIG_PYTHON_LIBS) to link against the appropriate library. It
-#   contains the SWIG Python runtime library that is needed by the type
-#   check system for example.
-#
-# LICENSE
-#
-#   Copyright (c) 2008 Sebastian Huber <sebastian-huber@web.de>
-#   Copyright (c) 2008 Alan W. Irwin <irwin@beluga.phys.uvic.ca>
-#   Copyright (c) 2008 Rafael Laboissiere <rafael@laboissiere.net>
-#   Copyright (c) 2008 Andrew Collier <colliera@ukzn.ac.za>
-#
-#   This program is free software; you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by the
-#   Free Software Foundation; either version 2 of the License, or (at your
-#   option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-#   Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License along
-#   with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#   As a special exception, the respective Autoconf Macro's copyright owner
-#   gives unlimited permission to copy, distribute and modify the configure
-#   scripts that are the output of Autoconf when processing the Macro. You
-#   need not follow the terms of the GNU General Public License when using
-#   or distributing such scripts, even though portions of the text of the
-#   Macro appear in them. The GNU General Public License (GPL) does govern
-#   all other use of the material that constitutes the Autoconf Macro.
-#
-#   This special exception to the GPL applies to versions of the Autoconf
-#   Macro released by the Autoconf Archive. When you make and distribute a
-#   modified version of the Autoconf Macro, you may extend this special
-#   exception to the GPL to apply to your modified version as well.
-
-#serial 7
-
-AU_ALIAS([SWIG_PYTHON], [AX_SWIG_PYTHON])
-AC_DEFUN([AX_SWIG_PYTHON],[
-        AC_REQUIRE([AX_PKG_SWIG])
-        AC_REQUIRE([AX_PYTHON_DEVEL])
-        test "x$1" != "xno" || swig_shadow=" -noproxy"
-        AC_SUBST([AX_SWIG_PYTHON_OPT],[-python$swig_shadow])
-        AC_SUBST([AX_SWIG_PYTHON_CPPFLAGS],[$PYTHON_CPPFLAGS])
-])
diff --git a/src/Makefile.am b/src/Makefile.am
index a092d0b1400f249e31d27e92bb4bc73367287858..4e52d79e7a429fa5a9d89aea7e06d15d4dd38c19 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -7,9 +7,7 @@ if ENABLE_PAM_SSO
 PAM_SSO_SUBDIR = pam_sso
 endif
 
-if SWIG
 PYTHON_SUBDIR = python sso_server
-endif
 
 SUBDIRS = \
 	sso \
diff --git a/src/python/Makefile.am b/src/python/Makefile.am
index 361b5e3ded5bdd34183be6edf840e5e3b8f124d1..836b840cd8336c3b4092be0fbc8a4076401260b5 100644
--- a/src/python/Makefile.am
+++ b/src/python/Makefile.am
@@ -15,10 +15,10 @@ check-local:
 	env \
 	    LD_LIBRARY_PATH=$(top_builddir)/src/sso/.libs \
 	    DYLD_LIBRARY_PATH=$(top_builddir)/src/sso/.libs \
-	    nosetests -v
+	    $(NOSETESTS) -v
 
 clean-local:
-	-rm -f sso/sso_c.py sso/sso_wrap.cpp $(wildcard sso/_sso_c.*so)
+	-rm -f $(wildcard sso/_pysso*.so)
 	$(PYTHON_SETUP_PY) clean --all
 
 install-exec-local:
diff --git a/src/python/setup.py b/src/python/setup.py
index fb077259c90c52e716915755a955ef5ef4aa4dbf..8646dfdc05645982fb6b398e22a8808630577fbe 100644
--- a/src/python/setup.py
+++ b/src/python/setup.py
@@ -10,7 +10,7 @@ top_srcdir = os.path.join(SRCDIR, '..')
 
 # If BUILD_MODULE is not defined, bundle libsso into the Python
 # extension so that this package is pip-installable directly.
-ext_sources = ['sso/sso.i']
+ext_sources = ['sso/sso_python.c']
 libraries = ['sso']
 if not os.getenv('BUILD_MODULE'):
     ext_sources.extend(os.path.join(top_srcdir, 'sso', x) for x in [
@@ -28,8 +28,8 @@ class build_py(_build_py):
 
 setup(name='sso',
       cmdclass={'build_py': build_py},
-      ext_modules=[Extension('sso._sso_c', ext_sources,
-                             swig_opts=['-modern', '-I' + top_srcdir, '-py3'],
+      ext_modules=[Extension('sso._pysso', ext_sources,
+                             #swig_opts=['-modern', '-I' + top_srcdir, '-py3'],
                              include_dirs=[top_srcdir],
                              libraries=libraries,
                              library_dirs=[SSO_LIB_DIR],
diff --git a/src/python/sso/.gitignore b/src/python/sso/.gitignore
index 7c49738690cda3d6aad46f84549be6c0af6cc2ce..140f8cf80f2c88e66c141b1c4074b92b29fde4e6 100644
--- a/src/python/sso/.gitignore
+++ b/src/python/sso/.gitignore
@@ -1,3 +1 @@
-_sso_c.so
-sso_c.py
-sso_wrap.c
+*.so
diff --git a/src/python/sso/__init__.py b/src/python/sso/__init__.py
index fabd5015b51c8419fbafb67e1cd725422d689174..9015c1f85b9c91968b4381fd822714bee949d3a6 100644
--- a/src/python/sso/__init__.py
+++ b/src/python/sso/__init__.py
@@ -1,67 +1,52 @@
-# Python 3 compatibility for basestring check as used in isinstance() below.
-try:
-    basestring
-except NameError:
-    basestring = (str, bytes)
-
-from .sso_c import sso_generate_keys as generate_keys
-from .sso_c import sso_ticket_new, sso_ticket_free, \
-     sso_ticket_sign, sso_ticket_open, sso_strerror, \
-     sso_validate
-
-
-class Error(Exception):
-
-    def __init__(self, code):
-        self.code = code
-        Exception.__init__(self, sso_strerror(code))
+import time
+from ._pysso import generate_keys, create_and_sign_ticket, verify_ticket, error
 
+Error = error
 
+# Ticket is just a simple container for ticket attributes.
 class Ticket(object):
 
-    def __init__(self, tkt_or_user=None, service=None, domain=None,
-                 nonce=None, groups=None, ttl=3600):
-        # Fix old constructor behavior with positional args.
-        if isinstance(groups, int):
-            ttl = groups
-            groups = None
-        if tkt_or_user is None or isinstance(tkt_or_user, basestring):
-            self._tkt = sso_ticket_new(
-                tkt_or_user, service, domain, nonce, groups, ttl)
-        else:
-            self._tkt = tkt_or_user
-
-    def __del__(self):
-        if hasattr(self, '_tkt'):
-            sso_ticket_free(self._tkt)
+    def __init__(self, user, service, domain, nonce=None, groups=None, ttl=3600, expires=None):
+        self._user = user
+        self._service = service
+        self._domain = domain
+        self._nonce = nonce
+        self._groups = groups
+        # 'expires' has precedence over 'ttl'.
+        if expires is None:
+            expires = int(time.time()) + ttl
+        self._expires = expires
 
     def __eq__(self, t2):
         return (self.user() == t2.user() and
                 self.service() == t2.service() and
                 self.domain() == t2.domain() and
-                self.expires() == t2.expires())
+                self.nonce() == t2.nonce())
 
     def empty(self):
-        return (not self._tkt.user and not self._tkt.domain
-                and not self._tkt.service)
+        return (not self._user and not self._domain
+                and not self._service)
 
     def user(self):
-        return self._tkt.user
+        return self._user
 
     def domain(self):
-        return self._tkt.domain
+        return self._domain
 
     def service(self):
-        return self._tkt.service
+        return self._service
 
     def nonce(self):
-        return self._tkt.nonce
+        return self._nonce
 
     def groups(self):
-        return self._tkt.groups
+        return self._groups
 
     def expires(self):
-        return self._tkt.expires
+        return self._expires
+
+    def ttl(self):
+        return int(self._expires - time.time())
 
 
 class Signer(object):
@@ -70,10 +55,15 @@ class Signer(object):
         self._sk = sk
 
     def sign(self, t):
-        r, signed = sso_ticket_sign(t._tkt, self._sk)
-        if r != 0:
-            raise Error(r)
-        return signed
+        return create_and_sign_ticket(
+            self._sk,
+            t.user(),
+            t.service(),
+            t.domain(),
+            t.nonce(),
+            t.groups(),
+            t.ttl(),
+        )
 
 
 class Verifier(object):
@@ -85,10 +75,12 @@ class Verifier(object):
         self._ok_groups = ok_groups
 
     def verify(self, encoded_tkt, nonce=None):
-        r, tkt = sso_ticket_open(encoded_tkt, self._pk)
-        if r != 0:
-            raise Error(r)
-        r = sso_validate(tkt, self._service, self._domain, nonce, self._ok_groups)
-        if r != 0:
-            raise Error(r)
-        return Ticket(tkt)
+        result = verify_ticket(self._pk, encoded_tkt, self._service, self._domain, nonce, self._ok_groups)
+        return Ticket(
+            result['user'],
+            self._service,
+            self._domain,
+            nonce,
+            result['groups'],
+            result['expires'],
+        )
diff --git a/src/python/sso/py3compat.h b/src/python/sso/py3compat.h
new file mode 100644
index 0000000000000000000000000000000000000000..84bb465d02c621f6dc9d1d0223b711575bfee4ef
--- /dev/null
+++ b/src/python/sso/py3compat.h
@@ -0,0 +1,191 @@
+/*
+   Unix SMB/CIFS implementation.
+   Python 3 compatibility macros
+   Copyright (C) Petr Viktorin <pviktori@redhat.com> 2015
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef _SAMBA_PY3COMPAT_H_
+#define _SAMBA_PY3COMPAT_H_
+#include <Python.h>
+
+/* Quick docs:
+ *
+ * The IS_PY3 preprocessor constant is 1 on Python 3, and 0 on Python 2.
+ *
+ * "PyStr_*" works like PyUnicode_* on Python 3, but uses bytestrings (str)
+ * under Python 2.
+ *
+ * "PyBytes_*" work like in Python 3; on Python 2 they are aliased to their
+ * PyString_* names.
+ *
+ * "PyInt_*" works like PyLong_*
+ *
+ * Syntax for module initialization is as in Python 3, except the entrypoint
+ * function definition and declaration:
+ *     PyMODINIT_FUNC PyInit_modulename(void);
+ *     PyMODINIT_FUNC PyInit_modulename(void)
+ *     {
+ *         ...
+ *     }
+ * is replaced by:
+ *     MODULE_INIT_FUNC(modulename)
+ *     {
+ *         ...
+ *     }
+ *
+ * In the entrypoint, create a module using PyModule_Create and PyModuleDef,
+ * and return it. See Python 3 documentation for details.
+ * For Python 2 compatibility, always set PyModuleDef.m_size to -1.
+ *
+ */
+
+#if PY_MAJOR_VERSION >= 3
+
+/***** Python 3 *****/
+
+#define IS_PY3 1
+
+/* Strings */
+
+#define PyStr_Type PyUnicode_Type
+#define PyStr_Check PyUnicode_Check
+#define PyStr_CheckExact PyUnicode_CheckExact
+#define PyStr_FromString PyUnicode_FromString
+#define PyStr_FromStringAndSize PyUnicode_FromStringAndSize
+#define PyStr_FromFormat PyUnicode_FromFormat
+#define PyStr_FromFormatV PyUnicode_FromFormatV
+#define PyStr_AsString PyUnicode_AsUTF8
+#define PyStr_Concat PyUnicode_Concat
+#define PyStr_Format PyUnicode_Format
+#define PyStr_InternInPlace PyUnicode_InternInPlace
+#define PyStr_InternFromString PyUnicode_InternFromString
+#define PyStr_Decode PyUnicode_Decode
+
+#define PyStr_AsUTF8String PyUnicode_AsUTF8String // returns PyBytes
+#define PyStr_AsUTF8 PyUnicode_AsUTF8
+#define PyStr_AsUTF8AndSize PyUnicode_AsUTF8AndSize
+
+/* description of bytes and string objects */
+#define PY_DESC_PY3_BYTES "bytes"
+#define PY_DESC_PY3_STRING "string"
+
+/* Determine if object is really bytes, for code that runs
+ * in python2 & python3 (note: PyBytes_Check is replaced by
+ * PyString_Check in python2) so care needs to be taken when
+ * writing code that will check if incoming type is bytes that
+ * will work as expected in python2 & python3
+ */
+
+#define IsPy3Bytes PyBytes_Check
+
+#define IsPy3BytesOrString(pystr) \
+    (PyStr_Check(pystr) || PyBytes_Check(pystr))
+
+#else
+
+/***** Python 2 *****/
+
+#define IS_PY3 0
+
+/* Strings */
+
+#define PyStr_Type PyString_Type
+#define PyStr_Check PyString_Check
+#define PyStr_CheckExact PyString_CheckExact
+#define PyStr_FromString PyString_FromString
+#define PyStr_FromStringAndSize PyString_FromStringAndSize
+#define PyStr_FromFormat PyString_FromFormat
+#define PyStr_FromFormatV PyString_FromFormatV
+#define PyStr_AsString PyString_AsString
+#define PyStr_Format PyString_Format
+#define PyStr_InternInPlace PyString_InternInPlace
+#define PyStr_InternFromString PyString_InternFromString
+#define PyStr_Decode PyString_Decode
+
+#define PyStr_AsUTF8String(str) (Py_INCREF(str), (str))
+#define PyStr_AsUTF8 PyString_AsString
+#define PyStr_AsUTF8AndSize(pystr, sizeptr) \
+    ((*sizeptr=PyString_Size(pystr)), PyString_AsString(pystr))
+
+#define PyBytes_Type PyString_Type
+#define PyBytes_Check PyString_Check
+#define PyBytes_CheckExact PyString_CheckExact
+#define PyBytes_FromString PyString_FromString
+#define PyBytes_FromStringAndSize PyString_FromStringAndSize
+#define PyBytes_FromFormat PyString_FromFormat
+#define PyBytes_FromFormatV PyString_FromFormatV
+#define PyBytes_Size PyString_Size
+#define PyBytes_GET_SIZE PyString_GET_SIZE
+#define PyBytes_AsString PyString_AsString
+#define PyBytes_AS_STRING PyString_AS_STRING
+#define PyBytes_AsStringAndSize PyString_AsStringAndSize
+#define PyBytes_Concat PyString_Concat
+#define PyBytes_ConcatAndDel PyString_ConcatAndDel
+#define _PyBytes_Resize _PyString_Resize
+
+/* description of bytes and string objects */
+#define PY_DESC_PY3_BYTES "string"
+#define PY_DESC_PY3_STRING "unicode"
+
+/* Determine if object is really bytes, for code that runs
+ * in python2 & python3 (note: PyBytes_Check is replaced by
+ * PyString_Check in python2) so care needs to be taken when
+ * writing code that will check if incoming type is bytes that
+ * will work as expected in python2 & python3
+ */
+
+#define IsPy3Bytes(pystr) false
+
+#define IsPy3BytesOrString PyStr_Check
+
+/* PyArg_ParseTuple/Py_BuildValue argument */
+
+#define PYARG_BYTES_LEN "s#"
+/*
+ * We want a format that will ensure unicode is encoded using the
+ * specified encoding 'utf8' (to obtain the char* array)
+ * In python3 we use "es" but in python2 the specifiying 'es' will
+ * result in the any incomming 'str' type being decoded first to ascii
+ * then encoded to the specified 'utf8' encoding. In order to avoid that
+ * we use format 'et' in python2 instead.
+ */
+#define PYARG_STR_UNI "et"
+
+/* Module init */
+
+#define PyModuleDef_HEAD_INIT 0
+
+typedef struct PyModuleDef {
+    int m_base;
+    const char* m_name;
+    const char* m_doc;
+    Py_ssize_t m_size;
+    PyMethodDef *m_methods;
+} PyModuleDef;
+
+#define PyModule_Create(def) \
+    Py_InitModule3((def)->m_name, (def)->m_methods, (def)->m_doc)
+
+#define MODULE_INIT_FUNC(name) \
+    static PyObject *PyInit_ ## name(void); \
+    void init ## name(void); \
+    void init ## name(void) { PyInit_ ## name(); } \
+    static PyObject *PyInit_ ## name(void)
+
+
+#endif
+
+#endif
diff --git a/src/python/sso/sso.i b/src/python/sso/sso.i
deleted file mode 100644
index aa250ce3221aa064f91ad50e7b107d04960a7aba..0000000000000000000000000000000000000000
--- a/src/python/sso/sso.i
+++ /dev/null
@@ -1,117 +0,0 @@
-%module sso_c
-//%include "typemaps.i"
-
-%{
-#include "sso/sso.h"
-%}
-
-// convert time_t to python ints
-%typemap(out) time_t {
-  $result = PyInt_FromLong((int)$1);
-}
-
-// convert unsigned char * to Python strings.
-%typemap(out) unsigned char * {
-  $result = PyString_FromString((char *)$1);
-}
-
-// convert Python string to unsigned char *
-%typemap(in) unsigned char * {
-  $1 = (unsigned char *)PyString_AsString($input);
-}
-
-// typemaps for groups
-%typemap(typecheck,precedence=SWIG_TYPECHECK_STRING) char ** {
-  PyObject *iter = PyObject_GetIter($input);
-  if (!iter) {
-    $1 = 0;
-  } else {
-    Py_DECREF(iter);
-    $1 = 1;
-  }
- }
-
-// groups as input
-%typemap(in) char ** {
-  if ($input == Py_None) {
-    $1 = NULL;
-  } else {
-    int bufalloc = 8, i = 0;
-    char **buf = (char **)malloc(sizeof(char *)*bufalloc);
-
-    PyObject *iter = PyObject_GetIter($input);
-    PyObject *item;
-    while ((item = PyIter_Next(iter))) {
-      buf[i++] = PyString_AsString(item);
-      if (i >= bufalloc - 1) {
-        bufalloc *= 2;
-        buf = (char **)realloc(buf, sizeof(char *)*bufalloc);
-      }
-      Py_DECREF(item);
-    }
-    buf[i] = NULL;
-    Py_DECREF(iter);
-    $1 = buf;
-  }
-}
-
-%typemap(freearg) char ** {
-  if ($1 != NULL) {
-    free($1);
-  }
-}
-
-// groups as output
-%typemap(out) char ** {
-  if ($1 == NULL) {
-    $result = Py_None;
-  } else {
-    PyObject *set = PySet_New(NULL);
-    for (char **i = $1; *i; i++) {
-      PySet_Add(set, PyString_FromString(*i));
-    }
-    $result = set;
-  }
-}
-
-// special typemaps for sso_generate_keys.
-%typemap(in, numinputs=0) (unsigned char *publicp, unsigned char *secretp) (unsigned char temp_pk[SSO_PUBLIC_KEY_SIZE], unsigned char temp_sk[SSO_SECRET_KEY_SIZE]) {
-  $1 = temp_pk;
-  $2 = temp_sk;
-}
-
-%typemap(argout) (unsigned char *publicp, unsigned char *secretp) {
-  $result = PyTuple_New(2);
-  PyTuple_SetItem($result, 0, PyString_FromStringAndSize((const char *)$1, SSO_PUBLIC_KEY_SIZE));
-  PyTuple_SetItem($result, 1, PyString_FromStringAndSize((const char *)$2, SSO_SECRET_KEY_SIZE));
-}
- 
-// sso_ticket_sign output arg
-%typemap(in, numinputs=0) (char *out, size_t outsz) (char temp[512]) {
-  $1 = temp;
-  $2 = sizeof(temp);
-}
-
-%typemap(argout) (char *out, size_t outsz) {
-  PyObject *temp_tuple = PyTuple_New(2);
-  PyTuple_SetItem(temp_tuple, 0, $result);
-  PyTuple_SetItem(temp_tuple, 1, PyString_FromString($1));
-  $result = temp_tuple;
-}
-
-// sso_ticket_open output arg
-%typemap(in, numinputs=0) sso_ticket_t * (sso_ticket_t temp = NULL) {
-  $1 = &temp;
-}
-
-%typemap(argout) sso_ticket_t * {
-  PyObject *temp_tuple = PyTuple_New(2);
-  PyTuple_SetItem(temp_tuple, 0, $result);
-  PyTuple_SetItem(temp_tuple, 1, SWIG_NewPointerObj(SWIG_as_voidptr(*($1)), SWIGTYPE_p_sso_ticket, 0));
-  $result = temp_tuple;
-}
-
-%include "sso/sso.h"
-
-
-
diff --git a/src/python/sso/sso_python.c b/src/python/sso/sso_python.c
new file mode 100644
index 0000000000000000000000000000000000000000..ec647f328a01dd558818a63cf83703987e343c56
--- /dev/null
+++ b/src/python/sso/sso_python.c
@@ -0,0 +1,241 @@
+#include "sso/sso.h"
+#include "py3compat.h"
+#include <stdlib.h>
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+#define PYSSO_STATIC_SIGN_BUFSIZE 1024
+
+static const char **parse_groups(PyObject *groups_obj) {
+  int bufalloc = 8, i = 0;
+  char **buf = (char **)malloc(sizeof(char *) * bufalloc);
+  PyObject *iter, *item;
+
+  iter = PyObject_GetIter(groups_obj);
+  if (!iter) {
+    return NULL;
+  }
+  while ((item = PyIter_Next(iter))) {
+    buf[i++] = PyStr_AsString(item);
+    if (i >= bufalloc - 1) {
+      bufalloc *= 2;
+      buf = (char **)realloc(buf, sizeof(char *) * bufalloc);
+    }
+    Py_DECREF(item);
+  }
+  buf[i] = NULL;
+  Py_DECREF(iter);
+  return (const char **)buf;
+}
+
+// Convert a list of groups to a Python list. If groups is NULL,
+// returns an empty list.
+static PyObject *groups_to_list(char **groups) {
+  PyObject *groups_obj;
+  char **p;
+  Py_ssize_t n = 0;
+  int i;
+
+  for (p = groups; p && *p; p++) {
+    n++;
+  }
+
+  groups_obj = PyList_New(n);
+  for (i = 0; i < n; i++) {
+    PyList_SET_ITEM(groups_obj, i, PyStr_FromString(groups[i]));
+  }
+  return groups_obj;
+}
+
+static PyObject *pysso_error;
+
+static void pysso_set_error(int err) {
+  PyErr_SetString(pysso_error, sso_strerror(err));
+}
+
+// create_and_sign_ticket(private_key, user, service, domain, [nonce, groups,
+// ttl])
+static PyObject *pysso_create_and_sign_ticket(PyObject *self, PyObject *args,
+                                              PyObject *kwargs) {
+  const char *priv_key = NULL;
+  Py_ssize_t priv_key_size = 0;
+  const char *user = NULL;
+  const char *service = NULL;
+  const char *domain = NULL;
+  const char *nonce = NULL;
+  PyObject *groups_obj = NULL;
+  const char **groups = NULL;
+  int ttl = 3600;
+  static char *kwlist[] = {"private_key", "user",   "service", "domain",
+                           "nonce",       "groups", "ttl",     NULL};
+
+  PyObject *result_obj = NULL;
+  sso_ticket_t ticket = NULL;
+  char buf[PYSSO_STATIC_SIGN_BUFSIZE]; /* on-stack buffer for signed output */
+  int err;
+
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs,
+#if IS_PY3
+                                   "y#sss|zOi:create_and_sign_ticket",
+#else
+                                   "s#sss|zOi:create_and_sign_ticket",
+#endif
+                                   kwlist, &priv_key, &priv_key_size, &user,
+                                   &service, &domain, &nonce, &groups_obj,
+                                   &ttl)) {
+    return NULL;
+  }
+
+  if (priv_key_size != SSO_SECRET_KEY_SIZE) {
+    PyErr_SetString(PyExc_ValueError, "Bad secret key size");
+    goto fail;
+  }
+
+  if (groups_obj && groups_obj != Py_None) {
+    groups = parse_groups(groups_obj);
+  }
+
+  ticket = sso_ticket_new(user, service, domain, nonce, groups, ttl);
+  if (!ticket) {
+    PyErr_SetString(PyExc_ValueError, "Invalid ticket parameters");
+    goto fail;
+  }
+
+  err = sso_ticket_sign(ticket, (const unsigned char *)priv_key, buf,
+                        sizeof(buf));
+  if (err != SSO_OK) {
+    pysso_set_error(err);
+    goto fail;
+  }
+
+  result_obj = PyStr_FromString(buf);
+
+fail:
+  if (groups) {
+    free(groups);
+  }
+  if (ticket) {
+    sso_ticket_free(ticket);
+  }
+  return result_obj;
+}
+
+// verify_ticket(public_key, ticket, service, domain, [nonce, groups])
+static PyObject *pysso_verify_ticket(PyObject *self, PyObject *args,
+                                     PyObject *kwargs) {
+  const char *pub_key = NULL;
+  Py_ssize_t pub_key_size = 0;
+  const char *serialized_ticket = NULL;
+  const char *service = NULL;
+  const char *domain = NULL;
+  const char *nonce = NULL;
+  PyObject *groups_obj = NULL;
+  const char **groups = NULL;
+  static char *kwlist[] = {"public_key", "ticket", "service", "domain",
+                           "nonce",      "groups", NULL};
+
+  PyObject *result_obj = NULL;
+  sso_ticket_t ticket = NULL;
+  int err;
+
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs,
+#if IS_PY3
+                                   "y#sss|zO:verify_ticket",
+#else
+                                   "s#sss|zO:verify_ticket",
+#endif
+                                   kwlist, &pub_key, &pub_key_size,
+                                   &serialized_ticket, &service, &domain,
+                                   &nonce, &groups_obj)) {
+    return NULL;
+  }
+
+  if (pub_key_size != SSO_PUBLIC_KEY_SIZE) {
+    PyErr_SetString(PyExc_ValueError, "Bad public key size");
+    goto fail;
+  }
+
+  if (groups_obj && groups_obj != Py_None) {
+    groups = parse_groups(groups_obj);
+  }
+
+  err = sso_ticket_open(&ticket, serialized_ticket,
+                        (const unsigned char *)pub_key);
+  if (err != SSO_OK) {
+    pysso_set_error(err);
+    goto fail;
+  }
+
+  err = sso_validate(ticket, service, domain, nonce, groups);
+  if (err != SSO_OK) {
+    pysso_set_error(err);
+    goto fail;
+  }
+
+  // Build a dictionary with the ticket values.
+  result_obj = PyDict_New();
+  PyDict_SetItem(result_obj, PyStr_FromString("user"),
+                 PyStr_FromString(ticket->user));
+  PyDict_SetItem(result_obj, PyStr_FromString("expires"),
+                 PyLong_FromLong(ticket->expires));
+  PyDict_SetItem(result_obj, PyStr_FromString("groups"),
+                 groups_to_list(ticket->groups));
+
+fail:
+  if (groups) {
+    free(groups);
+  }
+  if (ticket) {
+    sso_ticket_free(ticket);
+  }
+  return result_obj;
+}
+
+// generate_keys()
+static PyObject *pysso_generate_keys(PyObject *self, PyObject *args) {
+  unsigned char pubkey[SSO_PUBLIC_KEY_SIZE];
+  unsigned char privkey[SSO_SECRET_KEY_SIZE];
+  int err;
+  PyObject *pub, *priv;
+
+  err = sso_generate_keys(pubkey, privkey);
+  if (err != SSO_OK) {
+    pysso_set_error(err);
+    return NULL;
+  }
+
+  pub = PyBytes_FromStringAndSize((const char *)pubkey, SSO_PUBLIC_KEY_SIZE);
+  priv = PyBytes_FromStringAndSize((const char *)privkey, SSO_SECRET_KEY_SIZE);
+  return Py_BuildValue("OO", pub, priv);
+}
+
+static PyMethodDef pysso_methods[] = {
+    /* The cast of the function is necessary since PyCFunction values
+     * only take two PyObject* parameters, and keywdarg_parrot() takes
+     * three.
+     */
+    {"create_and_sign_ticket", (PyCFunction)pysso_create_and_sign_ticket,
+     METH_VARARGS | METH_KEYWORDS, "Sign a new SSO ticket."},
+    {"verify_ticket", (PyCFunction)pysso_verify_ticket,
+     METH_VARARGS | METH_KEYWORDS, "Verify a SSO ticket."},
+    {"generate_keys", (PyCFunction)pysso_generate_keys, METH_VARARGS,
+     "Generate a new SSO keypair."},
+    {NULL, NULL, 0, NULL}};
+
+static struct PyModuleDef pysso_module = {PyModuleDef_HEAD_INIT, "_pysso", NULL,
+                                          -1, pysso_methods};
+
+MODULE_INIT_FUNC(_pysso) {
+  PyObject *m;
+
+  m = PyModule_Create(&pysso_module);
+  if (!m) {
+    return NULL;
+  }
+
+  pysso_error = PyErr_NewException("_pysso.error", NULL, NULL);
+  Py_INCREF(pysso_error);
+  PyModule_AddObject(m, "error", pysso_error);
+  return m;
+}
diff --git a/src/python/test/signer_test.py b/src/python/test/signer_test.py
index 990fc9b056f0c803c6f53c0bbf32742603aaffd2..af413f7c9707b0629361e344592090054ddbd3d1 100644
--- a/src/python/test/signer_test.py
+++ b/src/python/test/signer_test.py
@@ -11,7 +11,7 @@ class SignerTest(unittest.TestCase):
 
     def _signok(self, tkt):
         signed = self.s.sign(tkt)
-        self.assertTrue(isinstance(signed, bytes))
+        self.assertTrue(isinstance(signed, str))
         self.assertGreater(len(signed), 0)
         #self.assertTrue('user' in base64.urlsafe_b64decode(signed))
 
@@ -32,9 +32,9 @@ class SignerTest(unittest.TestCase):
     def test_sign_ticket_with_nonce(self):
         self._signok(sso.Ticket('user', 'service', 'domain', nonce='testnonce'))
 
-    def test_sign_fail_with_empty_ticket(self):
-        tkt = sso.Ticket()
-        self.assertRaises(sso.Error, self.s.sign, tkt)
+    # def test_sign_fail_with_empty_ticket(self):
+    #     tkt = sso.Ticket()
+    #     self.assertRaises(sso.Error, self.s.sign, tkt)
 
     def test_sign_fail_with_invalid_ticket(self):
         tkt = sso.Ticket('|', 'service', 'domain')
diff --git a/src/python/test/ticket_test.py b/src/python/test/ticket_test.py
index 8631c47575f2697ef7bb434eeac6931ee580c376..6c81985324b9f1276070374fad538862afedcc12 100644
--- a/src/python/test/ticket_test.py
+++ b/src/python/test/ticket_test.py
@@ -15,9 +15,9 @@ class TicketTest(unittest.TestCase):
         self.assertTrue(isinstance(expires, int))
         self.assertTrue(expires > 0)
 
-    def test_create_empty_ticket(self):
-        tkt = sso.Ticket()
-        self.assertTrue(tkt.empty())
+    # def test_create_empty_ticket(self):
+    #     tkt = sso.Ticket()
+    #     self.assertTrue(tkt.empty())
 
     def test_create_ticket_with_groups(self):
         groups = set(['g1', 'g2'])
diff --git a/src/python/test/verifier_test.py b/src/python/test/verifier_test.py
index cc95e26554c0d5b0079990aebb7d45c32b8cc935..a23f9b6d724e0599f90509a5b1a68b9ee0d7ca73 100644
--- a/src/python/test/verifier_test.py
+++ b/src/python/test/verifier_test.py
@@ -50,12 +50,12 @@ class VerifierTest(unittest.TestCase):
         self.assertRaises(sso.Error, v.verify, signed, 'bad_nonce')
 
     def test_verify_fail_too_short(self):
-        signed = base64.b64encode('some data')
+        signed = base64.b64encode(b'some data').decode()
         v = sso.Verifier(self.public, 'service/', 'domain', self.groups)
         self.assertRaises(sso.Error, v.verify, signed)
 
     def test_verify_fail_not_signed(self):
-        signed = base64.b64encode('some data' * 10)
+        signed = base64.b64encode(b'some data' * 10).decode()
         v = sso.Verifier(self.public, 'service/', 'domain', self.groups)
         self.assertRaises(sso.Error, v.verify, signed)
 
diff --git a/src/sso_server/sso_server/test/sso_server_test.py b/src/sso_server/sso_server/test/sso_server_test.py
index e52f8751f59db8b032f2484cceb6b8734bd69a45..5fd44ca3e7ee90aab2e70d6db4371cc6e16fe530 100644
--- a/src/sso_server/sso_server/test/sso_server_test.py
+++ b/src/sso_server/sso_server/test/sso_server_test.py
@@ -48,7 +48,7 @@ class SSOServerTest(SSOServerTestBase):
             cookies.update(parse_cookie(x))
         return cookies
 
-    def _check_ticket(self, query_string, expected_d, expected_s):
+    def _check_ticket(self, query_string, expected_d, expected_s, nonce=None):
         # Check the query string arguments.
         args = urldecode(query_string)
         self.assertEquals(expected_d, args.get('d'))
@@ -57,9 +57,8 @@ class SSOServerTest(SSOServerTestBase):
 
         # Check that we got a valid ticket for the remote service.
         vrfy = sso.Verifier(self.app.login_service.public_key,
-                            expected_s,
-                            self.domain)
-        return vrfy.verify(args['t'])
+                            expected_s, self.domain)
+        return vrfy.verify(args['t'], nonce)
 
     def test_login_ok_with_cookie(self):
         # Test that we can login successfully if we already have a local
@@ -226,7 +225,8 @@ class SSOServerTest(SSOServerTestBase):
             path, qs = location.split('?', 1)
             self.assertEquals('https://someservice.org/sso_login', path)
             ticket = self._check_ticket(
-                qs, 'https://someservice.org/dest', 'someservice.org/')
+                qs, 'https://someservice.org/dest', 'someservice.org/',
+                'testnonce')
             self.assertEquals('admin', ticket.user())
             self.assertEquals('testnonce', ticket.nonce())