diff options
Diffstat (limited to 'repoze/bfg/authentication.py')
| -rw-r--r-- | repoze/bfg/authentication.py | 207 |
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) |
