1import binascii
2from codecs import utf_8_decode
3from codecs import utf_8_encode
4import hashlib
5import base64
6import re
7import time as time_mod
8
9from zope.interface import implementer
10
11from webob.cookies import CookieProfile
12
13from pyramid.compat import (
14    long,
15    text_type,
16    binary_type,
17    url_unquote,
18    url_quote,
19    bytes_,
20    ascii_native_,
21    native_,
22    )
23
24from pyramid.interfaces import (
25    IAuthenticationPolicy,
26    IDebugLogger,
27    )
28
29from pyramid.security import (
30    Authenticated,
31    Everyone,
32    )
33
34from pyramid.util import strings_differ
35
36VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$")
37
38
39class CallbackAuthenticationPolicy(object):
40    """ Abstract class """
41
42    debug = False
43    callback = None
44
45    def _log(self, msg, methodname, request):
46        logger = request.registry.queryUtility(IDebugLogger)
47        if logger:
48            cls = self.__class__
49            classname = cls.__module__ + '.' + cls.__name__
50            methodname = classname + '.' + methodname
51            logger.debug(methodname + ': ' + msg)
52
53    def _clean_principal(self, princid):
54        if princid in (Authenticated, Everyone):
55            princid = None
56        return princid
57
58    def authenticated_userid(self, request):
59        """ Return the authenticated userid or ``None``.
60
61        If no callback is registered, this will be the same as
62        ``unauthenticated_userid``.
63
64        If a ``callback`` is registered, this will return the userid if
65        and only if the callback returns a value that is not ``None``.
66
67        """
68        debug = self.debug
69        userid = self.unauthenticated_userid(request)
70        if userid is None:
71            debug and self._log(
72                'call to unauthenticated_userid returned None; returning None',
73                'authenticated_userid',
74                request)
75            return None
76        if self._clean_principal(userid) is None:
77            debug and self._log(
78                ('use of userid %r is disallowed by any built-in Pyramid '
79                 'security policy, returning None' % userid),
80                'authenticated_userid',
81                request)
82            return None
83
84        if self.callback is None:
85            debug and self._log(
86                'there was no groupfinder callback; returning %r' % (userid,),
87                'authenticated_userid',
88                request)
89            return userid
90        callback_ok = self.callback(userid, request)
91        if callback_ok is not None: # is not None!
92            debug and self._log(
93                'groupfinder callback returned %r; returning %r' % (
94                    callback_ok, userid),
95                'authenticated_userid',
96                request
97                )
98            return userid
99        debug and self._log(
100            'groupfinder callback returned None; returning None',
101            'authenticated_userid',
102            request
103            )
104
105    def effective_principals(self, request):
106        """ A list of effective principals derived from request.
107
108        This will return a list of principals including, at least,
109        :data:`pyramid.security.Everyone`. If there is no authenticated
110        userid, or the ``callback`` returns ``None``, this will be the
111        only principal:
112
113        .. code-block:: python
114
115            return [Everyone]
116
117        If the ``callback`` does not return ``None`` and an authenticated
118        userid is found, then the principals will include
119        :data:`pyramid.security.Authenticated`, the ``authenticated_userid``
120        and the list of principals returned by the ``callback``:
121
122        .. code-block:: python
123
124            extra_principals = callback(userid, request)
125            return [Everyone, Authenticated, userid] + extra_principals
126
127        """
128        debug = self.debug
129        effective_principals = [Everyone]
130        userid = self.unauthenticated_userid(request)
131
132        if userid is None:
133            debug and self._log(
134                'unauthenticated_userid returned %r; returning %r' % (
135                    userid, effective_principals),
136                'effective_principals',
137                request
138                )
139            return effective_principals
140
141        if self._clean_principal(userid) is None:
142            debug and self._log(
143                ('unauthenticated_userid returned disallowed %r; returning %r '
144                 'as if it was None' % (userid, effective_principals)),
145                'effective_principals',
146                request
147                )
148            return effective_principals
149
150        if self.callback is None:
151            debug and self._log(
152                'groupfinder callback is None, so groups is []',
153                'effective_principals',
154                request)
155            groups = []
156        else:
157            groups = self.callback(userid, request)
158            debug and self._log(
159                'groupfinder callback returned %r as groups' % (groups,),
160                'effective_principals',
161                request)
162
163        if groups is None: # is None!
164            debug and self._log(
165                'returning effective principals: %r' % (
166                    effective_principals,),
167                'effective_principals',
168                request
169                )
170            return effective_principals
171
172        effective_principals.append(Authenticated)
173        effective_principals.append(userid)
174        effective_principals.extend(groups)
175
176        debug and self._log(
177            'returning effective principals: %r' % (
178                effective_principals,),
179            'effective_principals',
180            request
181        )
182        return effective_principals
183
184
185@implementer(IAuthenticationPolicy)
186class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
187    """ A :app:`Pyramid` :term:`authentication policy` which
188    obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the
189    ``repoze.who.identity`` key in the WSGI environment).
190
191    Constructor Arguments
192
193    ``identifier_name``
194
195       Default: ``auth_tkt``.  The :mod:`repoze.who` plugin name that
196       performs remember/forget.  Optional.
197
198    ``callback``
199
200        Default: ``None``.  A callback passed the :mod:`repoze.who` identity
201        and the :term:`request`, expected to return ``None`` if the user
202        represented by the identity doesn't exist or a sequence of principal
203        identifiers (possibly empty) representing groups if the user does
204        exist.  If ``callback`` is None, the userid will be assumed to exist
205        with no group principals.
206
207    Objects of this class implement the interface described by
208    :class:`pyramid.interfaces.IAuthenticationPolicy`.
209    """
210
211    def __init__(self, identifier_name='auth_tkt', callback=None):
212        self.identifier_name = identifier_name
213        self.callback = callback
214
215    def _get_identity(self, request):
216        return request.environ.get('repoze.who.identity')
217
218    def _get_identifier(self, request):
219        plugins = request.environ.get('repoze.who.plugins')
220        if plugins is None:
221            return None
222        identifier = plugins[self.identifier_name]
223        return identifier
224
225    def authenticated_userid(self, request):
226        """ Return the authenticated userid or ``None``.
227
228        If no callback is registered, this will be the same as
229        ``unauthenticated_userid``.
230
231        If a ``callback`` is registered, this will return the userid if
232        and only if the callback returns a value that is not ``None``.
233
234        """
235        identity = self._get_identity(request)
236
237        if identity is None:
238            self.debug and self._log(
239                'repoze.who identity is None, returning None',
240                'authenticated_userid',
241                request)
242            return None
243
244        userid = identity['repoze.who.userid']
245
246        if userid is None:
247            self.debug and self._log(
248                'repoze.who.userid is None, returning None' % userid,
249                'authenticated_userid',
250                request)
251            return None
252
253        if self._clean_principal(userid) is None:
254            self.debug and self._log(
255                ('use of userid %r is disallowed by any built-in Pyramid '
256                 'security policy, returning None' % userid),
257                'authenticated_userid',
258                request)
259            return None
260
261        if self.callback is None:
262            return userid
263
264        if self.callback(identity, request) is not None: # is not None!
265            return userid
266
267    def unauthenticated_userid(self, request):
268        """ Return the ``repoze.who.userid`` key from the detected identity."""
269        identity = self._get_identity(request)
270        if identity is None:
271            return None
272        return identity['repoze.who.userid']
273
274    def effective_principals(self, request):
275        """ A list of effective principals derived from the identity.
276
277        This will return a list of principals including, at least,
278        :data:`pyramid.security.Everyone`. If there is no identity, or
279        the ``callback`` returns ``None``, this will be the only principal.
280
281        If the ``callback`` does not return ``None`` and an identity is
282        found, then the principals will include
283        :data:`pyramid.security.Authenticated`, the ``authenticated_userid``
284        and the list of principals returned by the ``callback``.
285
286        """
287        effective_principals = [Everyone]
288        identity = self._get_identity(request)
289
290        if identity is None:
291            self.debug and self._log(
292                ('repoze.who identity was None; returning %r' %
293                 effective_principals),
294                'effective_principals',
295                request
296                )
297            return effective_principals
298
299        if self.callback is None:
300            groups = []
301        else:
302            groups = self.callback(identity, request)
303
304        if groups is None: # is None!
305            self.debug and self._log(
306                ('security policy groups callback returned None; returning %r' %
307                 effective_principals),
308                'effective_principals',
309                request
310                )
311            return effective_principals
312
313        userid = identity['repoze.who.userid']
314
315        if userid is None:
316            self.debug and self._log(
317                ('repoze.who.userid was None; returning %r' %
318                 effective_principals),
319                'effective_principals',
320                request
321                )
322            return effective_principals
323
324        if self._clean_principal(userid) is None:
325            self.debug and self._log(
326                ('unauthenticated_userid returned disallowed %r; returning %r '
327                 'as if it was None' % (userid, effective_principals)),
328                'effective_principals',
329                request
330                )
331            return effective_principals
332
333        effective_principals.append(Authenticated)
334        effective_principals.append(userid)
335        effective_principals.extend(groups)
336        return effective_principals
337
338    def remember(self, request, userid, **kw):
339        """ Store the ``userid`` as ``repoze.who.userid``.
340
341        The identity to authenticated to :mod:`repoze.who`
342        will contain the given userid as ``userid``, and
343        provide all keyword arguments as additional identity
344        keys. Useful keys could be ``max_age`` or ``userdata``.
345        """
346        identifier = self._get_identifier(request)
347        if identifier is None:
348            return []
349        environ = request.environ
350        identity = kw
351        identity['repoze.who.userid'] = userid
352        return identifier.remember(environ, identity)
353
354    def forget(self, request):
355        """ Forget the current authenticated user.
356
357        Return headers that, if included in a response, will delete the
358        cookie responsible for tracking the current user.
359
360        """
361        identifier = self._get_identifier(request)
362        if identifier is None:
363            return []
364        identity = self._get_identity(request)
365        return identifier.forget(request.environ, identity)
366
367@implementer(IAuthenticationPolicy)
368class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
369    """ A :app:`Pyramid` :term:`authentication policy` which
370    obtains data from the ``REMOTE_USER`` WSGI environment variable.
371
372    Constructor Arguments
373
374    ``environ_key``
375
376        Default: ``REMOTE_USER``.  The key in the WSGI environ which
377        provides the userid.
378
379    ``callback``
380
381        Default: ``None``.  A callback passed the userid and the request,
382        expected to return None if the userid doesn't exist or a sequence of
383        principal identifiers (possibly empty) representing groups if the
384        user does exist.  If ``callback`` is None, the userid will be assumed
385        to exist with no group principals.
386
387    ``debug``
388
389        Default: ``False``.  If ``debug`` is ``True``, log messages to the
390        Pyramid debug logger about the results of various authentication
391        steps.  The output from debugging is useful for reporting to maillist
392        or IRC channels when asking for support.
393
394    Objects of this class implement the interface described by
395    :class:`pyramid.interfaces.IAuthenticationPolicy`.
396    """
397
398    def __init__(self, environ_key='REMOTE_USER', callback=None, debug=False):
399        self.environ_key = environ_key
400        self.callback = callback
401        self.debug = debug
402
403    def unauthenticated_userid(self, request):
404        """ The ``REMOTE_USER`` value found within the ``environ``."""
405        return request.environ.get(self.environ_key)
406
407    def remember(self, request, userid, **kw):
408        """ A no-op. The ``REMOTE_USER`` does not provide a protocol for
409        remembering the user. This will be application-specific and can
410        be done somewhere else or in a subclass."""
411        return []
412
413    def forget(self, request):
414        """ A no-op. The ``REMOTE_USER`` does not provide a protocol for
415        forgetting the user. This will be application-specific and can
416        be done somewhere else or in a subclass."""
417        return []
418
419@implementer(IAuthenticationPolicy)
420class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
421    """A :app:`Pyramid` :term:`authentication policy` which
422    obtains data from a Pyramid "auth ticket" cookie.
423
424    Constructor Arguments
425
426    ``secret``
427
428       The secret (a string) used for auth_tkt cookie signing.  This value
429       should be unique across all values provided to Pyramid for various
430       subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
431       Required.
432
433    ``callback``
434
435       Default: ``None``.  A callback passed the userid and the
436       request, expected to return ``None`` if the userid doesn't
437       exist or a sequence of principal identifiers (possibly empty) if
438       the user does exist.  If ``callback`` is ``None``, the userid
439       will be assumed to exist with no principals.  Optional.
440
441    ``cookie_name``
442
443       Default: ``auth_tkt``.  The cookie name used
444       (string).  Optional.
445
446    ``secure``
447
448       Default: ``False``.  Only send the cookie back over a secure
449       conn.  Optional.
450
451    ``include_ip``
452
453       Default: ``False``.  Make the requesting IP address part of
454       the authentication data in the cookie.  Optional.
455
456       For IPv6 this option is not recommended. The ``mod_auth_tkt``
457       specification does not specify how to handle IPv6 addresses, so using
458       this option in combination with IPv6 addresses may cause an
459       incompatible cookie. It ties the authentication ticket to that
460       individual's IPv6 address.
461
462    ``timeout``
463
464       Default: ``None``.  Maximum number of seconds which a newly
465       issued ticket will be considered valid.  After this amount of
466       time, the ticket will expire (effectively logging the user
467       out).  If this value is ``None``, the ticket never expires.
468       Optional.
469
470    ``reissue_time``
471
472       Default: ``None``.  If this parameter is set, it represents the number
473       of seconds that must pass before an authentication token cookie is
474       automatically reissued as the result of a request which requires
475       authentication.  The duration is measured as the number of seconds
476       since the last auth_tkt cookie was issued and 'now'.  If this value is
477       ``0``, a new ticket cookie will be reissued on every request which
478       requires authentication.
479
480       A good rule of thumb: if you want auto-expired cookies based on
481       inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
482       ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
483       (120 or 2 mins).  It's nonsensical to set the ``timeout`` value lower
484       than the ``reissue_time`` value, as the ticket will never be reissued
485       if so.  However, such a configuration is not explicitly prevented.
486
487       Optional.
488
489    ``max_age``
490
491       Default: ``None``.  The max age of the auth_tkt cookie, in
492       seconds.  This differs from ``timeout`` inasmuch as ``timeout``
493       represents the lifetime of the ticket contained in the cookie,
494       while this value represents the lifetime of the cookie itself.
495       When this value is set, the cookie's ``Max-Age`` and
496       ``Expires`` settings will be set, allowing the auth_tkt cookie
497       to last between browser sessions.  It is typically nonsensical
498       to set this to a value that is lower than ``timeout`` or
499       ``reissue_time``, although it is not explicitly prevented.
500       Optional.
501
502    ``path``
503
504       Default: ``/``. The path for which the auth_tkt cookie is valid.
505       May be desirable if the application only serves part of a domain.
506       Optional.
507
508    ``http_only``
509
510       Default: ``False``. Hide cookie from JavaScript by setting the
511       HttpOnly flag. Not honored by all browsers.
512       Optional.
513
514    ``wild_domain``
515
516       Default: ``True``. An auth_tkt cookie will be generated for the
517       wildcard domain. If your site is hosted as ``example.com`` this
518       will make the cookie available for sites underneath ``example.com``
519       such as ``www.example.com``.
520       Optional.
521
522    ``parent_domain``
523
524       Default: ``False``. An auth_tkt cookie will be generated for the
525       parent domain of the current site. For example if your site is
526       hosted under ``www.example.com`` a cookie will be generated for
527       ``.example.com``. This can be useful if you have multiple sites
528       sharing the same domain. This option supercedes the ``wild_domain``
529       option.
530       Optional.
531
532       This option is available as of :app:`Pyramid` 1.5.
533
534    ``domain``
535
536       Default: ``None``. If provided the auth_tkt cookie will only be
537       set for this domain. This option is not compatible with ``wild_domain``
538       and ``parent_domain``.
539       Optional.
540
541       This option is available as of :app:`Pyramid` 1.5.
542
543    ``hashalg``
544
545       Default: ``sha512`` (the literal string).
546
547       Any hash algorithm supported by Python's ``hashlib.new()`` function
548       can be used as the ``hashalg``.
549
550       Cookies generated by different instances of AuthTktAuthenticationPolicy
551       using different ``hashalg`` options are not compatible. Switching the
552       ``hashalg`` will imply that all existing users with a valid cookie will
553       be required to re-login.
554
555       This option is available as of :app:`Pyramid` 1.4.
556
557       Optional.
558
559    ``debug``
560
561        Default: ``False``.  If ``debug`` is ``True``, log messages to the
562        Pyramid debug logger about the results of various authentication
563        steps.  The output from debugging is useful for reporting to maillist
564        or IRC channels when asking for support.
565
566    Objects of this class implement the interface described by
567    :class:`pyramid.interfaces.IAuthenticationPolicy`.
568    """
569
570    def __init__(self,
571                 secret,
572                 callback=None,
573                 cookie_name='auth_tkt',
574                 secure=False,
575                 include_ip=False,
576                 timeout=None,
577                 reissue_time=None,
578                 max_age=None,
579                 path="/",
580                 http_only=False,
581                 wild_domain=True,
582                 debug=False,
583                 hashalg='sha512',
584                 parent_domain=False,
585                 domain=None,
586                 ):
587        self.cookie = AuthTktCookieHelper(
588            secret,
589            cookie_name=cookie_name,
590            secure=secure,
591            include_ip=include_ip,
592            timeout=timeout,
593            reissue_time=reissue_time,
594            max_age=max_age,
595            http_only=http_only,
596            path=path,
597            wild_domain=wild_domain,
598            hashalg=hashalg,
599            parent_domain=parent_domain,
600            domain=domain,
601            )
602        self.callback = callback
603        self.debug = debug
604
605    def unauthenticated_userid(self, request):
606        """ The userid key within the auth_tkt cookie."""
607        result = self.cookie.identify(request)
608        if result:
609            return result['userid']
610
611    def remember(self, request, userid, **kw):
612        """ Accepts the following kw args: ``max_age=<int-seconds>,
613        ``tokens=<sequence-of-ascii-strings>``.
614
615        Return a list of headers which will set appropriate cookies on
616        the response.
617
618        """
619        return self.cookie.remember(request, userid, **kw)
620
621    def forget(self, request):
622        """ A list of headers which will delete appropriate cookies."""
623        return self.cookie.forget(request)
624
625def b64encode(v):
626    return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'')
627
628def b64decode(v):
629    return base64.b64decode(bytes_(v))
630
631# this class licensed under the MIT license (stolen from Paste)
632class AuthTicket(object):
633    """
634    This class represents an authentication token.  You must pass in
635    the shared secret, the userid, and the IP address.  Optionally you
636    can include tokens (a list of strings, representing role names),
637    'user_data', which is arbitrary data available for your own use in
638    later scripts.  Lastly, you can override the cookie name and
639    timestamp.
640
641    Once you provide all the arguments, use .cookie_value() to
642    generate the appropriate authentication ticket.
643
644    Usage::
645
646        token = AuthTicket('sharedsecret', 'username',
647            os.environ['REMOTE_ADDR'], tokens=['admin'])
648        val = token.cookie_value()
649
650    """
651
652    def __init__(self, secret, userid, ip, tokens=(), user_data='',
653                 time=None, cookie_name='auth_tkt', secure=False,
654                 hashalg='md5'):
655        self.secret = secret
656        self.userid = userid
657        self.ip = ip
658        self.tokens = ','.join(tokens)
659        self.user_data = user_data
660        if time is None:
661            self.time = time_mod.time()
662        else:
663            self.time = time
664        self.cookie_name = cookie_name
665        self.secure = secure
666        self.hashalg = hashalg
667
668    def digest(self):
669        return calculate_digest(
670            self.ip, self.time, self.secret, self.userid, self.tokens,
671            self.user_data, self.hashalg)
672
673    def cookie_value(self):
674        v = '%s%08x%s!' % (self.digest(), int(self.time),
675                           url_quote(self.userid))
676        if self.tokens:
677            v += self.tokens + '!'
678        v += self.user_data
679        return v
680
681# this class licensed under the MIT license (stolen from Paste)
682class BadTicket(Exception):
683    """
684    Exception raised when a ticket can't be parsed.  If we get far enough to
685    determine what the expected digest should have been, expected is set.
686    This should not be shown by default, but can be useful for debugging.
687    """
688    def __init__(self, msg, expected=None):
689        self.expected = expected
690        Exception.__init__(self, msg)
691
692# this function licensed under the MIT license (stolen from Paste)
693def parse_ticket(secret, ticket, ip, hashalg='md5'):
694    """
695    Parse the ticket, returning (timestamp, userid, tokens, user_data).
696
697    If the ticket cannot be parsed, a ``BadTicket`` exception will be raised
698    with an explanation.
699    """
700    ticket = native_(ticket).strip('"')
701    digest_size = hashlib.new(hashalg).digest_size * 2
702    digest = ticket[:digest_size]
703    try:
704        timestamp = int(ticket[digest_size:digest_size + 8], 16)
705    except ValueError as e:
706        raise BadTicket('Timestamp is not a hex integer: %s' % e)
707    try:
708        userid, data = ticket[digest_size + 8:].split('!', 1)
709    except ValueError:
710        raise BadTicket('userid is not followed by !')
711    userid = url_unquote(userid)
712    if '!' in data:
713        tokens, user_data = data.split('!', 1)
714    else: # pragma: no cover (never generated)
715        # @@: Is this the right order?
716        tokens = ''
717        user_data = data
718
719    expected = calculate_digest(ip, timestamp, secret,
720                                userid, tokens, user_data, hashalg)
721
722    # Avoid timing attacks (see
723    # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf)
724    if strings_differ(expected, digest):
725        raise BadTicket('Digest signature is not correct',
726                        expected=(expected, digest))
727
728    tokens = tokens.split(',')
729
730    return (timestamp, userid, tokens, user_data)
731
732# this function licensed under the MIT license (stolen from Paste)
733def calculate_digest(ip, timestamp, secret, userid, tokens, user_data,
734                     hashalg='md5'):
735    secret = bytes_(secret, 'utf-8')
736    userid = bytes_(userid, 'utf-8')
737    tokens = bytes_(tokens, 'utf-8')
738    user_data = bytes_(user_data, 'utf-8')
739    hash_obj = hashlib.new(hashalg)
740
741    # Check to see if this is an IPv6 address
742    if ':' in ip:
743        ip_timestamp = ip + str(int(timestamp))
744        ip_timestamp = bytes_(ip_timestamp)
745    else:
746        # encode_ip_timestamp not required, left in for backwards compatibility
747        ip_timestamp = encode_ip_timestamp(ip, timestamp)
748
749    hash_obj.update(ip_timestamp + secret + userid + b'\0' +
750            tokens + b'\0' + user_data)
751    digest = hash_obj.hexdigest()
752    hash_obj2 = hashlib.new(hashalg)
753    hash_obj2.update(bytes_(digest) + secret)
754    return hash_obj2.hexdigest()
755
756# this function licensed under the MIT license (stolen from Paste)
757def encode_ip_timestamp(ip, timestamp):
758    ip_chars = ''.join(map(chr, map(int, ip.split('.'))))
759    t = int(timestamp)
760    ts = ((t & 0xff000000) >> 24,
761          (t & 0xff0000) >> 16,
762          (t & 0xff00) >> 8,
763          t & 0xff)
764    ts_chars = ''.join(map(chr, ts))
765    return bytes_(ip_chars + ts_chars)
766
767class AuthTktCookieHelper(object):
768    """
769    A helper class for use in third-party authentication policy
770    implementations.  See
771    :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
772    meanings of the constructor arguments.
773    """
774    parse_ticket = staticmethod(parse_ticket) # for tests
775    AuthTicket = AuthTicket # for tests
776    BadTicket = BadTicket # for tests
777    now = None # for tests
778
779    userid_type_decoders = {
780        'int':int,
781        'unicode':lambda x: utf_8_decode(x)[0], # bw compat for old cookies
782        'b64unicode': lambda x: utf_8_decode(b64decode(x))[0],
783        'b64str': lambda x: b64decode(x),
784        }
785
786    userid_type_encoders = {
787        int: ('int', str),
788        long: ('int', str),
789        text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])),
790        binary_type: ('b64str', lambda x: b64encode(x)),
791        }
792
793    def __init__(self, secret, cookie_name='auth_tkt', secure=False,
794                 include_ip=False, timeout=None, reissue_time=None,
795                 max_age=None, http_only=False, path="/", wild_domain=True,
796                 hashalg='md5', parent_domain=False, domain=None):
797
798        serializer = _SimpleSerializer()
799
800        self.cookie_profile = CookieProfile(
801            cookie_name=cookie_name,
802            secure=secure,
803            max_age=max_age,
804            httponly=http_only,
805            path=path,
806            serializer=serializer
807        )
808
809        self.secret = secret
810        self.cookie_name = cookie_name
811        self.secure = secure
812        self.include_ip = include_ip
813        self.timeout = timeout if timeout is None else int(timeout)
814        self.reissue_time = reissue_time if reissue_time is None else int(reissue_time)
815        self.max_age = max_age if max_age is None else int(max_age)
816        self.wild_domain = wild_domain
817        self.parent_domain = parent_domain
818        self.domain = domain
819        self.hashalg = hashalg
820
821    def _get_cookies(self, request, value, max_age=None):
822        cur_domain = request.domain
823
824        domains = []
825        if self.domain:
826            domains.append(self.domain)
827        else:
828            if self.parent_domain and cur_domain.count('.') > 1:
829                domains.append('.' + cur_domain.split('.', 1)[1])
830            else:
831                domains.append(None)
832                domains.append(cur_domain)
833                if self.wild_domain:
834                    domains.append('.' + cur_domain)
835
836        profile = self.cookie_profile(request)
837
838        kw = {}
839        kw['domains'] = domains
840        if max_age is not None:
841            kw['max_age'] = max_age
842
843        headers = profile.get_headers(value, **kw)
844        return headers
845
846    def identify(self, request):
847        """ Return a dictionary with authentication information, or ``None``
848        if no valid auth_tkt is attached to ``request``"""
849        environ = request.environ
850        cookie = request.cookies.get(self.cookie_name)
851
852        if cookie is None:
853            return None
854
855        if self.include_ip:
856            remote_addr = environ['REMOTE_ADDR']
857        else:
858            remote_addr = '0.0.0.0'
859
860        try:
861            timestamp, userid, tokens, user_data = self.parse_ticket(
862                self.secret, cookie, remote_addr, self.hashalg)
863        except self.BadTicket:
864            return None
865
866        now = self.now # service tests
867
868        if now is None:
869            now = time_mod.time()
870
871        if self.timeout and ( (timestamp + self.timeout) < now ):
872            # the auth_tkt data has expired
873            return None
874
875        userid_typename = 'userid_type:'
876        user_data_info = user_data.split('|')
877        for datum in filter(None, user_data_info):
878            if datum.startswith(userid_typename):
879                userid_type = datum[len(userid_typename):]
880                decoder = self.userid_type_decoders.get(userid_type)
881                if decoder:
882                    userid = decoder(userid)
883
884        reissue = self.reissue_time is not None
885
886        if reissue and not hasattr(request, '_authtkt_reissued'):
887            if ( (now - timestamp) > self.reissue_time ):
888                # See https://github.com/Pylons/pyramid/issues#issue/108
889                tokens = list(filter(None, tokens))
890                headers = self.remember(request, userid, max_age=self.max_age,
891                                        tokens=tokens)
892                def reissue_authtkt(request, response):
893                    if not hasattr(request, '_authtkt_reissue_revoked'):
894                        for k, v in headers:
895                            response.headerlist.append((k, v))
896                request.add_response_callback(reissue_authtkt)
897                request._authtkt_reissued = True
898
899        environ['REMOTE_USER_TOKENS'] = tokens
900        environ['REMOTE_USER_DATA'] = user_data
901        environ['AUTH_TYPE'] = 'cookie'
902
903        identity = {}
904        identity['timestamp'] = timestamp
905        identity['userid'] = userid
906        identity['tokens'] = tokens
907        identity['userdata'] = user_data
908        return identity
909
910    def forget(self, request):
911        """ Return a set of expires Set-Cookie headers, which will destroy
912        any existing auth_tkt cookie when attached to a response"""
913        request._authtkt_reissue_revoked = True
914        return self._get_cookies(request, None)
915
916    def remember(self, request, userid, max_age=None, tokens=()):
917        """ Return a set of Set-Cookie headers; when set into a response,
918        these headers will represent a valid authentication ticket.
919
920        ``max_age``
921          The max age of the auth_tkt cookie, in seconds.  When this value is
922          set, the cookie's ``Max-Age`` and ``Expires`` settings will be set,
923          allowing the auth_tkt cookie to last between browser sessions.  If
924          this value is ``None``, the ``max_age`` value provided to the
925          helper itself will be used as the ``max_age`` value.  Default:
926          ``None``.
927
928        ``tokens``
929          A sequence of strings that will be placed into the auth_tkt tokens
930          field.  Each string in the sequence must be of the Python ``str``
931          type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``.
932          Tokens are available in the returned identity when an auth_tkt is
933          found in the request and unpacked.  Default: ``()``.
934        """
935        max_age = self.max_age if max_age is None else int(max_age)
936
937        environ = request.environ
938
939        if self.include_ip:
940            remote_addr = environ['REMOTE_ADDR']
941        else:
942            remote_addr = '0.0.0.0'
943
944        user_data = ''
945
946        encoding_data = self.userid_type_encoders.get(type(userid))
947
948        if encoding_data:
949            encoding, encoder = encoding_data
950            userid = encoder(userid)
951            user_data = 'userid_type:%s' % encoding
952
953        new_tokens = []
954        for token in tokens:
955            if isinstance(token, text_type):
956                try:
957                    token = ascii_native_(token)
958                except UnicodeEncodeError:
959                    raise ValueError("Invalid token %r" % (token,))
960            if not (isinstance(token, str) and VALID_TOKEN.match(token)):
961                raise ValueError("Invalid token %r" % (token,))
962            new_tokens.append(token)
963        tokens = tuple(new_tokens)
964
965        if hasattr(request, '_authtkt_reissued'):
966            request._authtkt_reissue_revoked = True
967
968        ticket = self.AuthTicket(
969            self.secret,
970            userid,
971            remote_addr,
972            tokens=tokens,
973            user_data=user_data,
974            cookie_name=self.cookie_name,
975            secure=self.secure,
976            hashalg=self.hashalg
977            )
978
979        cookie_value = ticket.cookie_value()
980        return self._get_cookies(request, cookie_value, max_age)
981
982@implementer(IAuthenticationPolicy)
983class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
984    """ A :app:`Pyramid` authentication policy which gets its data from the
985    configured :term:`session`.  For this authentication policy to work, you
986    will have to follow the instructions in the :ref:`sessions_chapter` to
987    configure a :term:`session factory`.
988
989    Constructor Arguments
990
991    ``prefix``
992
993       A prefix used when storing the authentication parameters in the
994       session. Defaults to 'auth.'. Optional.
995
996    ``callback``
997
998       Default: ``None``.  A callback passed the userid and the
999       request, expected to return ``None`` if the userid doesn't
1000       exist or a sequence of principal identifiers (possibly empty) if
1001       the user does exist.  If ``callback`` is ``None``, the userid
1002       will be assumed to exist with no principals.  Optional.
1003
1004    ``debug``
1005
1006        Default: ``False``.  If ``debug`` is ``True``, log messages to the
1007        Pyramid debug logger about the results of various authentication
1008        steps.  The output from debugging is useful for reporting to maillist
1009        or IRC channels when asking for support.
1010
1011    """
1012
1013    def __init__(self, prefix='auth.', callback=None, debug=False):
1014        self.callback = callback
1015        self.prefix = prefix or ''
1016        self.userid_key = prefix + 'userid'
1017        self.debug = debug
1018
1019    def remember(self, request, userid, **kw):
1020        """ Store a userid in the session."""
1021        request.session[self.userid_key] = userid
1022        return []
1023
1024    def forget(self, request):
1025        """ Remove the stored userid from the session."""
1026        if self.userid_key in request.session:
1027            del request.session[self.userid_key]
1028        return []
1029
1030    def unauthenticated_userid(self, request):
1031        return request.session.get(self.userid_key)
1032
1033
1034@implementer(IAuthenticationPolicy)
1035class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
1036    """ A :app:`Pyramid` authentication policy which uses HTTP standard basic
1037    authentication protocol to authenticate users.  To use this policy you will
1038    need to provide a callback which checks the supplied user credentials
1039    against your source of login data.
1040
1041    Constructor Arguments
1042
1043    ``check``
1044
1045       A callback function passed a username, password and request, in that
1046       order as positional arguments.  Expected to return ``None`` if the
1047       userid doesn't exist or a sequence of principal identifiers (possibly
1048       empty) if the user does exist.
1049
1050    ``realm``
1051
1052       Default: ``"Realm"``.  The Basic Auth Realm string.  Usually displayed to
1053       the user by the browser in the login dialog.
1054
1055    ``debug``
1056
1057        Default: ``False``.  If ``debug`` is ``True``, log messages to the
1058        Pyramid debug logger about the results of various authentication
1059        steps.  The output from debugging is useful for reporting to maillist
1060        or IRC channels when asking for support.
1061
1062    **Issuing a challenge**
1063
1064    Regular browsers will not send username/password credentials unless they
1065    first receive a challenge from the server.  The following recipe will
1066    register a view that will send a Basic Auth challenge to the user whenever
1067    there is an attempt to call a view which results in a Forbidden response::
1068
1069        from pyramid.httpexceptions import HTTPUnauthorized
1070        from pyramid.security import forget
1071        from pyramid.view import forbidden_view_config
1072
1073        @forbidden_view_config()
1074        def basic_challenge(request):
1075            response = HTTPUnauthorized()
1076            response.headers.update(forget(request))
1077            return response
1078    """
1079    def __init__(self, check, realm='Realm', debug=False):
1080        self.check = check
1081        self.realm = realm
1082        self.debug = debug
1083
1084    def unauthenticated_userid(self, request):
1085        """ The userid parsed from the ``Authorization`` request header."""
1086        credentials = self._get_credentials(request)
1087        if credentials:
1088            return credentials[0]
1089
1090    def remember(self, request, userid, **kw):
1091        """ A no-op. Basic authentication does not provide a protocol for
1092        remembering the user. Credentials are sent on every request.
1093
1094        """
1095        return []
1096
1097    def forget(self, request):
1098        """ Returns challenge headers. This should be attached to a response
1099        to indicate that credentials are required."""
1100        return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
1101
1102    def callback(self, username, request):
1103        # Username arg is ignored.  Unfortunately _get_credentials winds up
1104        # getting called twice when authenticated_userid is called.  Avoiding
1105        # that, however, winds up duplicating logic from the superclass.
1106        credentials = self._get_credentials(request)
1107        if credentials:
1108            username, password = credentials
1109            return self.check(username, password, request)
1110
1111    def _get_credentials(self, request):
1112        authorization = request.headers.get('Authorization')
1113        if not authorization:
1114            return None
1115        try:
1116            authmeth, auth = authorization.split(' ', 1)
1117        except ValueError: # not enough values to unpack
1118            return None
1119        if authmeth.lower() != 'basic':
1120            return None
1121
1122        try:
1123            authbytes = b64decode(auth.strip())
1124        except (TypeError, binascii.Error): # can't decode
1125            return None
1126
1127        # try utf-8 first, then latin-1; see discussion in
1128        # https://github.com/Pylons/pyramid/issues/898
1129        try:
1130            auth = authbytes.decode('utf-8')
1131        except UnicodeDecodeError:
1132            auth = authbytes.decode('latin-1')
1133
1134        try:
1135            username, password = auth.split(':', 1)
1136        except ValueError: # not enough values to unpack
1137            return None
1138        return username, password
1139
1140class _SimpleSerializer(object):
1141    def loads(self, bstruct):
1142        return native_(bstruct)
1143
1144    def dumps(self, appstruct):
1145        return bytes_(appstruct)
1146