Skip to content

Commit bd0c7a6

Browse files
committed
first cut at moving paste.auth stuff into core
1 parent 0508684 commit bd0c7a6

File tree

4 files changed

+380
-128
lines changed

4 files changed

+380
-128
lines changed

CHANGES.txt

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
Next release
2+
============
3+
4+
Internal
5+
--------
6+
7+
- Internalize code previously depended upon as imports from the
8+
``paste.auth`` module (futureproof).
9+
110
1.2a5 (2011-09-04)
211
==================
312

LICENSE.txt

+24
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,27 @@ following license:
135135
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
136136
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
137137
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
138+
139+
Portions of the code marked as "stolen from Paste" are provided under the
140+
following license:
141+
142+
Copyright (c) 2006-2007 Ian Bicking and Contributors
143+
144+
Permission is hereby granted, free of charge, to any person obtaining
145+
a copy of this software and associated documentation files (the
146+
"Software"), to deal in the Software without restriction, including
147+
without limitation the rights to use, copy, modify, merge, publish,
148+
distribute, sublicense, and/or sell copies of the Software, and to
149+
permit persons to whom the Software is furnished to do so, subject to
150+
the following conditions:
151+
152+
The above copyright notice and this permission notice shall be
153+
included in all copies or substantial portions of the Software.
154+
155+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
156+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
157+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
158+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
159+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
160+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
161+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

pyramid/authentication.py

+149-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from codecs import utf_8_decode
22
from codecs import utf_8_encode
3+
from hashlib import md5
34
import datetime
45
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
98

109
from zope.interface import implements
1110

@@ -16,7 +15,6 @@
1615
from pyramid.security import Authenticated
1716
from pyramid.security import Everyone
1817

19-
2018
VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$")
2119

2220
class CallbackAuthenticationPolicy(object):
@@ -108,7 +106,6 @@ def effective_principals(self, request):
108106
)
109107
return effective_principals
110108

111-
112109
class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
113110
""" A :app:`Pyramid` :term:`authentication policy` which
114111
obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the
@@ -392,6 +389,140 @@ def b64encode(v):
392389
def b64decode(v):
393390
return v.decode('base64')
394391

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+
395526
EXPIRE = object()
396527

397528
class AuthTktCookieHelper(object):
@@ -401,7 +532,9 @@ class AuthTktCookieHelper(object):
401532
:class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
402533
meanings of the constructor arguments.
403534
"""
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
405538
now = None # for tests
406539

407540
userid_type_decoders = {
@@ -487,10 +620,9 @@ def identify(self, request):
487620
""" Return a dictionary with authentication information, or ``None``
488621
if no valid auth_tkt is attached to ``request``"""
489622
environ = request.environ
490-
cookies = get_cookies(environ)
491-
cookie = cookies.get(self.cookie_name)
623+
cookie = request.cookies.get(self.cookie_name)
492624

493-
if cookie is None or not cookie.value:
625+
if cookie is None:
494626
return None
495627

496628
if self.include_ip:
@@ -499,15 +631,15 @@ def identify(self, request):
499631
remote_addr = '0.0.0.0'
500632

501633
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:
505637
return None
506638

507639
now = self.now # service tests
508640

509641
if now is None:
510-
now = time.time()
642+
now = time_mod.time()
511643

512644
if self.timeout and ( (timestamp + self.timeout) < now ):
513645
# the auth_tkt data has expired
@@ -592,7 +724,7 @@ def remember(self, request, userid, max_age=None, tokens=()):
592724
if not (isinstance(token, str) and VALID_TOKEN.match(token)):
593725
raise ValueError("Invalid token %r" % (token,))
594726

595-
ticket = self.auth_tkt.AuthTicket(
727+
ticket = self.AuthTicket(
596728
self.secret,
597729
userid,
598730
remote_addr,
@@ -655,3 +787,5 @@ def forget(self, request):
655787
def unauthenticated_userid(self, request):
656788
return request.session.get(self.userid_key)
657789

790+
791+
# 14a3263f21e58dc0c1a4c994ab640bff4e6448d1ZWRpdG9y!userid_type:b64unicode

0 commit comments

Comments
 (0)