Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/authentication.py : 20%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import binascii
2from codecs import utf_8_decode
3from codecs import utf_8_encode
4from collections import namedtuple
5import hashlib
6import base64
7import re
8import time as time_mod
9import warnings
11from zope.interface import implementer
13from webob.cookies import CookieProfile
15from pyramid.compat import (
16 long,
17 text_type,
18 binary_type,
19 url_unquote,
20 url_quote,
21 bytes_,
22 ascii_native_,
23 native_,
24)
26from pyramid.interfaces import IAuthenticationPolicy, IDebugLogger
28from pyramid.security import Authenticated, Everyone
30from pyramid.util import strings_differ
31from pyramid.util import SimpleSerializer
33VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$")
36class CallbackAuthenticationPolicy(object):
37 """ Abstract class """
39 debug = False
40 callback = None
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)
50 def _clean_principal(self, princid):
51 if princid in (Authenticated, Everyone):
52 princid = None
53 return princid
55 def authenticated_userid(self, request):
56 """ Return the authenticated userid or ``None``.
58 If no callback is registered, this will be the same as
59 ``unauthenticated_userid``.
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``.
64 """
65 debug = self.debug
66 userid = self.unauthenticated_userid(request)
67 if userid is None:
68 debug and self._log(
69 'call to unauthenticated_userid returned None; returning None',
70 'authenticated_userid',
71 request,
72 )
73 return None
74 if self._clean_principal(userid) is None:
75 debug and self._log(
76 (
77 'use of userid %r is disallowed by any built-in Pyramid '
78 'security policy, returning None' % userid
79 ),
80 'authenticated_userid',
81 request,
82 )
83 return None
85 if self.callback is None:
86 debug and self._log(
87 'there was no groupfinder callback; returning %r' % (userid,),
88 'authenticated_userid',
89 request,
90 )
91 return userid
92 callback_ok = self.callback(userid, request)
93 if callback_ok is not None: # is not None!
94 debug and self._log(
95 'groupfinder callback returned %r; returning %r'
96 % (callback_ok, userid),
97 'authenticated_userid',
98 request,
99 )
100 return userid
101 debug and self._log(
102 'groupfinder callback returned None; returning None',
103 'authenticated_userid',
104 request,
105 )
107 def effective_principals(self, request):
108 """ A list of effective principals derived from request.
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:
115 .. code-block:: python
117 return [Everyone]
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``:
124 .. code-block:: python
126 extra_principals = callback(userid, request)
127 return [Everyone, Authenticated, userid] + extra_principals
129 """
130 debug = self.debug
131 effective_principals = [Everyone]
132 userid = self.unauthenticated_userid(request)
134 if userid is None:
135 debug and self._log(
136 'unauthenticated_userid returned %r; returning %r'
137 % (userid, effective_principals),
138 'effective_principals',
139 request,
140 )
141 return effective_principals
143 if self._clean_principal(userid) is None:
144 debug and self._log(
145 (
146 'unauthenticated_userid returned disallowed %r; returning '
147 '%r as if it was None' % (userid, effective_principals)
148 ),
149 'effective_principals',
150 request,
151 )
152 return effective_principals
154 if self.callback is None:
155 debug and self._log(
156 'groupfinder callback is None, so groups is []',
157 'effective_principals',
158 request,
159 )
160 groups = []
161 else:
162 groups = self.callback(userid, request)
163 debug and self._log(
164 'groupfinder callback returned %r as groups' % (groups,),
165 'effective_principals',
166 request,
167 )
169 if groups is None: # is None!
170 debug and self._log(
171 'returning effective principals: %r' % (effective_principals,),
172 'effective_principals',
173 request,
174 )
175 return effective_principals
177 effective_principals.append(Authenticated)
178 effective_principals.append(userid)
179 effective_principals.extend(groups)
181 debug and self._log(
182 'returning effective principals: %r' % (effective_principals,),
183 'effective_principals',
184 request,
185 )
186 return effective_principals
189@implementer(IAuthenticationPolicy)
190class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
191 """ A :app:`Pyramid` :term:`authentication policy` which
192 obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the
193 ``repoze.who.identity`` key in the WSGI environment).
195 Constructor Arguments
197 ``identifier_name``
199 Default: ``auth_tkt``. The :mod:`repoze.who` plugin name that
200 performs remember/forget. Optional.
202 ``callback``
204 Default: ``None``. A callback passed the :mod:`repoze.who` identity
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.
211 Objects of this class implement the interface described by
212 :class:`pyramid.interfaces.IAuthenticationPolicy`.
213 """
215 def __init__(self, identifier_name='auth_tkt', callback=None):
216 self.identifier_name = identifier_name
217 self.callback = callback
219 def _get_identity(self, request):
220 return request.environ.get('repoze.who.identity')
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
229 def authenticated_userid(self, request):
230 """ Return the authenticated userid or ``None``.
232 If no callback is registered, this will be the same as
233 ``unauthenticated_userid``.
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``.
238 """
239 identity = self._get_identity(request)
241 if identity is None:
242 self.debug and self._log(
243 'repoze.who identity is None, returning None',
244 'authenticated_userid',
245 request,
246 )
247 return None
249 userid = identity['repoze.who.userid']
251 if userid is None:
252 self.debug and self._log(
253 'repoze.who.userid is None, returning None' % userid,
254 'authenticated_userid',
255 request,
256 )
257 return None
259 if self._clean_principal(userid) is None:
260 self.debug and self._log(
261 (
262 'use of userid %r is disallowed by any built-in Pyramid '
263 'security policy, returning None' % userid
264 ),
265 'authenticated_userid',
266 request,
267 )
268 return None
270 if self.callback is None:
271 return userid
273 if self.callback(identity, request) is not None: # is not None!
274 return userid
276 def unauthenticated_userid(self, request):
277 """ Return the ``repoze.who.userid`` key from the detected identity."""
278 identity = self._get_identity(request)
279 if identity is None:
280 return None
281 return identity['repoze.who.userid']
283 def effective_principals(self, request):
284 """ A list of effective principals derived from the identity.
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.
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``.
295 """
296 effective_principals = [Everyone]
297 identity = self._get_identity(request)
299 if identity is None:
300 self.debug and self._log(
301 (
302 'repoze.who identity was None; returning %r'
303 % effective_principals
304 ),
305 'effective_principals',
306 request,
307 )
308 return effective_principals
310 if self.callback is None:
311 groups = []
312 else:
313 groups = self.callback(identity, request)
315 if groups is None: # is None!
316 self.debug and self._log(
317 (
318 'security policy groups callback returned None; returning '
319 '%r' % effective_principals
320 ),
321 'effective_principals',
322 request,
323 )
324 return effective_principals
326 userid = identity['repoze.who.userid']
328 if userid is None:
329 self.debug and self._log(
330 (
331 'repoze.who.userid was None; returning %r'
332 % effective_principals
333 ),
334 'effective_principals',
335 request,
336 )
337 return effective_principals
339 if self._clean_principal(userid) is None:
340 self.debug and self._log(
341 (
342 'unauthenticated_userid returned disallowed %r; returning '
343 '%r as if it was None' % (userid, effective_principals)
344 ),
345 'effective_principals',
346 request,
347 )
348 return effective_principals
350 effective_principals.append(Authenticated)
351 effective_principals.append(userid)
352 effective_principals.extend(groups)
353 return effective_principals
355 def remember(self, request, userid, **kw):
356 """ Store the ``userid`` as ``repoze.who.userid``.
358 The identity to authenticated to :mod:`repoze.who`
359 will contain the given userid as ``userid``, and
360 provide all keyword arguments as additional identity
361 keys. Useful keys could be ``max_age`` or ``userdata``.
362 """
363 identifier = self._get_identifier(request)
364 if identifier is None:
365 return []
366 environ = request.environ
367 identity = kw
368 identity['repoze.who.userid'] = userid
369 return identifier.remember(environ, identity)
371 def forget(self, request):
372 """ Forget the current authenticated user.
374 Return headers that, if included in a response, will delete the
375 cookie responsible for tracking the current user.
377 """
378 identifier = self._get_identifier(request)
379 if identifier is None:
380 return []
381 identity = self._get_identity(request)
382 return identifier.forget(request.environ, identity)
385@implementer(IAuthenticationPolicy)
386class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
387 """ A :app:`Pyramid` :term:`authentication policy` which
388 obtains data from the ``REMOTE_USER`` WSGI environment variable.
390 Constructor Arguments
392 ``environ_key``
394 Default: ``REMOTE_USER``. The key in the WSGI environ which
395 provides the userid.
397 ``callback``
399 Default: ``None``. A callback passed the userid and the request,
400 expected to return None if the userid doesn't exist or a sequence of
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.
405 ``debug``
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.
412 Objects of this class implement the interface described by
413 :class:`pyramid.interfaces.IAuthenticationPolicy`.
414 """
416 def __init__(self, environ_key='REMOTE_USER', callback=None, debug=False):
417 self.environ_key = environ_key
418 self.callback = callback
419 self.debug = debug
421 def unauthenticated_userid(self, request):
422 """ The ``REMOTE_USER`` value found within the ``environ``."""
423 return request.environ.get(self.environ_key)
425 def remember(self, request, userid, **kw):
426 """ A no-op. The ``REMOTE_USER`` does not provide a protocol for
427 remembering the user. This will be application-specific and can
428 be done somewhere else or in a subclass."""
429 return []
431 def forget(self, request):
432 """ A no-op. The ``REMOTE_USER`` does not provide a protocol for
433 forgetting the user. This will be application-specific and can
434 be done somewhere else or in a subclass."""
435 return []
438@implementer(IAuthenticationPolicy)
439class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
440 """A :app:`Pyramid` :term:`authentication policy` which
441 obtains data from a Pyramid "auth ticket" cookie.
443 Constructor Arguments
445 ``secret``
447 The secret (a string) used for auth_tkt cookie signing. This value
448 should be unique across all values provided to Pyramid for various
449 subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
450 Required.
452 ``callback``
454 Default: ``None``. A callback passed the userid and the
455 request, expected to return ``None`` if the userid doesn't
456 exist or a sequence of principal identifiers (possibly empty) if
457 the user does exist. If ``callback`` is ``None``, the userid
458 will be assumed to exist with no principals. Optional.
460 ``cookie_name``
462 Default: ``auth_tkt``. The cookie name used
463 (string). Optional.
465 ``secure``
467 Default: ``False``. Only send the cookie back over a secure
468 conn. Optional.
470 ``include_ip``
472 Default: ``False``. Make the requesting IP address part of
473 the authentication data in the cookie. Optional.
475 For IPv6 this option is not recommended. The ``mod_auth_tkt``
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.
481 ``timeout``
483 Default: ``None``. Maximum number of seconds which a newly
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.
487 Optional.
489 ``reissue_time``
491 Default: ``None``. If this parameter is set, it represents the number
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.
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.
506 Optional.
508 ``max_age``
510 Default: ``None``. The max age of the auth_tkt cookie, in
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
516 to last between browser sessions. It is typically nonsensical
517 to set this to a value that is lower than ``timeout`` or
518 ``reissue_time``, although it is not explicitly prevented.
519 Optional.
521 ``path``
523 Default: ``/``. The path for which the auth_tkt cookie is valid.
524 May be desirable if the application only serves part of a domain.
525 Optional.
527 ``http_only``
529 Default: ``False``. Hide cookie from JavaScript by setting the
530 HttpOnly flag. Not honored by all browsers.
531 Optional.
533 ``wild_domain``
535 Default: ``True``. An auth_tkt cookie will be generated for the
536 wildcard domain. If your site is hosted as ``example.com`` this
537 will make the cookie available for sites underneath ``example.com``
538 such as ``www.example.com``.
539 Optional.
541 ``parent_domain``
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.
551 ``domain``
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.
558 ``hashalg``
560 Default: ``sha512`` (the literal string).
562 Any hash algorithm supported by Python's ``hashlib.new()`` function
563 can be used as the ``hashalg``.
565 Cookies generated by different instances of AuthTktAuthenticationPolicy
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.
570 Optional.
572 ``debug``
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.
579 ``samesite``
581 Default: ``'Lax'``. The 'samesite' option of the session cookie. Set
582 the value to ``None`` to turn off the samesite option.
584 This option is available as of :app:`Pyramid` 1.10.
586 .. versionchanged:: 1.4
588 Added the ``hashalg`` option, defaulting to ``sha512``.
590 .. versionchanged:: 1.5
592 Added the ``domain`` option.
594 Added the ``parent_domain`` option.
596 .. versionchanged:: 1.10
598 Added the ``samesite`` option and made the default ``'Lax'``.
600 Objects of this class implement the interface described by
601 :class:`pyramid.interfaces.IAuthenticationPolicy`.
603 """
605 def __init__(
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 ):
624 self.cookie = AuthTktCookieHelper(
625 secret,
626 cookie_name=cookie_name,
627 secure=secure,
628 include_ip=include_ip,
629 timeout=timeout,
630 reissue_time=reissue_time,
631 max_age=max_age,
632 http_only=http_only,
633 path=path,
634 wild_domain=wild_domain,
635 hashalg=hashalg,
636 parent_domain=parent_domain,
637 domain=domain,
638 samesite=samesite,
639 )
640 self.callback = callback
641 self.debug = debug
643 def unauthenticated_userid(self, request):
644 """ The userid key within the auth_tkt cookie."""
645 result = self.cookie.identify(request)
646 if result:
647 return result['userid']
649 def remember(self, request, userid, **kw):
650 """ Accepts the following kw args: ``max_age=<int-seconds>,
651 ``tokens=<sequence-of-ascii-strings>``.
653 Return a list of headers which will set appropriate cookies on
654 the response.
656 """
657 return self.cookie.remember(request, userid, **kw)
659 def forget(self, request):
660 """ A list of headers which will delete appropriate cookies."""
661 return self.cookie.forget(request)
664def b64encode(v):
665 return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'')
668def b64decode(v):
669 return base64.b64decode(bytes_(v))
672# this class licensed under the MIT license (stolen from Paste)
673class 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.
682 Once you provide all the arguments, use .cookie_value() to
683 generate the appropriate authentication ticket.
685 Usage::
687 token = AuthTicket('sharedsecret', 'username',
688 os.environ['REMOTE_ADDR'], tokens=['admin'])
689 val = token.cookie_value()
691 """
693 def __init__(
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 ):
705 self.secret = secret
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
716 self.hashalg = hashalg
718 def digest(self):
719 return calculate_digest(
720 self.ip,
721 self.time,
722 self.secret,
723 self.userid,
724 self.tokens,
725 self.user_data,
726 self.hashalg,
727 )
729 def cookie_value(self):
730 v = '%s%08x%s!' % (
731 self.digest(),
732 int(self.time),
733 url_quote(self.userid),
734 )
735 if self.tokens:
736 v += self.tokens + '!'
737 v += self.user_data
738 return v
741# this class licensed under the MIT license (stolen from Paste)
742class 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 """
749 def __init__(self, msg, expected=None):
750 self.expected = expected
751 Exception.__init__(self, msg)
754# this function licensed under the MIT license (stolen from Paste)
755def parse_ticket(secret, ticket, ip, hashalg='md5'):
756 """
757 Parse the ticket, returning (timestamp, userid, tokens, user_data).
759 If the ticket cannot be parsed, a ``BadTicket`` exception will be raised
760 with an explanation.
761 """
762 ticket = native_(ticket).strip('"')
763 digest_size = hashlib.new(hashalg).digest_size * 2
764 digest = ticket[:digest_size]
765 try:
766 timestamp = int(ticket[digest_size : digest_size + 8], 16)
767 except ValueError as e:
768 raise BadTicket('Timestamp is not a hex integer: %s' % e)
769 try:
770 userid, data = ticket[digest_size + 8 :].split('!', 1)
771 except ValueError:
772 raise BadTicket('userid is not followed by !')
773 userid = url_unquote(userid)
774 if '!' in data:
775 tokens, user_data = data.split('!', 1)
776 else: # pragma: no cover (never generated)
777 # @@: Is this the right order?
778 tokens = ''
779 user_data = data
781 expected = calculate_digest(
782 ip, timestamp, secret, userid, tokens, user_data, hashalg
783 )
785 # Avoid timing attacks (see
786 # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf)
787 if strings_differ(expected, digest):
788 raise BadTicket(
789 'Digest signature is not correct', expected=(expected, digest)
790 )
792 tokens = tokens.split(',')
794 return (timestamp, userid, tokens, user_data)
797# this function licensed under the MIT license (stolen from Paste)
798def calculate_digest(
799 ip, timestamp, secret, userid, tokens, user_data, hashalg='md5'
800):
801 secret = bytes_(secret, 'utf-8')
802 userid = bytes_(userid, 'utf-8')
803 tokens = bytes_(tokens, 'utf-8')
804 user_data = bytes_(user_data, 'utf-8')
805 hash_obj = hashlib.new(hashalg)
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)
815 hash_obj.update(
816 ip_timestamp + secret + userid + b'\0' + tokens + b'\0' + user_data
817 )
818 digest = hash_obj.hexdigest()
819 hash_obj2 = hashlib.new(hashalg)
820 hash_obj2.update(bytes_(digest) + secret)
821 return hash_obj2.hexdigest()
824# this function licensed under the MIT license (stolen from Paste)
825def encode_ip_timestamp(ip, timestamp):
826 ip_chars = ''.join(map(chr, map(int, ip.split('.'))))
827 t = int(timestamp)
828 ts = (
829 (t & 0xFF000000) >> 24,
830 (t & 0xFF0000) >> 16,
831 (t & 0xFF00) >> 8,
832 t & 0xFF,
833 )
834 ts_chars = ''.join(map(chr, ts))
835 return bytes_(ip_chars + ts_chars)
838class AuthTktCookieHelper(object):
839 """
840 A helper class for use in third-party authentication policy
841 implementations. See
842 :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
843 meanings of the constructor arguments.
844 """
846 parse_ticket = staticmethod(parse_ticket) # for tests
847 AuthTicket = AuthTicket # for tests
848 BadTicket = BadTicket # for tests
849 now = None # for tests
851 userid_type_decoders = {
852 'int': int,
853 'unicode': lambda x: utf_8_decode(x)[0], # bw compat for old cookies
854 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0],
855 'b64str': lambda x: b64decode(x),
856 }
858 userid_type_encoders = {
859 int: ('int', str),
860 long: ('int', str),
861 text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])),
862 binary_type: ('b64str', lambda x: b64encode(x)),
863 }
865 def __init__(
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 ):
883 serializer = SimpleSerializer()
885 self.cookie_profile = CookieProfile(
886 cookie_name=cookie_name,
887 secure=secure,
888 max_age=max_age,
889 httponly=http_only,
890 path=path,
891 serializer=serializer,
892 samesite=samesite,
893 )
895 self.secret = secret
896 self.cookie_name = cookie_name
897 self.secure = secure
898 self.include_ip = include_ip
899 self.timeout = timeout if timeout is None else int(timeout)
900 self.reissue_time = (
901 reissue_time if reissue_time is None else int(reissue_time)
902 )
903 self.max_age = max_age if max_age is None else int(max_age)
904 self.wild_domain = wild_domain
905 self.parent_domain = parent_domain
906 self.domain = domain
907 self.hashalg = hashalg
909 def _get_cookies(self, request, value, max_age=None):
910 cur_domain = request.domain
912 domains = []
913 if self.domain:
914 domains.append(self.domain)
915 else:
916 if self.parent_domain and cur_domain.count('.') > 1:
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)
924 profile = self.cookie_profile(request)
926 kw = {}
927 kw['domains'] = domains
928 if max_age is not None:
929 kw['max_age'] = max_age
931 headers = profile.get_headers(value, **kw)
932 return headers
934 def identify(self, request):
935 """ Return a dictionary with authentication information, or ``None``
936 if no valid auth_tkt is attached to ``request``"""
937 environ = request.environ
938 cookie = request.cookies.get(self.cookie_name)
940 if cookie is None:
941 return None
943 if self.include_ip:
944 remote_addr = environ['REMOTE_ADDR']
945 else:
946 remote_addr = '0.0.0.0'
948 try:
949 timestamp, userid, tokens, user_data = self.parse_ticket(
950 self.secret, cookie, remote_addr, self.hashalg
951 )
952 except self.BadTicket:
953 return None
955 now = self.now # service tests
957 if now is None:
958 now = time_mod.time()
960 if self.timeout and ((timestamp + self.timeout) < now):
961 # the auth_tkt data has expired
962 return None
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):
968 userid_type = datum[len(userid_typename) :]
969 decoder = self.userid_type_decoders.get(userid_type)
970 if decoder:
971 userid = decoder(userid)
973 reissue = self.reissue_time is not None
975 if reissue and not hasattr(request, '_authtkt_reissued'):
976 if (now - timestamp) > self.reissue_time:
977 # See https://github.com/Pylons/pyramid/issues#issue/108
978 tokens = list(filter(None, tokens))
979 headers = self.remember(
980 request, userid, max_age=self.max_age, tokens=tokens
981 )
983 def reissue_authtkt(request, response):
984 if not hasattr(request, '_authtkt_reissue_revoked'):
985 for k, v in headers:
986 response.headerlist.append((k, v))
988 request.add_response_callback(reissue_authtkt)
989 request._authtkt_reissued = True
991 environ['REMOTE_USER_TOKENS'] = tokens
992 environ['REMOTE_USER_DATA'] = user_data
993 environ['AUTH_TYPE'] = 'cookie'
995 identity = {}
996 identity['timestamp'] = timestamp
997 identity['userid'] = userid
998 identity['tokens'] = tokens
999 identity['userdata'] = user_data
1000 return identity
1002 def forget(self, request):
1003 """ Return a set of expires Set-Cookie headers, which will destroy
1004 any existing auth_tkt cookie when attached to a response"""
1005 request._authtkt_reissue_revoked = True
1006 return self._get_cookies(request, None)
1008 def remember(self, request, userid, max_age=None, tokens=()):
1009 """ Return a set of Set-Cookie headers; when set into a response,
1010 these headers will represent a valid authentication ticket.
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,
1015 allowing the auth_tkt cookie to last between browser sessions. If
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``.
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 """
1027 max_age = self.max_age if max_age is None else int(max_age)
1029 environ = request.environ
1031 if self.include_ip:
1032 remote_addr = environ['REMOTE_ADDR']
1033 else:
1034 remote_addr = '0.0.0.0'
1036 user_data = ''
1038 encoding_data = self.userid_type_encoders.get(type(userid))
1040 if encoding_data:
1041 encoding, encoder = encoding_data
1042 else:
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 "
1047 "string as the userid, it will not be decoded back to the "
1048 "type provided.".format(type(userid)),
1049 RuntimeWarning,
1050 )
1051 encoding, encoder = self.userid_type_encoders.get(text_type)
1052 userid = str(userid)
1054 userid = encoder(userid)
1055 user_data = 'userid_type:%s' % encoding
1057 new_tokens = []
1058 for token in tokens:
1059 if isinstance(token, text_type):
1060 try:
1061 token = ascii_native_(token)
1062 except UnicodeEncodeError:
1063 raise ValueError("Invalid token %r" % (token,))
1064 if not (isinstance(token, str) and VALID_TOKEN.match(token)):
1065 raise ValueError("Invalid token %r" % (token,))
1066 new_tokens.append(token)
1067 tokens = tuple(new_tokens)
1069 if hasattr(request, '_authtkt_reissued'):
1070 request._authtkt_reissue_revoked = True
1072 ticket = self.AuthTicket(
1073 self.secret,
1074 userid,
1075 remote_addr,
1076 tokens=tokens,
1077 user_data=user_data,
1078 cookie_name=self.cookie_name,
1079 secure=self.secure,
1080 hashalg=self.hashalg,
1081 )
1083 cookie_value = ticket.cookie_value()
1084 return self._get_cookies(request, cookie_value, max_age)
1087@implementer(IAuthenticationPolicy)
1088class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
1089 """ A :app:`Pyramid` authentication policy which gets its data from the
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`.
1094 Constructor Arguments
1096 ``prefix``
1098 A prefix used when storing the authentication parameters in the
1099 session. Defaults to 'auth.'. Optional.
1101 ``callback``
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.
1109 ``debug``
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.
1116 """
1118 def __init__(self, prefix='auth.', callback=None, debug=False):
1119 self.callback = callback
1120 self.prefix = prefix or ''
1121 self.userid_key = prefix + 'userid'
1122 self.debug = debug
1124 def remember(self, request, userid, **kw):
1125 """ Store a userid in the session."""
1126 request.session[self.userid_key] = userid
1127 return []
1129 def forget(self, request):
1130 """ Remove the stored userid from the session."""
1131 if self.userid_key in request.session:
1132 del request.session[self.userid_key]
1133 return []
1135 def unauthenticated_userid(self, request):
1136 return request.session.get(self.userid_key)
1139@implementer(IAuthenticationPolicy)
1140class 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.
1146 Constructor Arguments
1148 ``check``
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.
1155 ``realm``
1157 Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed
1158 to the user by the browser in the login dialog.
1160 ``debug``
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.
1167 **Issuing a challenge**
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::
1174 from pyramid.httpexceptions import HTTPUnauthorized
1175 from pyramid.security import forget
1176 from pyramid.view import forbidden_view_config
1178 @forbidden_view_config()
1179 def forbidden_view(request):
1180 if request.authenticated_userid is None:
1181 response = HTTPUnauthorized()
1182 response.headers.update(forget(request))
1183 return response
1184 return HTTPForbidden()
1185 """
1187 def __init__(self, check, realm='Realm', debug=False):
1188 self.check = check
1189 self.realm = realm
1190 self.debug = debug
1192 def unauthenticated_userid(self, request):
1193 """ The userid parsed from the ``Authorization`` request header."""
1194 credentials = extract_http_basic_credentials(request)
1195 if credentials:
1196 return credentials.username
1198 def remember(self, request, userid, **kw):
1199 """ A no-op. Basic authentication does not provide a protocol for
1200 remembering the user. Credentials are sent on every request.
1202 """
1203 return []
1205 def forget(self, request):
1206 """ Returns challenge headers. This should be attached to a response
1207 to indicate that credentials are required."""
1208 return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
1210 def callback(self, username, request):
1211 # Username arg is ignored. Unfortunately
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.
1215 credentials = extract_http_basic_credentials(request)
1216 if credentials:
1217 username, password = credentials
1218 return self.check(username, password, request)
1221HTTPBasicCredentials = namedtuple(
1222 'HTTPBasicCredentials', ['username', 'password']
1223)
1226def extract_http_basic_credentials(request):
1227 """ A helper function for extraction of HTTP Basic credentials
1228 from a given :term:`request`.
1230 Returns a :class:`.HTTPBasicCredentials` 2-tuple with ``username`` and
1231 ``password`` attributes or ``None`` if no credentials could be found.
1233 """
1234 authorization = request.headers.get('Authorization')
1235 if not authorization:
1236 return None
1238 try:
1239 authmeth, auth = authorization.split(' ', 1)
1240 except ValueError: # not enough values to unpack
1241 return None
1243 if authmeth.lower() != 'basic':
1244 return None
1246 try:
1247 authbytes = b64decode(auth.strip())
1248 except (TypeError, binascii.Error): # can't decode
1249 return None
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')
1258 try:
1259 username, password = auth.split(':', 1)
1260 except ValueError: # not enough values to unpack
1261 return None
1263 return HTTPBasicCredentials(username, password)