commit | author | age
|
299b4c
|
1 |
import datetime |
4f3525
|
2 |
from calendar import timegm |
TS |
3 |
from email.utils import formatdate |
b95a59
|
4 |
from codecs import utf_8_decode |
CM |
5 |
from codecs import utf_8_encode |
0ee58d
|
6 |
import os |
cd1198
|
7 |
import time |
7483ec
|
8 |
try: |
DT |
9 |
import hashlib |
|
10 |
except ImportError: |
|
11 |
import md5 as hashlib # Will only support md5 algorithm |
4f3525
|
12 |
from wsgiref.handlers import _monthname # Locale-independent, RFC-2616 |
TS |
13 |
from wsgiref.handlers import _weekdayname # Locale-independent, RFC-2616 |
8729b3
|
14 |
try: |
J |
15 |
from urllib.parse import urlencode, parse_qsl |
|
16 |
except ImportError: |
|
17 |
from urllib import urlencode |
|
18 |
from urlparse import parse_qsl |
b95a59
|
19 |
|
1fa560
|
20 |
from zope.interface import implementer |
a5b033
|
21 |
|
cb5426
|
22 |
from repoze.who.interfaces import IIdentifier |
b0f81f
|
23 |
from repoze.who.interfaces import IAuthenticator |
6919cb
|
24 |
from repoze.who._compat import get_cookies |
TS |
25 |
import repoze.who._auth_tkt as auth_tkt |
9df42f
|
26 |
from repoze.who._compat import STRING_TYPES |
d6fa0a
|
27 |
from repoze.who._compat import u |
a5b033
|
28 |
|
4f3525
|
29 |
_UTCNOW = None # unit tests can replace |
TS |
30 |
def _utcnow(): #pragma NO COVERAGE |
|
31 |
if _UTCNOW is not None: |
|
32 |
return _UTCNOW |
|
33 |
return datetime.datetime.utcnow() |
798feb
|
34 |
|
7483ec
|
35 |
DEFAULT_DIGEST = hashlib.md5 |
e43169
|
36 |
|
1fa560
|
37 |
@implementer(IIdentifier, IAuthenticator) |
a5b033
|
38 |
class AuthTktCookiePlugin(object): |
779caf
|
39 |
|
0fa6f9
|
40 |
userid_typename = 'userid_type' |
d6fa0a
|
41 |
userid_type_decoders = {'int': int, |
TS |
42 |
'unicode': lambda x: utf_8_decode(x)[0], |
|
43 |
} |
779caf
|
44 |
|
d6fa0a
|
45 |
userid_type_encoders = {int: ('int', str), |
TS |
46 |
} |
6919cb
|
47 |
try: |
TS |
48 |
userid_type_encoders[long] = ('int', str) |
|
49 |
except NameError: #pragma NO COVER Python >= 3.0 |
|
50 |
pass |
|
51 |
try: |
|
52 |
userid_type_encoders[unicode] = ('unicode', |
|
53 |
lambda x: utf_8_encode(x)[0]) |
|
54 |
except NameError: #pragma NO COVER Python >= 3.0 |
|
55 |
pass |
|
56 |
|
a5b033
|
57 |
def __init__(self, secret, cookie_name='auth_tkt', |
cd1198
|
58 |
secure=False, include_ip=False, |
7483ec
|
59 |
timeout=None, reissue_time=None, userid_checker=None, |
DT |
60 |
digest_algo=DEFAULT_DIGEST): |
a5b033
|
61 |
self.secret = secret |
CM |
62 |
self.cookie_name = cookie_name |
|
63 |
self.include_ip = include_ip |
|
64 |
self.secure = secure |
cd1198
|
65 |
if timeout and ( (not reissue_time) or (reissue_time > timeout) ): |
CM |
66 |
raise ValueError('When timeout is specified, reissue_time must ' |
|
67 |
'be set to a lower value') |
|
68 |
self.timeout = timeout |
|
69 |
self.reissue_time = reissue_time |
a6f6dc
|
70 |
self.userid_checker = userid_checker |
7483ec
|
71 |
self.digest_algo = digest_algo |
a5b033
|
72 |
|
CM |
73 |
# IIdentifier |
|
74 |
def identify(self, environ): |
|
75 |
cookies = get_cookies(environ) |
|
76 |
cookie = cookies.get(self.cookie_name) |
|
77 |
|
|
78 |
if cookie is None or not cookie.value: |
40a968
|
79 |
return None |
a5b033
|
80 |
|
CM |
81 |
if self.include_ip: |
|
82 |
remote_addr = environ['REMOTE_ADDR'] |
|
83 |
else: |
|
84 |
remote_addr = '0.0.0.0' |
|
85 |
|
|
86 |
try: |
|
87 |
timestamp, userid, tokens, user_data = auth_tkt.parse_ticket( |
7483ec
|
88 |
self.secret, cookie.value, remote_addr, self.digest_algo) |
a5b033
|
89 |
except auth_tkt.BadTicket: |
a6f6dc
|
90 |
return None |
CM |
91 |
|
cd1198
|
92 |
if self.timeout and ( (timestamp + self.timeout) < time.time() ): |
40a968
|
93 |
return None |
779caf
|
94 |
|
0fa6f9
|
95 |
user_data_dict = dict(parse_qsl(user_data)) |
JV |
96 |
userid_type = user_data_dict.get(self.userid_typename) |
|
97 |
if userid_type: |
|
98 |
decoder = self.userid_type_decoders.get(userid_type) |
|
99 |
if decoder: |
|
100 |
userid = decoder(userid) |
a5b033
|
101 |
|
CM |
102 |
environ['REMOTE_USER_TOKENS'] = tokens |
|
103 |
environ['REMOTE_USER_DATA'] = user_data |
|
104 |
environ['AUTH_TYPE'] = 'cookie' |
56d0c5
|
105 |
|
a5b033
|
106 |
identity = {} |
CM |
107 |
identity['timestamp'] = timestamp |
b0f81f
|
108 |
identity['repoze.who.plugins.auth_tkt.userid'] = userid |
a5b033
|
109 |
identity['tokens'] = tokens |
0fa6f9
|
110 |
identity['userdata'] = user_data_dict |
a5b033
|
111 |
return identity |
519300
|
112 |
|
CM |
113 |
# IIdentifier |
|
114 |
def forget(self, environ, identity): |
|
115 |
# return a set of expires Set-Cookie headers |
798feb
|
116 |
return self._get_cookies(environ, 'INVALID', 0) |
a5b033
|
117 |
|
CM |
118 |
# IIdentifier |
|
119 |
def remember(self, environ, identity): |
|
120 |
if self.include_ip: |
|
121 |
remote_addr = environ['REMOTE_ADDR'] |
|
122 |
else: |
|
123 |
remote_addr = '0.0.0.0' |
|
124 |
|
|
125 |
cookies = get_cookies(environ) |
|
126 |
old_cookie = cookies.get(self.cookie_name) |
|
127 |
existing = cookies.get(self.cookie_name) |
|
128 |
old_cookie_value = getattr(existing, 'value', None) |
299b4c
|
129 |
max_age = identity.get('max_age', None) |
a5b033
|
130 |
|
fc9a88
|
131 |
timestamp, userid, tokens, userdata = None, '', (), '' |
a5b033
|
132 |
|
CM |
133 |
if old_cookie_value: |
|
134 |
try: |
|
135 |
timestamp,userid,tokens,userdata = auth_tkt.parse_ticket( |
7483ec
|
136 |
self.secret, old_cookie_value, remote_addr, |
DT |
137 |
self.digest_algo) |
a5b033
|
138 |
except auth_tkt.BadTicket: |
CM |
139 |
pass |
fc9a88
|
140 |
tokens = tuple(tokens) |
a5b033
|
141 |
|
cb5426
|
142 |
who_userid = identity['repoze.who.userid'] |
fc9a88
|
143 |
who_tokens = tuple(identity.get('tokens', ())) |
0fa6f9
|
144 |
who_userdata_dict = identity.get('userdata', {}) |
779caf
|
145 |
|
CM |
146 |
encoding_data = self.userid_type_encoders.get(type(who_userid)) |
|
147 |
if encoding_data: |
|
148 |
encoding, encoder = encoding_data |
|
149 |
who_userid = encoder(who_userid) |
0fa6f9
|
150 |
who_userdata_dict[self.userid_typename] = encoding |
JV |
151 |
|
|
152 |
who_userdata = urlencode(who_userdata_dict) |
|
153 |
|
a5b033
|
154 |
old_data = (userid, tokens, userdata) |
cb5426
|
155 |
new_data = (who_userid, who_tokens, who_userdata) |
a5b033
|
156 |
|
cd1198
|
157 |
if old_data != new_data or (self.reissue_time and |
CM |
158 |
( (timestamp + self.reissue_time) < time.time() )): |
a5b033
|
159 |
ticket = auth_tkt.AuthTicket( |
CM |
160 |
self.secret, |
cb5426
|
161 |
who_userid, |
a5b033
|
162 |
remote_addr, |
cb5426
|
163 |
tokens=who_tokens, |
CM |
164 |
user_data=who_userdata, |
a5b033
|
165 |
cookie_name=self.cookie_name, |
7483ec
|
166 |
secure=self.secure, |
DT |
167 |
digest_algo=self.digest_algo) |
a5b033
|
168 |
new_cookie_value = ticket.cookie_value() |
519300
|
169 |
|
a5b033
|
170 |
if old_cookie_value != new_cookie_value: |
519300
|
171 |
# return a set of Set-Cookie headers |
299b4c
|
172 |
return self._get_cookies(environ, new_cookie_value, max_age) |
a5b033
|
173 |
|
b0f81f
|
174 |
# IAuthenticator |
TS |
175 |
def authenticate(self, environ, identity): |
|
176 |
userid = identity.get('repoze.who.plugins.auth_tkt.userid') |
|
177 |
if userid is None: |
|
178 |
return None |
|
179 |
if self.userid_checker and not self.userid_checker(userid): |
|
180 |
return None |
|
181 |
identity['repoze.who.userid'] = userid |
|
182 |
return userid |
|
183 |
|
|
184 |
def _get_cookies(self, environ, value, max_age=None): |
|
185 |
if max_age is not None: |
15e365
|
186 |
max_age = int(max_age) |
4f3525
|
187 |
later = _utcnow() + datetime.timedelta(seconds=max_age) |
b0f81f
|
188 |
# Wdy, DD-Mon-YY HH:MM:SS GMT |
4f3525
|
189 |
expires = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( |
TS |
190 |
_weekdayname[later.weekday()], |
|
191 |
later.day, |
|
192 |
_monthname[later.month], |
|
193 |
later.year, |
|
194 |
later.hour, |
|
195 |
later.minute, |
|
196 |
later.second, |
|
197 |
) |
b0f81f
|
198 |
# the Expires header is *required* at least for IE7 (IE7 does |
TS |
199 |
# not respect Max-Age) |
|
200 |
max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires) |
|
201 |
else: |
|
202 |
max_age = '' |
|
203 |
|
d7e647
|
204 |
secure = '' |
BS |
205 |
if self.secure: |
3b5782
|
206 |
secure = '; secure; HttpOnly' |
d7e647
|
207 |
|
b0f81f
|
208 |
cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) |
7f9907
|
209 |
cur_domain = cur_domain.split(':')[0] # drop port |
b0f81f
|
210 |
wild_domain = '.' + cur_domain |
TS |
211 |
cookies = [ |
d7e647
|
212 |
('Set-Cookie', '%s="%s"; Path=/%s%s' % ( |
BS |
213 |
self.cookie_name, value, max_age, secure)), |
|
214 |
('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % ( |
|
215 |
self.cookie_name, value, cur_domain, max_age, secure)), |
|
216 |
('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % ( |
|
217 |
self.cookie_name, value, wild_domain, max_age, secure)) |
b0f81f
|
218 |
] |
TS |
219 |
return cookies |
|
220 |
|
a5b033
|
221 |
def __repr__(self): |
394ea6
|
222 |
return '<%s %s>' % (self.__class__.__name__, |
TS |
223 |
id(self)) #pragma NO COVERAGE |
a5b033
|
224 |
|
515c69
|
225 |
def _bool(value): |
9df42f
|
226 |
if isinstance(value, STRING_TYPES): |
515c69
|
227 |
return value.lower() in ('yes', 'true', '1') |
TS |
228 |
return value |
|
229 |
|
|
230 |
def make_plugin(secret=None, |
0ee58d
|
231 |
secretfile=None, |
a5b033
|
232 |
cookie_name='auth_tkt', |
0ee58d
|
233 |
secure=False, |
TS |
234 |
include_ip=False, |
cd1198
|
235 |
timeout=None, |
CM |
236 |
reissue_time=None, |
a6f6dc
|
237 |
userid_checker=None, |
7483ec
|
238 |
digest_algo=DEFAULT_DIGEST, |
0ee58d
|
239 |
): |
a6f6dc
|
240 |
from repoze.who.utils import resolveDotted |
0ee58d
|
241 |
if (secret is None and secretfile is None): |
TS |
242 |
raise ValueError("One of 'secret' or 'secretfile' must not be None.") |
|
243 |
if (secret is not None and secretfile is not None): |
|
244 |
raise ValueError("Specify only one of 'secret' or 'secretfile'.") |
|
245 |
if secretfile: |
|
246 |
secretfile = os.path.abspath(os.path.expanduser(secretfile)) |
|
247 |
if not os.path.exists(secretfile): |
|
248 |
raise ValueError("No such 'secretfile': %s" % secretfile) |
c272ce
|
249 |
with open(secretfile) as f: |
TS |
250 |
secret = f.read().strip() |
cd1198
|
251 |
if timeout: |
CM |
252 |
timeout = int(timeout) |
|
253 |
if reissue_time: |
|
254 |
reissue_time = int(reissue_time) |
a6f6dc
|
255 |
if userid_checker is not None: |
CM |
256 |
userid_checker = resolveDotted(userid_checker) |
7483ec
|
257 |
if isinstance(digest_algo, str): |
DT |
258 |
try: |
|
259 |
digest_algo = getattr(hashlib, digest_algo) |
|
260 |
except AttributeError: |
|
261 |
raise ValueError("No such 'digest_algo': %s" % digest_algo) |
|
262 |
|
cd1198
|
263 |
plugin = AuthTktCookiePlugin(secret, |
CM |
264 |
cookie_name, |
|
265 |
_bool(secure), |
|
266 |
_bool(include_ip), |
|
267 |
timeout, |
|
268 |
reissue_time, |
a6f6dc
|
269 |
userid_checker, |
7483ec
|
270 |
digest_algo, |
cd1198
|
271 |
) |
a5b033
|
272 |
return plugin |
CM |
273 |
|