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