summaryrefslogtreecommitdiff
path: root/repoze/bfg/authentication.py
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-10-23 20:02:14 +0000
committerChris McDonough <chrism@agendaless.com>2009-10-23 20:02:14 +0000
commit839ea015f9bc8c8096107e700a42bb872e9dc0c8 (patch)
treeb93b7a9ae7fc3842f633c254740a5ce7f6208e53 /repoze/bfg/authentication.py
parent9cb00f863f0c23f00f232b495c6829a9adda8432 (diff)
downloadpyramid-839ea015f9bc8c8096107e700a42bb872e9dc0c8.tar.gz
pyramid-839ea015f9bc8c8096107e700a42bb872e9dc0c8.tar.bz2
pyramid-839ea015f9bc8c8096107e700a42bb872e9dc0c8.zip
- Added ``max_age`` parameter to ``authtktauthenticationpolicy`` ZCML
directive. If this value is set, it must be an integer representing the number of seconds which the auth tkt cookie will survive. Mainly, its existence allows the auth_tkt cookie to survive across browser sessions. - The ``reissue_time`` argument to the ``authtktauthenticationpolicy`` ZCML directive now actually works. When it is set to an integer value, an authticket set-cookie header is appended to the response whenever a request requires authentication and 'now' minus the authticket's timestamp is greater than ``reissue_time`` seconds. - The router now checks for a ``global_response_headers`` attribute of the request object before returning a response. If this value exists, it is presumed to be a sequence of two-tuples, representing a set of headers to append to the 'normal' response headers. This feature is internal, rather than exposed internally, because it's unclear whether it will stay around in the long term. It was added to support the ``reissue_time`` feature of the authtkt authentication policy. - The ``authtkt`` authentication policy ``remember`` method now no longer honors ``token`` or ``userdata`` keyword arguments.
Diffstat (limited to 'repoze/bfg/authentication.py')
-rw-r--r--repoze/bfg/authentication.py207
1 files changed, 115 insertions, 92 deletions
diff --git a/repoze/bfg/authentication.py b/repoze/bfg/authentication.py
index 5fa653a74..e88952f86 100644
--- a/repoze/bfg/authentication.py
+++ b/repoze/bfg/authentication.py
@@ -10,6 +10,7 @@ from zope.interface import implements
from repoze.bfg.interfaces import IAuthenticationPolicy
+from repoze.bfg.request import add_global_response_headers
from repoze.bfg.security import Authenticated
from repoze.bfg.security import Everyone
@@ -173,7 +174,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
expected to return None if the userid doesn't exist or a sequence
of group identifiers (possibly empty) if the user does exist. If
``callback`` is None, the userid will be assumed to exist with no
- groups.
+ groups. Optional.
``cookie_name``
@@ -192,16 +193,39 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
``timeout``
- Default: ``None``. Maximum age in seconds allowed for a cookie
- to live. If ``timeout`` is specified, you must also set
- ``reissue_time`` to a lower value.
+ Default: ``None``. Maximum number of seconds after which a
+ newly issued ticket will be considered valid. After this
+ amount of time, the ticket will expire (effectively logging the
+ user out). If this value is ``None``, the token never expires.
+ Optional.
``reissue_time``
- Default: ``None``. If ``reissue_time`` is specified, when we
- encounter a cookie that is older than the reissue time (in
- seconds), but younger that the ``timeout``, a new cookie will
- be issued.
+ Default: ``None``. If this parameter is set, it represents the
+ number of seconds that must pass before an authentication token
+ cookie is reissued. The duration is measured as the number of
+ seconds since the last auth_tkt cookie was issued and 'now'.
+ If the ``timeout`` value is ``None``, this parameter has no
+ effect. If this parameter is provided, and the value of
+ ``timeout`` is not ``None``, the value of ``reissue_time`` must
+ be smaller than value of ``timeout``. A good rule of thumb: if
+ you want auto-reissued cookies: set this to the ``timeout``
+ value divided by ten. If this value is ``0``, a new ticket
+ cookie will be reissued on every request which needs
+ authentication. Optional.
+
+ ``max_age``
+
+ Default: ``None``. The max age of the auth_tkt cookie, in
+ seconds. This differs from ``timeout`` inasmuch as ``timeout``
+ represents the lifetime of the ticket contained in the cookie,
+ while this value represents the lifetime of the cookie itself.
+ When this value is set, the cookie's ``Max-Age`` and ``Expires``
+ settings will be set, allowing the auth_tkt cookie to last
+ between browser sessions. It is typically nonsenical to set
+ this to a value that is lower than ``timeout`` or
+ ``reissue_time``, although it is not explicitly prevented.
+ Optional.
"""
implements(IAuthenticationPolicy)
def __init__(self,
@@ -211,7 +235,8 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
secure=False,
include_ip=False,
timeout=None,
- reissue_time=None):
+ reissue_time=None,
+ max_age=None):
self.cookie = AuthTktCookieHelper(
secret,
cookie_name=cookie_name,
@@ -219,6 +244,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
include_ip=include_ip,
timeout=timeout,
reissue_time=reissue_time,
+ max_age=max_age,
)
self.callback = callback
@@ -228,38 +254,77 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
return result['userid']
def remember(self, request, principal, **kw):
- """ Accepts the following kw args: ``tokens``, ``userdata``,
- ``max_age``."""
+ """ Accepts the following kw args: ``max_age``."""
return self.cookie.remember(request, principal, **kw)
def forget(self, request):
return self.cookie.forget(request)
-
+
+def b64encode(v):
+ return v.encode('base64').strip().replace('\n', '')
+
+def b64decode(v):
+ return v.decode('base64')
+
+EXPIRE = object()
+
class AuthTktCookieHelper(object):
+ auth_tkt = auth_tkt # for tests
+
userid_type_decoders = {
'int':int,
- 'unicode':lambda x: utf_8_decode(x)[0],
+ 'unicode':lambda x: utf_8_decode(x)[0], # bw compat for old cookies
+ 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0],
+ 'b64str': lambda x: b64decode(x),
}
userid_type_encoders = {
int: ('int', str),
long: ('int', str),
- unicode: ('unicode', lambda x: utf_8_encode(x)[0]),
+ unicode: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])),
+ str: ('b64str', lambda x: b64encode(x)),
}
def __init__(self, secret, cookie_name='auth_tkt', secure=False,
- include_ip=False, timeout=None, reissue_time=None):
+ include_ip=False, timeout=None, reissue_time=None,
+ max_age=None):
self.secret = secret
self.cookie_name = cookie_name
self.include_ip = include_ip
self.secure = secure
- if timeout and ( (not reissue_time) or (reissue_time > timeout) ):
- raise ValueError('When timeout is specified, reissue_time must '
- 'be set to a lower value')
self.timeout = timeout
+ if reissue_time is not None and timeout is not None:
+ if reissue_time > timeout:
+ raise ValueError('reissue_time must be lower than timeout')
self.reissue_time = reissue_time
+ self.max_age = max_age
+
+ def _get_cookies(self, environ, value, max_age=None):
+ if max_age is EXPIRE:
+ max_age = "; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT"
+ elif max_age is not None:
+ later = datetime.datetime.utcnow() + datetime.timedelta(
+ seconds=int(max_age))
+ # Wdy, DD-Mon-YY HH:MM:SS GMT
+ expires = later.strftime('%a, %d %b %Y %H:%M:%S GMT')
+ # the Expires header is *required* at least for IE7 (IE7 does
+ # not respect Max-Age)
+ max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires)
+ else:
+ max_age = ''
+
+ cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
+ wild_domain = '.' + cur_domain
+ cookies = [
+ ('Set-Cookie', '%s="%s"; Path=/%s' % (
+ self.cookie_name, value, max_age)),
+ ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % (
+ self.cookie_name, value, cur_domain, max_age)),
+ ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % (
+ self.cookie_name, value, wild_domain, max_age))
+ ]
+ return cookies
- # IIdentifier
def identify(self, request):
environ = request.environ
cookies = get_cookies(environ)
@@ -274,12 +339,14 @@ class AuthTktCookieHelper(object):
remote_addr = '0.0.0.0'
try:
- timestamp, userid, tokens, user_data = auth_tkt.parse_ticket(
+ timestamp, userid, tokens, user_data = self.auth_tkt.parse_ticket(
self.secret, cookie.value, remote_addr)
- except auth_tkt.BadTicket:
+ except self.auth_tkt.BadTicket:
return None
- if self.timeout and ( (timestamp + self.timeout) < time.time() ):
+ now = time.time()
+
+ if self.timeout and ( (timestamp + self.timeout) < now ):
return None
userid_typename = 'userid_type:'
@@ -290,7 +357,15 @@ class AuthTktCookieHelper(object):
decoder = self.userid_type_decoders.get(userid_type)
if decoder:
userid = decoder(userid)
+
+ reissue = self.reissue_time is not None
+ if not hasattr(request, '_authtkt_reissued'):
+ if reissue and ( (now - timestamp) > self.reissue_time):
+ headers = self.remember(request, userid, max_age=self.max_age)
+ add_global_response_headers(request, headers)
+ request._authtkt_reissued = True
+
environ['REMOTE_USER_TOKENS'] = tokens
environ['REMOTE_USER_DATA'] = user_data
environ['AUTH_TYPE'] = 'cookie'
@@ -302,90 +377,38 @@ class AuthTktCookieHelper(object):
identity['userdata'] = user_data
return identity
- def _get_cookies(self, environ, value, max_age=None):
- if max_age is not None:
- later = datetime.datetime.now() + datetime.timedelta(
- seconds=int(max_age))
- # Wdy, DD-Mon-YY HH:MM:SS GMT
- expires = later.strftime('%a, %d %b %Y %H:%M:%S')
- # the Expires header is *required* at least for IE7 (IE7 does
- # not respect Max-Age)
- max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires)
- else:
- max_age = ''
-
- cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
- wild_domain = '.' + cur_domain
- cookies = [
- ('Set-Cookie', '%s="%s"; Path=/%s' % (
- self.cookie_name, value, max_age)),
- ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % (
- self.cookie_name, value, cur_domain, max_age)),
- ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % (
- self.cookie_name, value, wild_domain, max_age))
- ]
- return cookies
-
- # IIdentifier
def forget(self, request):
# return a set of expires Set-Cookie headers
environ = request.environ
- return self._get_cookies(environ, '""')
+ return self._get_cookies(environ, '', max_age=EXPIRE)
- # IIdentifier
- def remember(self, request, userid, tokens='', userdata='', max_age=None):
+ def remember(self, request, userid, max_age=None):
+ max_age = max_age or self.max_age
environ = request.environ
+
if self.include_ip:
remote_addr = environ['REMOTE_ADDR']
else:
remote_addr = '0.0.0.0'
- cookies = get_cookies(environ)
- old_cookie = cookies.get(self.cookie_name)
- existing = cookies.get(self.cookie_name)
- old_cookie_value = getattr(existing, 'value', None)
-
- timestamp, old_userid, old_tokens, old_userdata = None, '', '', ''
-
- expired = False
-
- if old_cookie_value:
- try:
- (timestamp,old_userid,old_tokens,
- old_userdata) = auth_tkt.parse_ticket(
- self.secret, old_cookie_value, remote_addr)
- now = time.time()
- expired = self.timeout and ((timestamp + self.timeout) < now)
- except auth_tkt.BadTicket:
- expired = False
+ user_data = ''
encoding_data = self.userid_type_encoders.get(type(userid))
if encoding_data:
encoding, encoder = encoding_data
userid = encoder(userid)
- userdata = 'userid_type:%s' % encoding
+ user_data = 'userid_type:%s' % encoding
- if not isinstance(tokens, basestring):
- tokens = ','.join(tokens)
- if not isinstance(old_tokens, basestring):
- old_tokens = ','.join(old_tokens)
- old_data = (old_userid, old_tokens, old_userdata)
- new_data = (userid, tokens, userdata)
-
- if old_data != new_data or expired:
- ticket = auth_tkt.AuthTicket(
- self.secret,
- userid,
- remote_addr,
- tokens=tokens,
- user_data=userdata,
- cookie_name=self.cookie_name,
- secure=self.secure)
- new_cookie_value = ticket.cookie_value()
-
- cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
- wild_domain = '.' + cur_domain
- if old_cookie_value != new_cookie_value:
- # return a set of Set-Cookie headers
- return self._get_cookies(environ, new_cookie_value, max_age)
+ ticket = self.auth_tkt.AuthTicket(
+ self.secret,
+ userid,
+ remote_addr,
+ user_data=user_data,
+ cookie_name=self.cookie_name,
+ secure=self.secure)
+
+ cookie_value = ticket.cookie_value()
+ cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
+ wild_domain = '.' + cur_domain
+ return self._get_cookies(environ, cookie_value, max_age)