~ubuntuone-pqm-team/canonical-identity-provider/trunk

« back to all changes in this revision

Viewing changes to doctests/stories/openid/basics.txt

  • Committer: Danny Tamez
  • Date: 2010-04-21 15:29:24 UTC
  • Revision ID: danny.tamez@canonical.com-20100421152924-lq1m92tstk2iz75a
Canonical SSO Provider (Open Source) - Initial Commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
Copyright 2010 Canonical Ltd.  This software is licensed under the
 
2
GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
= OpenID =
 
5
 
 
6
== Introduction ==
 
7
 
 
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.
 
10
 
 
11
    >>> anon_browser.open('http://openid.launchpad.dev')
 
12
    >>> print anon_browser.headers
 
13
    Status: 200 Ok
 
14
    ...
 
15
    Content-Type: text/html...
 
16
    ...
 
17
 
 
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.
 
21
 
 
22
NB. The plus is in the query to ensure this test is running with an up to
 
23
date enough Zope3
 
24
 
 
25
    >>> anon_browser.open('http://launchpad.dev/+openid-consumer?foo=%2Bbar')
 
26
    >>> print anon_browser.contents
 
27
    Consumer received GET
 
28
    foo:+bar
 
29
 
 
30
 
 
31
== associate Mode ==
 
32
 
 
33
Establish a shared secret between Consumer and Identity Provider.
 
34
 
 
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
 
37
POST request.
 
38
 
 
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
 
45
    Status: 200 Ok
 
46
    ...
 
47
    Content-Type: text/plain
 
48
    ...
 
49
    >>> print anon_browser.contents
 
50
    assoc_handle:{HMAC-SHA1}{...}{...}
 
51
    assoc_type:HMAC-SHA1
 
52
    expires_in:1209...
 
53
    mac_key:...
 
54
    <BLANKLINE>
 
55
 
 
56
 
 
57
Get the association handle, which we will need for later tests.
 
58
 
 
59
    >>> import re
 
60
    >>> [assoc_handle] = re.findall('assoc_handle:(.*)', anon_browser.contents)
 
61
 
 
62
== checkid_immediate Mode ==
 
63
 
 
64
Ask an Identity Provider if a End User owns the Claimed Identifier,
 
65
getting back an immediate "yes" or "can't say" answer.
 
66
 
 
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.
 
70
 
 
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',
 
77
    ...     })
 
78
    >>> anon_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
 
79
    >>> print anon_browser.contents
 
80
    Consumer received GET
 
81
    openid.assoc_handle:...
 
82
    openid.mode:id_res
 
83
    ...
 
84
    openid.user_setup_url:http://openid.launchpad.dev/+openid?...
 
85
    <BLANKLINE>
 
86
 
 
87
 
 
88
An error is returned to the consumer if an attempt to login as an invalid
 
89
user.
 
90
 
 
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',
 
96
    ...     })
 
97
    >>> user_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
 
98
    >>> print user_browser.contents
 
99
    Consumer received GET
 
100
    openid.assoc_handle:...
 
101
    openid.mode:id_res
 
102
    ...
 
103
    openid.user_setup_url:http://openid.launchpad.dev/+openid?...
 
104
    <BLANKLINE>
 
105
 
 
106
We test a success below in the 'Sticky Authorization' section.
 
107
 
 
108
 
 
109
== checkid_setup Mode ==
 
110
 
 
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.
 
113
 
 
114
    >>> [setup_url] = re.findall(
 
115
    ...     '(?m)^openid.user_setup_url:(.*)$', anon_browser.contents
 
116
    ...     )
 
117
 
 
118
 
 
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:
 
123
 
 
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()
 
132
 
 
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
 
139
    openid.mode:id_res
 
140
    openid.op_endpoint:http://openid.launchpad.dev/+openid
 
141
    openid.response_nonce:...
 
142
    openid.return_to:http://launchpad.dev/+openid-consumer
 
143
    openid.sig:...
 
144
    openid.signed:...
 
145
 
 
146
We will record the signature from this response to use in the next test:
 
147
 
 
148
    >>> [sig] = re.findall('sig:(.*)', mark_browser.contents)
 
149
 
 
150
 
 
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:
 
154
 
 
155
    >>> mark_browser.open(setup_url)
 
156
    >>> mark_browser.open(mark_browser.url[:-6]+'cancel')
 
157
 
 
158
    >>> print mark_browser.url
 
159
    http://launchpad.dev/+openid-consumer?...
 
160
    >>> print mark_browser.contents
 
161
    Consumer received GET
 
