2
This file is part of lazr.authentication.
4
lazr.authentication is free software: you can redistribute it and/or modify it
5
under the terms of the GNU Lesser General Public License as published by
6
the Free Software Foundation, version 3 of the License.
8
lazr.authentication is distributed in the hope that it will be useful, but
9
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
11
License for more details.
13
You should have received a copy of the GNU Lesser General Public License
14
along with lazr.authentication. If not, see <http://www.gnu.org/licenses/>.
19
lazr.authentication defines some simple WSGI middleware for protecting
20
resources with different kinds of HTTP authentication.
22
>>> from __future__ import print_function
23
>>> import lazr.authentication
24
>>> print('VERSION: %s' % lazr.authentication.__version__)
31
The BasicAuthMiddleware implements HTTP Basic Auth. Its constructor
32
takes a number of arguments, including a callback function that
33
performs the actual authentication. This function returns an object
34
identifying the user who's trying to authenticate. If the
35
authentication credentials are invalid, it's supposed to return None.
37
First, let's create a really simple WSGI application that responds to
38
any request with a 200 response code.
40
>>> def dummy_application(environ, start_response):
41
... start_response('200', [('Content-type','text/plain')])
42
... return [b'Success']
44
Now let's protect that application. Here's an authentication callback
47
>>> def authenticate(username, password):
48
... """Accepts "user/password", rejects everything else.
50
... :return: The username, if the credentials are valid.
53
... if username == "user" and password == "password":
57
>>> print(authenticate("user", "password"))
60
>>> print(authenticate("notuser", "password"))
63
Here's a WSGI application that protects the application using
66
>>> from lazr.authentication.wsgi import BasicAuthMiddleware
67
>>> def protected_application():
68
... return BasicAuthMiddleware(
69
... dummy_application, realm="WSGI middleware test",
70
... protect_path_pattern=".*protected.*",
71
... authenticate_with=authenticate)
73
>>> import wsgi_intercept
74
>>> from wsgi_intercept.httplib2_intercept import install
76
>>> wsgi_intercept.add_wsgi_intercept(
77
... 'basictest', 80, protected_application)
79
Most of the application's URLs are not protected by the
80
middleware. You can access them without providing credentials.
83
>>> client = httplib2.Http()
84
>>> response, body = client.request('http://basictest/')
85
>>> print(response['status'])
87
>>> print(body.decode())
90
Any URL that includes the string "protected" is protected by the
91
middleware, and cannot be accessed without credentials.
93
>>> response, body = client.request('http://basictest/protected/')
94
>>> print(response['status'])
96
>>> print(response['www-authenticate'])
97
Basic realm="WSGI middleware test"
99
>>> response, body = client.request(
100
... 'http://basictest/this-is-protected-as-well/')
101
>>> print(response['status'])
104
The check_credentials() implementation given at the beginning of the
105
test will only accept the user/password combination "user"/"password".
106
Provide a bad username or password and you'll get a 401.
108
>>> client.add_credentials("baduser", "baspassword")
109
>>> response, body = client.request('http://basictest/protected/')
110
>>> print(response['status'])
113
Provide the correct credentials and you'll get a 200, even for the
116
>>> client.add_credentials("user", "password")
117
>>> response, body = client.request('http://basictest/protected/')
118
>>> print(response['status'])
123
>>> _ = wsgi_intercept.remove_wsgi_intercept('basictest', 80)
128
BasicAuthMiddleware instances can be stacked, each instance protecting
129
a different path pattern. Here, we'll use stacking to protect
130
the two regexes ".*protected.*" and ".*different.*", without combining
131
them into one complex regex.
133
>>> def return_user_application(environ, start_response):
134
... start_response('200', [('Content-type','text/plain')])
135
... return [str(environ['authenticated_user']).encode('utf-8')]
137
>>> def protected_application():
138
... protected = BasicAuthMiddleware(
139
... return_user_application, realm="WSGI middleware test",
140
... protect_path_pattern=".*protected.*",
141
... authenticate_with=authenticate)
142
... return BasicAuthMiddleware(
143
... protected, realm="Another middleware",
144
... protect_path_pattern=".*different.*",
145
... authenticate_with=authenticate)
149
>>> wsgi_intercept.add_wsgi_intercept(
150
... 'stacked', 80, protected_application)
151
>>> client = httplib2.Http()
153
Both path patterns are protected:
155
>>> response, body = client.request('http://stacked/protected')
156
>>> print(response['status'])
158
>>> response, body = client.request('http://stacked/different')
159
>>> print(response['status'])
162
Both path patterns control respond to the same credentials.
164
>>> client.add_credentials("user", "password")
166
>>> response, body = client.request('http://stacked/protected-resource')
167
>>> print(response['status'])
169
>>> print(body.decode())
172
>>> response, body = client.request('http://stacked/different-resource')
173
>>> print(response['status'])
175
>>> print(body.decode())
178
>>> _ = wsgi_intercept.remove_wsgi_intercept('stacked', 80)
183
The OAuthMiddleware implements section 7 ("Accessing Protected
184
Resources") of the OAuth specification. That is, it makes sure that
185
incoming consumer keys and access tokens pass some application-defined
186
test. It does not help you serve request tokens or exchange a request
187
token for an access token.
189
We'll use OAuthMiddleware to protect the same simple application we
190
protected earlier with BasicAuthMiddleware. But since we're using
191
OAuth, we'll be checking a consumer key and access token, instead of a
192
username and password.
194
>>> from oauth.oauth import OAuthConsumer, OAuthToken
196
>>> valid_consumer = OAuthConsumer("consumer", '')
197
>>> valid_token = OAuthToken("token", "secret")
199
>>> def authenticate(consumer, token, parameters):
200
... """Accepts the valid consumer and token, rejects everything else.
202
... :return: The consumer, if the credentials are valid.
205
... if consumer == valid_consumer and token == valid_token:
209
>>> print(authenticate(valid_consumer, valid_token, None).key)
212
>>> invalid_consumer = OAuthConsumer("other consumer", '')
213
>>> print(authenticate(invalid_consumer, valid_token, None))
216
To test the OAuthMiddleware's security features, we'll also need to
217
create a data store. In a real application the data store would
218
probably be a database containing the registered consumer keys and
219
tokens. We're using a simple data store designed for testing, and
220
telling it about the one valid consumer and token.
222
>>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore
223
>>> data_store = SimpleOAuthDataStore(
224
... {valid_consumer.key : valid_consumer},
225
... {valid_token.key : valid_token})
227
>>> print(data_store.lookup_consumer("consumer").key)
229
>>> print(data_store.lookup_consumer("badconsumer"))
232
The data store tracks the use of OAuth nonces. If you call the data
233
store's lookup_nonce() twice with the same values, the first call will
234
return False and the second call will return True.
236
>>> print(data_store.lookup_nonce("consumer", "token", "nonce"))
238
>>> print(data_store.lookup_nonce("consumer", "token", "nonce"))
241
>>> print(data_store.lookup_nonce("newconsumer", "token", "nonce"))
244
Now let's protect an application with lazr.authenticate's
245
OAuthMiddleware, using our authentication technique and our simple
248
>>> from lazr.authentication.wsgi import OAuthMiddleware
249
>>> def protected_application():
250
... return OAuthMiddleware(
251
... dummy_application, realm="OAuth test",
252
... authenticate_with=authenticate, data_store=data_store)
254
>>> wsgi_intercept.add_wsgi_intercept(
255
... 'oauthtest', 80, protected_application)
256
>>> client = httplib2.Http()
258
A properly signed request will go through to the underlying WSGI
261
>>> from oauth.oauth import (
262
... OAuthRequest, OAuthSignatureMethod_PLAINTEXT)
263
>>> def sign_request(url, consumer=valid_consumer, token=valid_token):
264
... request = OAuthRequest().from_consumer_and_token(
265
... consumer, token, http_url=url)
266
... request.sign_request(
267
... OAuthSignatureMethod_PLAINTEXT(), consumer, token)
268
... headers = request.to_header('OAuth test')
271
>>> url = 'http://oauthtest/'
272
>>> headers = sign_request(url)
273
>>> response, body = client.request(url, headers=headers)
274
>>> print(response['status'])
276
>>> print(body.decode())
279
If you replay a signed HTTP request that worked the first time, it
280
will fail the second time, because you'll be sending a nonce that was
283
>>> response, body = client.request(url, headers=headers)
284
>>> print(response['status'])
287
An unsigned request will fail.
289
>>> response, body = client.request('http://oauthtest/')
290
>>> print(response['status'])
292
>>> print(response['www-authenticate'])
293
OAuth realm="OAuth test"
295
A request signed with invalid credentials will fail.
297
>>> bad_token = OAuthToken("token", "badsecret")
298
>>> headers = sign_request(url, token=bad_token)
299
>>> response, body = client.request(url, headers=headers)
300
>>> print(response['status'])
305
>>> _ = wsgi_intercept.remove_wsgi_intercept('oauthtest', 80)
307
.. pypi description ends here