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