1
Copyright 2010 Canonical Ltd. This software is licensed under the
2
GNU Affero General Public License version 3 (see the file LICENSE).
8
Launchpad is an OpenID provider. If the URL is accessed by a web browser,
9
an informative message is displayed as per the OpenID spec.
11
>>> anon_browser.open('http://openid.launchpad.dev')
12
>>> print anon_browser.headers
15
Content-Type: text/html...
18
We are going to fake a consumer for these examples. In order to ensure
19
that the consumer is being fed the correct replies, we use a view that
20
renders the parameters in the response in an easily testable format.
22
NB. The plus is in the query to ensure this test is running with an up to
25
>>> anon_browser.open('http://launchpad.dev/+openid-consumer?foo=%2Bbar')
26
>>> print anon_browser.contents
33
Establish a shared secret between Consumer and Identity Provider.
35
After determining the URL of the OpenID server, the next thing a consumer
36
needs to do is associate with the server and get a shared secret via a
39
>>> from urllib import urlencode
40
>>> anon_browser.open(
41
... 'http://openid.launchpad.dev/+openid', data=urlencode({
42
... 'openid.mode': 'associate',
43
... 'openid.assoc_type': 'HMAC-SHA1'}))
44
>>> print anon_browser.headers
47
Content-Type: text/plain
49
>>> print anon_browser.contents
50
assoc_handle:{HMAC-SHA1}{...}{...}
57
Get the association handle, which we will need for later tests.
60
>>> [assoc_handle] = re.findall('assoc_handle:(.*)', anon_browser.contents)
62
== checkid_immediate Mode ==
64
Ask an Identity Provider if a End User owns the Claimed Identifier,
65
getting back an immediate "yes" or "can't say" answer.
67
Once the shared secret is negotiated, the consumer can send
68
checkid_immediate and checkid_setup GET requests.checkid_immediate requests
69
will currently return "can't say" as we are not yet logged into Launchpad.
71
>>> args = urlencode({
72
... 'openid.mode': 'checkid_immediate',
73
... 'openid.identity':
74
... 'http://openid.launchpad.dev/+id/mark_oid',
75
... 'openid.assoc_handle': assoc_handle,
76
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
78
>>> anon_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
79
>>> print anon_browser.contents
81
openid.assoc_handle:...
84
openid.user_setup_url:http://openid.launchpad.dev/+openid?...
88
An error is returned to the consumer if an attempt to login as an invalid
91
>>> args = urlencode({
92
... 'openid.mode': 'checkid_immediate',
93
... 'openid.identity': 'http://launchpad.dev/+id/limi_oid',
94
... 'openid.assoc_handle': assoc_handle,
95
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
97
>>> user_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
98
>>> print user_browser.contents
100
openid.assoc_handle:...
103
openid.user_setup_url:http://openid.launchpad.dev/+openid?...
106
We test a success below in the 'Sticky Authorization' section.
109
== checkid_setup Mode ==
111
checkid_setup is interactive with the user. We can extract the
112
URL for the checkid_setup from the result of the previous test.
114
>>> [setup_url] = re.findall(
115
... '(?m)^openid.user_setup_url:(.*)$', anon_browser.contents
119
Lets start a new browser session so we don't have any credentials.
120
When we go to the OpenID setup URL, we are presented with a login
121
form. By entering an email address and password, we are directed back
122
to the consumer, completing the OpenID request:
124
>>> mark_browser = setupBrowser()
125
>>> mark_browser.open(setup_url)
126
>>> print mark_browser.url
127
http://openid.launchpad.dev/.../+decide
128
>>> mark_browser.getControl(name='email').value = 'mark@example.com'
129
>>> mark_browser.getControl(name='password').value = 'test'
130
>>> mark_browser.getControl(name='continue').click()
131
>>> mark_browser.getControl(name='yes').click()
133
>>> print mark_browser.url
134
http://launchpad.dev/+openid-consumer?...
135
>>> print mark_browser.contents
136
Consumer received GET
137
openid.assoc_handle:...
138
openid.identity:http://openid.launchpad.dev/+id/mark_oid
140
openid.op_endpoint:http://openid.launchpad.dev/+openid
141
openid.response_nonce:...
142
openid.return_to:http://launchpad.dev/+openid-consumer
146
We will record the signature from this response to use in the next test:
148
>>> [sig] = re.findall('sig:(.*)', mark_browser.contents)
151
If we had been logged into Launchpad, we would instead have seen a
152
simple approve/deny form since Launchpad already knows who we are.
153
This can be seen using the existing browser session:
155
>>> mark_browser.open(setup_url)
156
>>> mark_browser.open(mark_browser.url[:-6]+'cancel')
158
>>> print mark_browser.url
159
http://launchpad.dev/+openid-consumer?...
160
>>> print mark_browser.contents
161
Consumer received GET
162
openid.mode:cancel...
165
== check_authentication Mode ==
167
Ask an Identity Provider if a message is valid. For dumb, stateless
168
Consumers or when verifying an invalidate_handle response.
170
If an association handle is stateful (genereted using the associate Mode),
171
check_authentication will fail.
173
>>> args = urlencode({
174
... 'openid.mode': 'check_authentication',
175
... 'openid.assoc_handle': assoc_handle,
176
... 'openid.sig': sig,
177
... 'openid.signed': 'return_to,mode,identity',
178
... 'openid.identity':
179
... 'http://openid.launchpad.dev/+id/mark_oid',
180
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
182
>>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
183
>>> print mark_browser.contents
188
If we are a dumb consumer though, we must invoke the check_authentication
189
mode, passing back the association handle, signature and values of all
190
fields that were signed.
192
>>> args = urlencode({
193
... 'openid.mode': 'checkid_setup',
194
... 'openid.identity':
195
... 'http://openid.launchpad.dev/+id/mark_oid',
196
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
198
>>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
199
>>> mark_browser.getControl(name='yes', index=0).click()
200
>>> print mark_browser.contents
201
Consumer received GET
202
openid.assoc_handle:...
203
openid.identity:http://openid.launchpad.dev/+id/mark_oid
205
openid.op_endpoint:http://openid.launchpad.dev/+openid
206
openid.response_nonce:...
207
openid.return_to:http://launchpad.dev/+openid-consumer
211
>>> fields = dict(line.split(':', 1)
212
... for line in mark_browser.contents.splitlines()[1:]
213
... if line.startswith('openid.'))
214
>>> signed = ['openid.' + name
215
... for name in fields['openid.signed'].split(',')]
216
>>> message = dict((key, value) for (key, value) in fields.items()
217
... if key in signed)
219
... 'openid.mode': 'check_authentication',
220
... 'openid.assoc_handle': fields['openid.assoc_handle'],
221
... 'openid.sig': fields['openid.sig'],
222
... 'openid.signed': fields['openid.signed'],
225
>>> args = urlencode(message)
226
>>> mark_browser.open('http://openid.launchpad.dev/+openid', args)
227
>>> print mark_browser.contents
232
== Sticky Authorization ==
234
Users can select how long our OpenID server will continue to authorize
235
them to a particular consumer.
237
# >>> mark_browser.open(setup_url)
238
# >>> mark_browser.getControl(name='allow_duration').value=['3600']
239
# >>> mark_browser.getControl('Sign In', index=0).click()
240
# >>> print mark_browser.contents
241
# Consumer received GET
243
# openid.identity:http://openid.launchpad.dev/+id/mark_oid
245
# openid.return_to:http://launchpad.dev/+openid-consumer
249
Now that we have authorized for 1 hour, further auth requests
250
automatically succeed without user intervention.
252
# >>> mark_browser.open(setup_url)
253
# >>> print mark_browser.contents
254
# Consumer received GET
256
# openid.identity:http://openid.launchpad.dev/+id/mark_oid
258
# openid.return_to:http://launchpad.dev/+openid-consumer
262
== Identity Ownership ==
264
You cannot log in as someone elses identity. If you try to, you will be
265
prompted with a login screen to connect as the correct user.
269
>>> args = urlencode({
270
... 'openid.mode': 'checkid_immediate',
271
... 'openid.identity':
272
... 'http://openid.launchpad.dev/+id/stub_oid',
273
... 'openid.assoc_handle': assoc_handle,
274
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
276
>>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
277
>>> print mark_browser.contents
278
Consumer received GET
279
openid.assoc_handle:...
283
openid.user_setup_url:http://openid.launchpad.dev/+openid?...
289
>>> [setup_url] = re.findall(
290
... '(?m)^openid.user_setup_url:(.*)$', mark_browser.contents
293
# >>> mark_browser.handleErrors = False
294
# >>> mark_browser.open(setup_url)
295
# Traceback (most recent call last):
297
# Unauthorized: You are not authorized to use this OpenID identifier.
298
# >>> mark_browser.handleErrors = True
300
>>> mark_browser.open(setup_url)
301
>>> print mark_browser.url
302
http://launchpad.dev/+openid-consumer?...
303
>>> print mark_browser.contents
304
Consumer received GET
305
openid.mode:cancel...
308
== Invalid identities ==
310
If you attempt interactive authentication with an invalid OpenID
311
identifier, you get a nice error page.
313
>>> args = urlencode({
314
... 'openid.mode': 'checkid_immediate',
315
... 'openid.identity':
316
... 'http://some/other/site',
317
... 'openid.assoc_handle': assoc_handle,
318
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
320
>>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
321
>>> [setup_url] = re.findall(
322
... '(?m)^openid.user_setup_url:(.*)$', mark_browser.contents
324
>>> mark_browser.open(setup_url)
325
>>> mark_browser.getControl(name='continue').click()
326
>>> print mark_browser.url
327
http://launchpad.dev/+openid-consumer?openid.mode=cancel
330
== Broken Consumers ==
332
Really bad requests might trigger a protocol error. These are such edge cases
333
that I can't even be bothered to figure out how to prevent the u'' unicode
334
prefix from showing up. We might want to figure it out one day if we feel
337
>>> args = urlencode({
338
... 'openid.mode': 'whoops',
340
>>> mark_browser.handleErrors = True
341
>>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
342
>>> print mark_browser.contents
343
error:... mode u'whoops'
348
If there is a valid return_to, then the consumer gets notified.
350
>>> args = urlencode({
351
... 'openid.mode': 'whoops',
352
... 'openid.return_to': 'http://launchpad.dev/+openid-consumer',
354
>>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
355
>>> print mark_browser.contents
356
Consumer received GET
357
openid.error:... mode u'whoops'