Tres Seaver
2016-05-31 2d1f2cf61df8f507f9ed605d28c63af6e2ddc85a
commit | author | age
02a504 1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
AO 2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3 ##########################################################################
4 #
5 # Copyright (c) 2005 Imaginary Landscape LLC and Contributors.
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining
8 # a copy of this software and associated documentation files (the
9 # "Software"), to deal in the Software without restriction, including
10 # without limitation the rights to use, copy, modify, merge, publish,
11 # distribute, sublicense, and/or sell copies of the Software, and to
12 # permit persons to whom the Software is furnished to do so, subject to
13 # the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be
16 # included in all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 ##########################################################################
26 """
27 Implementation of cookie signing as done in `mod_auth_tkt
28 <http://www.openfusion.com.au/labs/mod_auth_tkt/>`_.
29
30 mod_auth_tkt is an Apache module that looks for these signed cookies
31 and sets ``REMOTE_USER``, ``REMOTE_USER_TOKENS`` (a comma-separated
32 list of groups) and ``REMOTE_USER_DATA`` (arbitrary string data).
33
34 This module is an alternative to the ``paste.auth.cookie`` module;
35 it's primary benefit is compatibility with mod_auth_tkt, which in turn
36 makes it possible to use the same authentication process with
37 non-Python code run under Apache.
38 """
2d1f2c 39 import hashlib
02a504 40 import time as time_mod
6919cb 41
0b4b05 42 from repoze.who._compat import encodestring
adef05 43 from repoze.who._compat import SimpleCookie
TS 44 from repoze.who._compat import url_quote
45 from repoze.who._compat import url_unquote
fcf53b 46
DT 47 DEFAULT_DIGEST = hashlib.md5
02a504 48
AO 49
50 class AuthTicket(object):
51
52     """
53     This class represents an authentication token.  You must pass in
54     the shared secret, the userid, and the IP address.  Optionally you
55     can include tokens (a list of strings, representing role names),
56     'user_data', which is arbitrary data available for your own use in
fcf53b 57     later scripts.  Lastly, you can override the timestamp, cookie name,
DT 58     whether to secure the cookie and the digest algorithm (for details
59     look at ``AuthTKTMiddleware``).
02a504 60
AO 61     Once you provide all the arguments, use .cookie_value() to
62     generate the appropriate authentication ticket.  .cookie()
63     generates a Cookie object, the str() of which is the complete
64     cookie header to be sent.
65
66     CGI usage::
67
68         token = auth_tkt.AuthTick('sharedsecret', 'username',
69             os.environ['REMOTE_ADDR'], tokens=['admin'])
fcf53b 70         print('Status: 200 OK')
DT 71         print('Content-type: text/html')
72         print(token.cookie())
73         print("")
02a504 74         ... redirect HTML ...
AO 75
76     Webware usage::
77
78         token = auth_tkt.AuthTick('sharedsecret', 'username',
79             self.request().environ()['REMOTE_ADDR'], tokens=['admin'])
80         self.response().setCookie('auth_tkt', token.cookie_value())
81
82     Be careful not to do an HTTP redirect after login; use meta
83     refresh or Javascript -- some browsers have bugs where cookies
84     aren't saved when set on a redirect.
85     """
86
87     def __init__(self, secret, userid, ip, tokens=(), user_data='',
88                  time=None, cookie_name='auth_tkt',
fcf53b 89                  secure=False, digest_algo=DEFAULT_DIGEST):
02a504 90         self.secret = secret
AO 91         self.userid = userid
92         self.ip = ip
93         self.tokens = ','.join(tokens)
94         self.user_data = user_data
95         if time is None:
96             self.time = time_mod.time()
97         else:
98             self.time = time
99         self.cookie_name = cookie_name
100         self.secure = secure
fcf53b 101         if isinstance(digest_algo, str):
DT 102             # correct specification of digest from hashlib or fail
103             self.digest_algo = getattr(hashlib, digest_algo)
104         else:
105             self.digest_algo = digest_algo
02a504 106
AO 107     def digest(self):
108         return calculate_digest(
109             self.ip, self.time, self.secret, self.userid, self.tokens,
fcf53b 110             self.user_data, self.digest_algo)
02a504 111
AO 112     def cookie_value(self):
6919cb 113         v = '%s%08x%s!' % (self.digest(), int(self.time),
TS 114                            url_quote(self.userid))
02a504 115         if self.tokens:
AO 116             v += self.tokens + '!'
117         v += self.user_data
118         return v
119
120     def cookie(self):
6919cb 121         c = SimpleCookie()
0b4b05 122         c_val = encodestring(self.cookie_value())
TS 123         c_val = c_val.strip().replace('\n', '')
6919cb 124         c[self.cookie_name] = c_val
02a504 125         c[self.cookie_name]['path'] = '/'
AO 126         if self.secure:
127             c[self.cookie_name]['secure'] = 'true'
128         return c
129
130
131 class BadTicket(Exception):
132     """
133     Exception raised when a ticket can't be parsed.  If we get
134     far enough to determine what the expected digest should have
135     been, expected is set.  This should not be shown by default,
136     but can be useful for debugging.
137     """
138     def __init__(self, msg, expected=None):
139         self.expected = expected
140         Exception.__init__(self, msg)
141
142
fcf53b 143 def parse_ticket(secret, ticket, ip, digest_algo=DEFAULT_DIGEST):
02a504 144     """
AO 145     Parse the ticket, returning (timestamp, userid, tokens, user_data).
146
147     If the ticket cannot be parsed, ``BadTicket`` will be raised with
148     an explanation.
149     """
fcf53b 150     if isinstance(digest_algo, str):
DT 151         # correct specification of digest from hashlib or fail
152         digest_algo = getattr(hashlib, digest_algo)
153     digest_hexa_size = digest_algo().digest_size * 2
02a504 154     ticket = ticket.strip('"')
fcf53b 155     digest = ticket[:digest_hexa_size]
02a504 156     try:
fcf53b 157         timestamp = int(ticket[digest_hexa_size:digest_hexa_size + 8], 16)
6919cb 158     except ValueError as e:
02a504 159         raise BadTicket('Timestamp is not a hex integer: %s' % e)
AO 160     try:
fcf53b 161         userid, data = ticket[digest_hexa_size + 8:].split('!', 1)
02a504 162     except ValueError:
AO 163         raise BadTicket('userid is not followed by !')
164     userid = url_unquote(userid)
165     if '!' in data:
166         tokens, user_data = data.split('!', 1)
167     else:
168         # @@: Is this the right order?
169         tokens = ''
170         user_data = data
171
172     expected = calculate_digest(ip, timestamp, secret,
fcf53b 173                                 userid, tokens, user_data,
DT 174                                 digest_algo)
02a504 175
AO 176     if expected != digest:
177         raise BadTicket('Digest signature is not correct',
178                         expected=(expected, digest))
179
180     tokens = tokens.split(',')
181
182     return (timestamp, userid, tokens, user_data)
183
184
fcf53b 185 def calculate_digest(ip, timestamp, secret, userid, tokens, user_data,
DT 186                      digest_algo):
02a504 187     secret = maybe_encode(secret)
AO 188     userid = maybe_encode(userid)
189     tokens = maybe_encode(tokens)
190     user_data = maybe_encode(user_data)
fcf53b 191     digest0 = digest_algo(
8f1fc8 192         encode_ip_timestamp(ip, timestamp) + secret + userid + b'\0'
TS 193         + tokens + b'\0' + user_data).hexdigest()
fcf53b 194     digest = digest_algo(maybe_encode(digest0) + secret).hexdigest()
02a504 195     return digest
AO 196
197
ff80e0 198 if type(chr(1)) == type(b''): #pragma NO COVER Python < 3.0
8f1fc8 199     def ints2bytes(ints):
TS 200         return b''.join(map(chr, ints))
201 else: #pragma NO COVER Python >= 3.0
202     def ints2bytes(ints):
203         return bytes(ints)
204
02a504 205 def encode_ip_timestamp(ip, timestamp):
8f1fc8 206     ip_chars = ints2bytes(map(int, ip.split('.')))
02a504 207     t = int(timestamp)
AO 208     ts = ((t & 0xff000000) >> 24,
209           (t & 0xff0000) >> 16,
210           (t & 0xff00) >> 8,
211           t & 0xff)
8f1fc8 212     ts_chars = ints2bytes(ts)
02a504 213     return ip_chars + ts_chars
AO 214
215
216 def maybe_encode(s, encoding='utf8'):
d6391c 217     if not isinstance(s, type(b'')):
02a504 218         s = s.encode(encoding)
AO 219     return s
220
221
ff80e0 222 # Original Paste AuthTktMiddleware stripped:  we don't have a use for it.