~lazr-developers/lazr.authentication/trunk

« back to all changes in this revision

Viewing changes to src/lazr/authentication/docs/index.rst

  • Committer: Jürgen Gmach
  • Date: 2021-11-05 14:27:41 UTC
  • Revision ID: juergen.gmach@canonical.com-20211105142741-crqrjqm3dhkcy9wd
Moved to git

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
..
2
 
    This file is part of lazr.authentication.
3
 
 
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.
7
 
 
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.
12
 
 
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/>.
15
 
 
16
 
WSGI Middleware
17
 
===============
18
 
 
19
 
lazr.authentication defines some simple WSGI middleware for protecting
20
 
resources with different kinds of HTTP authentication.
21
 
 
22
 
    >>> from __future__ import print_function
23
 
    >>> import lazr.authentication
24
 
    >>> print('VERSION: %s' % lazr.authentication.__version__)
25
 
    VERSION: ...
26
 
 
27
 
 
28
 
BasicAuthMiddleware
29
 
-------------------
30
 
 
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.
36
 
 
37
 
First, let's create a really simple WSGI application that responds to
38
 
any request with a 200 response code.
39
 
 
40
 
    >>> def dummy_application(environ, start_response):
41
 
    ...         start_response('200', [('Content-type','text/plain')])
42
 
    ...         return [b'Success']
43
 
 
44
 
Now let's protect that application. Here's an authentication callback
45
 
function.
46
 
 
47
 
    >>> def authenticate(username, password):
48
 
    ...     """Accepts "user/password", rejects everything else.
49
 
    ...
50
 
    ...     :return: The username, if the credentials are valid.
51
 
    ...              None, otherwise.
52
 
    ...     """
53
 
    ...     if username == "user" and password == "password":
54
 
    ...         return username
55
 
    ...     return None
56
 
 
57
 
    >>> print(authenticate("user", "password"))
58
 
    user
59
 
 
60
 
    >>> print(authenticate("notuser", "password"))
61
 
    None
62
 
 
63
 
Here's a WSGI application that protects the application using
64
 
BasicAuthMiddleware.
65
 
 
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)
72
 
 
73
 
    >>> import wsgi_intercept
74
 
    >>> from wsgi_intercept.httplib2_intercept import install
75
 
    >>> install()
76
 
    >>> wsgi_intercept.add_wsgi_intercept(
77
 
    ...     'basictest', 80, protected_application)
78
 
 
79
 
Most of the application's URLs are not protected by the
80
 
middleware. You can access them without providing credentials.
81
 
 
82
 
    >>> import httplib2
83
 
    >>> client = httplib2.Http()
84
 
    >>> response, body = client.request('http://basictest/')
85
 
    >>> print(response['status'])
86
 
    200
87
 
    >>> print(body.decode())
88
 
    Success
89
 
 
90
 
Any URL that includes the string "protected" is protected by the
91
 
middleware, and cannot be accessed without credentials.
92
 
 
93
 
    >>> response, body = client.request('http://basictest/protected/')
94
 
    >>> print(response['status'])
95
 
    401
96
 
    >>> print(response['www-authenticate'])
97
 
    Basic realm="WSGI middleware test"
98
 
 
99
 
    >>> response, body = client.request(
100
 
    ...     'http://basictest/this-is-protected-as-well/')
101
 
    >>> print(response['status'])
102
 
    401
103
 
 
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.
107
 
 
108
 
    >>> client.add_credentials("baduser", "baspassword")
109
 
    >>> response, body = client.request('http://basictest/protected/')
110
 
    >>> print(response['status'])
111
 
    401
112
 
 
113
 
Provide the correct credentials and you'll get a 200, even for the
114
 
protected URIs.
115
 
 
116
 
    >>> client.add_credentials("user", "password")
117
 
    >>> response, body = client.request('http://basictest/protected/')
118
 
    >>> print(response['status'])
119
 
    200
120
 
 
121
 
Teardown.
122
 
 
123
 
    >>> _ = wsgi_intercept.remove_wsgi_intercept('basictest', 80)
124
 
 
125
 
Stacking
126
 
********
127
 
 
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.
132
 
 
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')]
136
 
 
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)
146
 
 
147
 
Setup.
148
 
 
149
 
    >>> wsgi_intercept.add_wsgi_intercept(
150
 
    ...     'stacked', 80, protected_application)
151
 
    >>> client = httplib2.Http()
152
 
 
153
 
Both path patterns are protected:
154
 
 
155
 
    >>> response, body = client.request('http://stacked/protected')
