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. |