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