1
1
from codecs import utf_8_decode
2
2
from codecs import utf_8_encode
3
+ from hashlib import md5
3
4
import datetime
4
5
import re
5
- import time
6
-
7
- from paste .auth import auth_tkt
8
- from paste .request import get_cookies
6
+ import time as time_mod
7
+ import urllib
9
8
10
9
from zope .interface import implements
11
10
16
15
from pyramid .security import Authenticated
17
16
from pyramid .security import Everyone
18
17
19
-
20
18
VALID_TOKEN = re .compile (r"^[A-Za-z][A-Za-z0-9+_-]*$" )
21
19
22
20
class CallbackAuthenticationPolicy (object ):
@@ -108,7 +106,6 @@ def effective_principals(self, request):
108
106
)
109
107
return effective_principals
110
108
111
-
112
109
class RepozeWho1AuthenticationPolicy (CallbackAuthenticationPolicy ):
113
110
""" A :app:`Pyramid` :term:`authentication policy` which
114
111
obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the
@@ -392,6 +389,140 @@ def b64encode(v):
392
389
def b64decode (v ):
393
390
return v .decode ('base64' )
394
391
392
+ # this class licensed under the MIT license (stolen from Paste)
393
+ class AuthTicket (object ):
394
+ """
395
+ This class represents an authentication token. You must pass in
396
+ the shared secret, the userid, and the IP address. Optionally you
397
+ can include tokens (a list of strings, representing role names),
398
+ 'user_data', which is arbitrary data available for your own use in
399
+ later scripts. Lastly, you can override the cookie name and
400
+ timestamp.
401
+
402
+ Once you provide all the arguments, use .cookie_value() to
403
+ generate the appropriate authentication ticket.
404
+
405
+ CGI usage::
406
+
407
+ token = auth_tkt.AuthTick('sharedsecret', 'username',
408
+ os.environ['REMOTE_ADDR'], tokens=['admin'])
409
+ print 'Status: 200 OK'
410
+ print 'Content-type: text/html'
411
+ print token.cookie()
412
+ print
413
+ ... redirect HTML ...
414
+
415
+ Webware usage::
416
+
417
+ token = auth_tkt.AuthTick('sharedsecret', 'username',
418
+ self.request().environ()['REMOTE_ADDR'], tokens=['admin'])
419
+ self.response().setCookie('auth_tkt', token.cookie_value())
420
+ """
421
+
422
+ def __init__ (self , secret , userid , ip , tokens = (), user_data = '' ,
423
+ time = None , cookie_name = 'auth_tkt' ,
424
+ secure = False ):
425
+ self .secret = secret
426
+ self .userid = userid
427
+ self .ip = ip
428
+ self .tokens = ',' .join (tokens )
429
+ self .user_data = user_data
430
+ if time is None :
431
+ self .time = time_mod .time ()
432
+ else :
433
+ self .time = time
434
+ self .cookie_name = cookie_name
435
+ self .secure = secure
436
+
437
+ def digest (self ):
438
+ return calculate_digest (
439
+ self .ip , self .time , self .secret , self .userid , self .tokens ,
440
+ self .user_data )
441
+
442
+ def cookie_value (self ):
443
+ v = '%s%08x%s!' % (self .digest (), int (self .time ),
444
+ urllib .quote (self .userid ))
445
+ if self .tokens :
446
+ v += self .tokens + '!'
447
+ v += self .user_data
448
+ return v
449
+
450
+ # this class licensed under the MIT license (stolen from Paste)
451
+ class BadTicket (Exception ):
452
+ """
453
+ Exception raised when a ticket can't be parsed. If we get far enough to
454
+ determine what the expected digest should have been, expected is set.
455
+ This should not be shown by default, but can be useful for debugging.
456
+ """
457
+ def __init__ (self , msg , expected = None ):
458
+ self .expected = expected
459
+ Exception .__init__ (self , msg )
460
+
461
+ # this function licensed under the MIT license (stolen from Paste)
462
+ def parse_ticket (secret , ticket , ip ):
463
+ """
464
+ Parse the ticket, returning (timestamp, userid, tokens, user_data).
465
+
466
+ If the ticket cannot be parsed, a ``BadTicket`` exception will be raised
467
+ with an explanation.
468
+ """
469
+ ticket = ticket .strip ('"' )
470
+ digest = ticket [:32 ]
471
+ try :
472
+ timestamp = int (ticket [32 :40 ], 16 )
473
+ except ValueError , e :
474
+ raise BadTicket ('Timestamp is not a hex integer: %s' % e )
475
+ try :
476
+ userid , data = ticket [40 :].split ('!' , 1 )
477
+ except ValueError :
478
+ raise BadTicket ('userid is not followed by !' )
479
+ userid = urllib .unquote (userid )
480
+ if '!' in data :
481
+ tokens , user_data = data .split ('!' , 1 )
482
+ else : # pragma: no cover (never generated)
483
+ # @@: Is this the right order?
484
+ tokens = ''
485
+ user_data = data
486
+
487
+ expected = calculate_digest (ip , timestamp , secret ,
488
+ userid , tokens , user_data )
489
+
490
+ if expected != digest :
491
+ raise BadTicket ('Digest signature is not correct' ,
492
+ expected = (expected , digest ))
493
+
494
+ tokens = tokens .split (',' )
495
+
496
+ return (timestamp , userid , tokens , user_data )
497
+
498
+ # this function licensed under the MIT license (stolen from Paste)
499
+ def calculate_digest (ip , timestamp , secret , userid , tokens , user_data ):
500
+ secret = maybe_encode (secret )
501
+ userid = maybe_encode (userid )
502
+ tokens = maybe_encode (tokens )
503
+ user_data = maybe_encode (user_data )
504
+ digest0 = md5 (
505
+ encode_ip_timestamp (ip , timestamp ) + secret + userid + '\0 '
506
+ + tokens + '\0 ' + user_data ).hexdigest ()
507
+ digest = md5 (digest0 + secret ).hexdigest ()
508
+ return digest
509
+
510
+ # this function licensed under the MIT license (stolen from Paste)
511
+ def encode_ip_timestamp (ip , timestamp ):
512
+ ip_chars = '' .join (map (chr , map (int , ip .split ('.' ))))
513
+ t = int (timestamp )
514
+ ts = ((t & 0xff000000 ) >> 24 ,
515
+ (t & 0xff0000 ) >> 16 ,
516
+ (t & 0xff00 ) >> 8 ,
517
+ t & 0xff )
518
+ ts_chars = '' .join (map (chr , ts ))
519
+ return ip_chars + ts_chars
520
+
521
+ def maybe_encode (s , encoding = 'utf8' ):
522
+ if isinstance (s , unicode ):
523
+ s = s .encode (encoding )
524
+ return s
525
+
395
526
EXPIRE = object ()
396
527
397
528
class AuthTktCookieHelper (object ):
@@ -401,7 +532,9 @@ class AuthTktCookieHelper(object):
401
532
:class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
402
533
meanings of the constructor arguments.
403
534
"""
404
- auth_tkt = auth_tkt # for tests
535
+ parse_ticket = staticmethod (parse_ticket ) # for tests
536
+ AuthTicket = AuthTicket # for tests
537
+ BadTicket = BadTicket # for tests
405
538
now = None # for tests
406
539
407
540
userid_type_decoders = {
@@ -487,10 +620,9 @@ def identify(self, request):
487
620
""" Return a dictionary with authentication information, or ``None``
488
621
if no valid auth_tkt is attached to ``request``"""
489
622
environ = request .environ
490
- cookies = get_cookies (environ )
491
- cookie = cookies .get (self .cookie_name )
623
+ cookie = request .cookies .get (self .cookie_name )
492
624
493
- if cookie is None or not cookie . value :
625
+ if cookie is None :
494
626
return None
495
627
496
628
if self .include_ip :
@@ -499,15 +631,15 @@ def identify(self, request):
499
631
remote_addr = '0.0.0.0'
500
632
501
633
try :
502
- timestamp , userid , tokens , user_data = self .auth_tkt . parse_ticket (
503
- self .secret , cookie . value , remote_addr )
504
- except self .auth_tkt . BadTicket :
634
+ timestamp , userid , tokens , user_data = self .parse_ticket (
635
+ self .secret , cookie , remote_addr )
636
+ except self .BadTicket :
505
637
return None
506
638
507
639
now = self .now # service tests
508
640
509
641
if now is None :
510
- now = time .time ()
642
+ now = time_mod .time ()
511
643
512
644
if self .timeout and ( (timestamp + self .timeout ) < now ):
513
645
# the auth_tkt data has expired
@@ -592,7 +724,7 @@ def remember(self, request, userid, max_age=None, tokens=()):
592
724
if not (isinstance (token , str ) and VALID_TOKEN .match (token )):
593
725
raise ValueError ("Invalid token %r" % (token ,))
594
726
595
- ticket = self .auth_tkt . AuthTicket (
727
+ ticket = self .AuthTicket (
596
728
self .secret ,
597
729
userid ,
598
730
remote_addr ,
@@ -655,3 +787,5 @@ def forget(self, request):
655
787
def unauthenticated_userid (self , request ):
656
788
return request .session .get (self .userid_key )
657
789
790
+
791
+ # 14a3263f21e58dc0c1a4c994ab640bff4e6448d1ZWRpdG9y!userid_type:b64unicode
0 commit comments