Chris McDonough
2009-06-17 a6f6dc9a0d7a912a95e75f5f67a1282ddfdabf28
- Add a ``userid_checker`` argument to the auth_tkt identifier plugin,
courtesty of Gustavo Narea.

If ``userid_checker`` is provided, it must be a dotted Python name
that resolves to a function which accepts a userid and returns a
boolean True or False, indicating whether that user exists in a
database. This is a workaround. Due to a design bug in repoze.who,
the only way who can check for user existence is to use one or more
IAuthenticator plugin ``authenticate`` methods. If an
IAuthenticator's ``authenticate`` method returns true, it means that
the user exists. However most IAuthenticator plugins expect *both*
a username and a password, and will return False unconditionally if
both aren't supplied. This means that an authenticator can't be
used to check if the user "only" exists. The identity provided by
an auth_tkt does not contain a password to check against. The
actual design bug in repoze.who is this: when a user presents
credentials from an auth_tkt, he is considered "preauthenticated".
IAuthenticator.authenticate is just never called for a
"preauthenticated" identity, which works fine, but it means that the
user will be considered authenticated even if you deleted the user's
record from whatever database you happen to be using. However, if
you use a userid_checker, you can ensure that a user exists for the
auth_tkt supplied userid. If the userid_checker returns False, the
auth_tkt credentials are considered "no good".


4 files modified
99 ■■■■■ changed files
CHANGES.txt 28 ●●●●● patch | view | raw | blame | history
docs/narr.rst 24 ●●●●● patch | view | raw | blame | history
repoze/who/plugins/auth_tkt.py 11 ●●●●● patch | view | raw | blame | history
repoze/who/plugins/tests/test_authtkt.py 36 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -15,6 +15,34 @@
- One-hundred percent unit test coverage.
- Add ``timeout`` and ``reissue_time`` arguments to the auth_tkt
  identifier plugin, courtesty of Paul Johnston.
- Add a ``userid_checker`` argument to the auth_tkt identifier plugin,
  courtesty of Gustavo Narea.
  If ``userid_checker`` is provided, it must be a dotted Python name
  that resolves to a function which accepts a userid and returns a
  boolean True or False, indicating whether that user exists in a
  database.  This is a workaround.  Due to a design bug in repoze.who,
  the only way who can check for user existence is to use one or more
  IAuthenticator plugin ``authenticate`` methods.  If an
  IAuthenticator's ``authenticate`` method returns true, it means that
  the user exists.  However most IAuthenticator plugins expect *both*
  a username and a password, and will return False unconditionally if
  both aren't supplied.  This means that an authenticator can't be
  used to check if the user "only" exists.  The identity provided by
  an auth_tkt does not contain a password to check against.  The
  actual design bug in repoze.who is this: when a user presents
  credentials from an auth_tkt, he is considered "preauthenticated".
  IAuthenticator.authenticate is just never called for a
  "preauthenticated" identity, which works fine, but it means that the
  user will be considered authenticated even if you deleted the user's
  record from whatever database you happen to be using.  However, if
  you use a userid_checker, you can ensure that a user exists for the
  auth_tkt supplied userid.  If the userid_checker returns False, the
  auth_tkt credentials are considered "no good".
1.0.13 (2009/4/24)
==================
docs/narr.rst
@@ -212,7 +212,7 @@
.. module:: repoze.who.plugins.auth_tkt
.. class:: AuthTktCookiePlugin(secret [, cookie_name='auth_tkt' [, secure=False [, include_ip=False [, timeout=None [, reissue_time=None]]]]])
.. class:: AuthTktCookiePlugin(secret [, secretfile=None, [, cookie_name='auth_tkt' [, secure=False [, include_ip=False [, timeout=None [, reissue_time=None [, userid_checker=None]]]]]]])
  An :class:`AuthTktCookiePlugin` is an ``IIdentifier`` plugin which
  remembers its identity state in a client-side cookie.  This plugin
@@ -231,6 +231,28 @@
  be issued. If *timeout* is specified, you must also set
  *reissue_time* to a lower value.
  If ``userid_checker`` is provided, it must be a dotted Python name
  that resolves to a function which accepts a userid and returns a
  boolean True or False, indicating whether that user exists in a
  database.  This is a workaround.  Due to a design bug in repoze.who,
  the only way who can check for user existence is to use one or more
  IAuthenticator plugin ``authenticate`` methods.  If an
  IAuthenticator's ``authenticate`` method returns true, it means that
  the user exists.  However most IAuthenticator plugins expect *both*
  a username and a password, and will return False unconditionally if
  both aren't supplied.  This means that an authenticator can't be
  used to check if the user "only" exists.  The identity provided by
  an auth_tkt does not contain a password to check against.  The
  actual design bug in repoze.who is this: when a user presents
  credentials from an auth_tkt, he is considered "preauthenticated".
  IAuthenticator.authenticate is just never called for a
  "preauthenticated" identity, which works fine, but it means that the
  user will be considered authenticated even if you deleted the user's
  record from whatever database you happen to be using.  However, if
  you use a userid_checker, you can ensure that a user exists for the
  auth_tkt supplied userid.  If the userid_checker returns False, the
  auth_tkt credentials are considered "no good".