162
    openid.mode:cancel...
 
163
 
 
164
 
 
165
== check_authentication Mode ==
 
166
 
 
167
Ask an Identity Provider if a message is valid. For dumb, stateless
 
168
Consumers or when verifying an invalidate_handle response.
 
169
 
 
170
If an association handle is stateful (genereted using the associate Mode),
 
171
check_authentication will fail.
 
172
 
 
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',
 
181
    ...     })
 
182
    >>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
 
183
    >>> print mark_browser.contents
 
184
    is_valid:false
 
185
    <BLANKLINE>
 
186
 
 
187
 
 
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.
 
191
 
 
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',
 
197
    ...     })
 
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
 
204
    openid.mode:id_res
 
205
    openid.op_endpoint:http://openid.launchpad.dev/+openid
 
206
    openid.response_nonce:...
 
207
    openid.return_to:http://launchpad.dev/+openid-consumer
 
208
    openid.sig:...
 
209
    openid.signed:...
 
210
 
 
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)
 
218
    >>> message.update({
 
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'],
 
223
    ...     })
 
224
 
 
225
    >>> args = urlencode(message)
 
226
    >>> mark_browser.open('http://openid.launchpad.dev/+openid', args)
 
227
    >>> print mark_browser.contents
 
228
    is_valid:true
 
229
    <BLANKLINE>
 
230
 
 
231
 
 
232
== Sticky Authorization ==
 
233
 
 
234
Users can select how long our OpenID server will continue to authorize
 
235
them to a particular consumer.
 
236
 
 
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
 
242
#    ...
 
243
#    openid.identity:http://openid.launchpad.dev/+id/mark_oid
 
244
#    openid.mode:id_res
 
245
#    openid.return_to:http://launchpad.dev/+openid-consumer
 
246
#    ...
 
247
 
 
248
 
 
249
Now that we have authorized for 1 hour, further auth requests
 
250
automatically succeed without user intervention.
 
251
 
 
252
#    >>> mark_browser.open(setup_url)
 
253
#    >>> print mark_browser.contents
 
254
#    Consumer received GET
 
255
#    ...
 
256
#    openid.identity:http://openid.launchpad.dev/+id/mark_oid
 
257
#    openid.mode:id_res
 
258
#    openid.return_to:http://launchpad.dev/+openid-consumer
 
259
#    ...
 
260
 
 
261
 
 
262
== Identity Ownership ==
 
263
 
 
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.
 
266
 
 
267
Immediate mode:
 
268
 
 
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',
 
275
    ...     })
 
276
    >>> mark_browser.open('http://openid.launchpad.dev/+openid?%s' % args)
 
277
    >>> print mark_browser.contents
 
278
    Consumer received GET
 
279
    openid.assoc_handle:...
 
280
    openid.mode:id_res
 
281
    openid.sig:...
 
282
    openid.signed:...
 
283
    openid.user_setup_url:http://openid.launchpad.dev/+openid?...
 
284
    <BLANKLINE>
 
285
 
 
286
 
 
287
Interactive mode:
 
288
 
 
289
    >>> [setup_url] = re.findall(
 
290
    ...     '(?m)^openid.user_setup_url:(.*)$', mark_browser.contents
 
291
    ...     )
 
292
 
 
293
#    >>> mark_browser.handleErrors = False
 
294
#    >>> mark_browser.open(setup_url)
 
295
#    Traceback (most recent call last):
 
296
#    ...
 
297
#    Unauthorized: You are not authorized to use this OpenID identifier.
 
298
#    >>> mark_browser.handleErrors = True
 
299
 
 
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...
 
306
 
 
307
 
 
308
== Invalid identities ==
 
309
 
 
310
If you attempt interactive authentication with an invalid OpenID
 
311
identifier, you get a nice error page.
 
312
 
 
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',
 
319
    ...     })
 
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
 
323
    ...     )
 
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
 
328
 
 
329
 
 
330
== Broken Consumers ==
 
331
 
 
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
 
335
inclined.
 
336
 
 
337
    >>> args = urlencode({
 
338
    ...     'openid.mode': 'whoops',
 
339
    ...     })
 
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'
 
344
    mode:error
 
345
    <BLANKLINE>
 
346
 
 
347
 
 
348
If there is a valid return_to, then the consumer gets notified.
 
349
 
 
350
    >>> args = urlencode({
 
351
    ...     'openid.mode': 'whoops',
 
352
    ...     'openid.return_to': 'http://launchpad.dev/+openid-consumer',
 
353
    ...     })
 
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'
 
358
    openid.mode:error
 
359