156
 
    >>> print(response['status'])
157
 
    401
158
 
    >>> response, body = client.request('http://stacked/different')
159
 
    >>> print(response['status'])
160
 
    401
161
 
 
162
 
Both path patterns control respond to the same credentials.
163
 
 
164
 
    >>> client.add_credentials("user", "password")
165
 
 
166
 
    >>> response, body = client.request('http://stacked/protected-resource')
167
 
    >>> print(response['status'])
168
 
    200
169
 
    >>> print(body.decode())
170
 
    user
171
 
 
172
 
    >>> response, body = client.request('http://stacked/different-resource')
173
 
    >>> print(response['status'])
174
 
    200
175
 
    >>> print(body.decode())
176
 
    user
177
 
 
178
 
    >>> _ = wsgi_intercept.remove_wsgi_intercept('stacked', 80)
179
 
 
180
 
OAuthMiddleware
181
 
---------------
182
 
 
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.
188
 
 
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.
193
 
 
194
 
    >>> from oauth.oauth import OAuthConsumer, OAuthToken
195
 
 
196
 
    >>> valid_consumer = OAuthConsumer("consumer", '')
197
 
    >>> valid_token = OAuthToken("token", "secret")
198
 
 
199
 
    >>> def authenticate(consumer, token, parameters):
200
 
    ...     """Accepts the valid consumer and token, rejects everything else.
201
 
    ...
202
 
    ...     :return: The consumer, if the credentials are valid.
203
 
    ...              None, otherwise.
204
 
    ...     """
205
 
    ...     if consumer == valid_consumer and token == valid_token:
206
 
    ...         return consumer
207
 
    ...     return None
208
 
 
209
 
    >>> print(authenticate(valid_consumer, valid_token, None).key)
210
 
    consumer
211
 
 
212
 
    >>> invalid_consumer = OAuthConsumer("other consumer", '')
213
 
    >>> print(authenticate(invalid_consumer, valid_token, None))
214
 
    None
215
 
 
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.
221
 
 
222
 
    >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore
223
 
    >>> data_store = SimpleOAuthDataStore(
224
 
    ...     {valid_consumer.key : valid_consumer},
225
 
    ...     {valid_token.key : valid_token})
226
 
 
227
 
    >>> print(data_store.lookup_consumer("consumer").key)
228
 
    consumer
229
 
    >>> print(data_store.lookup_consumer("badconsumer"))
230
 
    None
231
 
 
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.
235
 
 
236
 
    >>> print(data_store.lookup_nonce("consumer", "token", "nonce"))
237
 
    False
238
 
    >>> print(data_store.lookup_nonce("consumer", "token", "nonce"))
239
 
    True
240
 
 
241
 
    >>> print(data_store.lookup_nonce("newconsumer", "token", "nonce"))
242
 
    False
243
 
 
244
 
Now let's protect an application with lazr.authenticate's
245
 
OAuthMiddleware, using our authentication technique and our simple
246
 
data store.
247
 
 
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)
253
 
 
254
 
    >>> wsgi_intercept.add_wsgi_intercept(
255
 
    ...     'oauthtest', 80, protected_application)
256
 
    >>> client = httplib2.Http()
257
 
 
258
 
A properly signed request will go through to the underlying WSGI
259
 
application.
260
 
 
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')
269
 
    ...     return headers
270
 
 
271
 
    >>> url = 'http://oauthtest/'
272
 
    >>> headers = sign_request(url)
273
 
    >>> response, body = client.request(url, headers=headers)
274
 
    >>> print(response['status'])
275
 
    200
276
 
    >>> print(body.decode())
277
 
    Success
278
 
 
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
281
 
already used.
282
 
 
283
 
    >>> response, body = client.request(url, headers=headers)
284
 
    >>> print(response['status'])
285
 
    401
286
 
 
287
 
An unsigned request will fail.
288
 
 
289
 
    >>> response, body = client.request('http://oauthtest/')
290
 
    >>> print(response['status'])
291
 
    401
292
 
    >>> print(response['www-authenticate'])
293
 
    OAuth realm="OAuth test"
294
 
 
295
 
A request signed with invalid credentials will fail.
296
 
 
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'])
301
 
    401
302
 
 
303
 
Teardown.
304
 
 
305
 
    >>> _ = wsgi_intercept.remove_wsgi_intercept('oauthtest', 80)
306
 
 
307
 
.. pypi description ends here
308
 
 
309
 
.. toctree::
310
 
   :glob:
311
 
 
312
 
   NEWS