.. note::
   Using the *include_ip* setting for public-facing applications may
   cause problems for some users.  `One study
repoze/who/plugins/auth_tkt.py
@@ -27,7 +27,7 @@
    
    def __init__(self, secret, cookie_name='auth_tkt',
                 secure=False, include_ip=False,
                 timeout=None, reissue_time=None):
                 timeout=None, reissue_time=None, userid_checker=None):
        self.secret = secret
        self.cookie_name = cookie_name
        self.include_ip = include_ip
@@ -37,6 +37,7 @@
                             'be set to a lower value')
        self.timeout = timeout
        self.reissue_time = reissue_time
        self.userid_checker = userid_checker
    # IIdentifier
    def identify(self, environ):
@@ -55,6 +56,9 @@
            timestamp, userid, tokens, user_data = auth_tkt.parse_ticket(
                self.secret, cookie.value, remote_addr)
        except auth_tkt.BadTicket:
            return None
        if self.userid_checker and not self.userid_checker(userid):
            return None
        if self.timeout and ( (timestamp + self.timeout) < time.time() ):
@@ -170,7 +174,9 @@
                include_ip=False,
                timeout=None,
                reissue_time=None,
                userid_checker=None,
               ):
    from repoze.who.utils import resolveDotted
    if (secret is None and secretfile is None):
        raise ValueError("One of 'secret' or 'secretfile' must not be None.")
    if (secret is not None and secretfile is not None):
@@ -184,12 +190,15 @@
        timeout = int(timeout)
    if reissue_time:
        reissue_time = int(reissue_time)
    if userid_checker is not None:
        userid_checker = resolveDotted(userid_checker)
    plugin = AuthTktCookiePlugin(secret,
                                 cookie_name,
                                 _bool(secure),
                                 _bool(include_ip),
                                 timeout,
                                 reissue_time,
                                 userid_checker,
                                 )
    return plugin
repoze/who/plugins/tests/test_authtkt.py
@@ -331,9 +331,45 @@
        self.assertEqual(plugin.timeout, 5)
        self.assertEqual(plugin.reissue_time, 1)
    def test_factory_with_userid_checker(self):
        from repoze.who.plugins.auth_tkt import make_plugin
        plugin = make_plugin(
            'secret',
            userid_checker='repoze.who.plugins.auth_tkt:make_plugin')
        self.assertEqual(plugin.userid_checker, make_plugin)
    def test_timeout_no_reissue(self):
        self.assertRaises(ValueError, self._makeOne, 'userid', timeout=1)
    def test_timeout_lower_than_reissue(self):
        self.assertRaises(ValueError, self._makeOne, 'userid', timeout=1,
                          reissue_time=2)
    def test_identify_with_checker_and_existing_account(self):
        plugin = self._makeOne('secret', userid_checker=dummy_userid_checker)
        val = self._makeTicket(userid='existing')
        environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val})
        result = plugin.identify(environ)
        self.assertEqual(len(result), 4)
        self.assertEqual(result['tokens'], [''])
        self.assertEqual(result['repoze.who.userid'], 'existing')
        self.assertEqual(result['userdata'], 'userdata')
        self.failUnless('timestamp' in result)
        self.assertEqual(environ['REMOTE_USER_TOKENS'], [''])
        self.assertEqual(environ['REMOTE_USER_DATA'],'userdata')
        self.assertEqual(environ['AUTH_TYPE'],'cookie')
    def test_identify_with_checker_and_non_existing_account(self):
        plugin = self._makeOne('secret', userid_checker=dummy_userid_checker)
        val = self._makeTicket(userid='nonexisting')
        environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val})
        original_environ = environ.copy()
        result = plugin.identify(environ)
        self.assertEqual(result, None)
        # The environ must not have been modified, excuding the paste.cookies
        # variable:
        del environ['paste.cookies']
        self.assertEqual(environ, original_environ)
def dummy_userid_checker(userid):
    return userid == 'existing'