1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
"""
Login/authentication middleware
NOT YET FINISHED
"""
import wsgilib
import sha
from paste.deploy import converters
from paste.util import import_string
def middleware(
application,
global_conf,
http_login=False,
http_realm='Secure Website',
http_overwrite_realm=True,
http_and_cookie=True,
cookie_prefix='',
login_page='_login/login_form',
logout_page='_login/logout_form',
secret=None,
authenticator=None,
):
"""
Configuration:
http_login:
If true, then we'll prefer HTTP Basic logins, passing a 401 to
the user. If false, we'll use form logins with Cookie
authentication.
http_realm:
The realm to use. If http_overwrite_realm is true then we will
force this to be the realm (even if the application supplies
its own realm).
http_and_cookie:
If true, we'll give the user a login cookie even if they use
HTTP. Then we don't have to throw a 401 on every page to get
them to re-login.
cookie_prefix:
Used before all cookie names; like a domain.
login_page:
If using cookie login and we get a 401, we'll turn it into a
200 and do an internal redirect to this page (using recursive).
logout_page:
Ditto the logout (logout will at some point be triggered with
another key we add to the environment).
secret:
We use this for signing cookies. We'll generate it automatically
if it's not provided explicitly (set it explicitly to be sure
it is stable).
authenticator:
When we do HTTP logins we need to tell if they are using the
correct login immediately. See the Authenticator object for
the framework of an implementation.
When you require a login, return a 401 error. When a login has
occurred, the logged-in username will be in REMOTE_USER. When the
user is logged in, but denied access, use a 403 error (not a 401).
It might be useful to have another middleware that wraps an application
and returns a 401 error, based on parsing the URL.
Currently, the login form, if used, is rendered at the URL requested
by the user, instead of issuing an HTTP redirect. This will require
some attention to caching issues, but allows forms to be POSTed without
losing data after the login (as long as the login page contains the
appropriate hidden fields.)
Also, the cookie is not deleted on an unsuccessful login attempt.
The cookie is issued with path '/' and no expiration date. This
should probably be overridable.
Environment variables used:
paste.login.signer:
signer, created from UsernameSigner class
paste.login._dologin:
user name to be logged in, either from HTTP auth
or from form submission (XXX form not implement)
paste.login._doredirect:
login page to which to redirect
paste.login._loginredirect:
set to True iff _doredirect set and login_page is
relative, else undefined. Used where?
"""
http_login = converters.asbool(http_login)
http_overwrite_realm = converters.asbool(http_overwrite_realm)
http_and_cookie = converters.asbool(http_and_cookie)
if authenticator and isinstance(authenticator, (str, unicode)):
authenticator = import_string.eval_import(authenticator)
if http_login:
assert authenticator, (
"You must provide an authenticator argument if you "
"are using http_login")
if secret is None:
secret = global_conf.get('secret')
if secret is None:
secret = create_secret()
cookie_name = cookie_prefix + '_login_auth'
signer = UsernameSigner(secret)
def login_application(environ, start_response):
orig_script_name = environ['SCRIPT_NAME']
orig_path_info = environ['PATH_INFO']
cookies = wsgilib.get_cookies(environ)
cookie = cookies.get(cookie_name)
username = None
environ['paste.login.signer'] = signer
if cookie and cookie.value:
username = signer.check_signature(
cookie.value, environ['wsgi.errors'])
authenticatee = (
environ.get('HTTP_AUTHORIZATION') or
environ.get('HTTP_CGI_AUTHORIZATION'))
if (not username
and authenticator
and authenticatee):
username = authenticator().check_basic_auth(authenticatee)
if http_and_cookie:
environ['paste.login._dologin'] = username
if username:
environ['REMOTE_USER'] = username
def login_start_response(status, headers, exc_info=None):
if environ.get('paste.login._dologin'):
cookie = SimpleCookie(cookie_name,
signer.make_signature(username),
'/')
headers.append(('Set-Cookie', str(cookie)))
del environ['paste.login._dologin']
status_int = int(status.split(None, 1)[0].strip())
if status_int == 401 and http_login:
if (http_overwrite_realm
or not wsgilib.has_header(headers, 'www-authenticate')):
headers.append(('WWW-Authenticate', 'Basic realm="%s"' % http_realm))
elif status_int == 401:
status = '200 OK'
if login_page.startswith('/'):
assert environ.has_key('paste.recursive.include'), (
"You must use the recursive middleware to "
"use a non-relative page for the login_page")
environ['paste.login._doredirect'] = login_page
return garbage_writer
return start_response(status, headers, exc_info)
app_iter = application(environ, login_start_response)
if environ.get('paste.login._doredirect'):
page_name = environ['paste.login._doredirect']
del environ['paste.login._doredirect']
eat_app_iter(app_iter)
if login_page.startswith('/'):
app_iter = environ['paste.recursive.forward'](
login_page[1:])
else:
# Don't use recursive, since login page is
# internal to
new_environ = environ.copy()
new_environ['SCRIPT_NAME'] = orig_script_name
new_environ['PATH_INFO'] = '/' + login_page
new_environ['paste.login._loginredirect'] = True
app_iter = login_application(new_environ, start_response)
return app_iter
return login_application
def encodestrip(s):
return s.encode('base64').strip('\n')
class UsernameSigner(object):
def __init__(self, secret):
self.secret = secret
def digest(self, username):
return sha.new(self.secret+username).digest()
def __call__(self, username):
return encodestrip(self.digest(username))
def check_signature(self, b64value, errors):
value = b64value.decode('base64')
if ' ' not in value:
errors.write('Badly formatted cookie: %r\n' % value)
return None
signature, username = value.split(' ', 1)
sig_hash = self.digest(username)
if sig_hash == signature:
return username
errors.write('Bad signature: %r\n' % value)
return None
def make_signature(self, username):
return encodestrip(self.digest(username) + " " + username)
def login_user(self, username, environ):
"""
Adds a username so that the login middleware will later set
the user to be logged in (with a cookie).
"""
environ['paste.login._dologin'] = username
class SimpleCookie(object):
def __init__(self, cookie_name, signed_val, path):
self.cookie_name = cookie_name
self.signed_val = signed_val
self.path = '/'
def __str__(self):
return "%s=%s; Path=%s" % (self.cookie_name,
self.signed_val, self.path)
class Authenticator(object):
"""
This is the basic framework for an authenticating object.
"""
def check_basic_auth(self, auth):
"""Returns either the authenticated username or, if unauthorized,
None."""
assert auth.lower().startswith('basic ')
type, auth = auth.split()
auth = auth.strip().decode('base64')
username, password = auth.split(':')
if self.check_auth(username, password):
return username
return None
def check_auth(self, username, password):
raise NotImplementedError
########################################
## Utility functions
########################################
def create_secret():
# @@: obviously not a good secret generator: should be randomized
# somehow, and maybe store the secret somewhere for later use.
return 'secret'
def garbage_writer(s):
"""
When we don't care about the written output.
"""
pass
def eat_app_iter(app_iter):
"""
When we don't care about the iterated output.
"""
try:
for s in app_iter:
pass
finally:
if hasattr(app_iter, 'close'):
app_iter.close()
|