1
2 """OpenID support for Relying Parties (aka Consumers).
3
4 This module documents the main interface with the OpenID consumer
5 library. The only part of the library which has to be used and isn't
6 documented in full here is the store required to create an
7 C{L{Consumer}} instance. More on the abstract store type and
8 concrete implementations of it that are provided in the documentation
9 for the C{L{__init__<Consumer.__init__>}} method of the
10 C{L{Consumer}} class.
11
12
13 OVERVIEW
14 ========
15
16 The OpenID identity verification process most commonly uses the
17 following steps, as visible to the user of this library:
18
19 1. The user enters their OpenID into a field on the consumer's
20 site, and hits a login button.
21
22 2. The consumer site discovers the user's OpenID provider using
23 the Yadis protocol.
24
25 3. The consumer site sends the browser a redirect to the
26 OpenID provider. This is the authentication request as
27 described in the OpenID specification.
28
29 4. The OpenID provider's site sends the browser a redirect
30 back to the consumer site. This redirect contains the
31 provider's response to the authentication request.
32
33 The most important part of the flow to note is the consumer's site
34 must handle two separate HTTP requests in order to perform the
35 full identity check.
36
37
38 LIBRARY DESIGN
39 ==============
40
41 This consumer library is designed with that flow in mind. The
42 goal is to make it as easy as possible to perform the above steps
43 securely.
44
45 At a high level, there are two important parts in the consumer
46 library. The first important part is this module, which contains
47 the interface to actually use this library. The second is the
48 C{L{openid.store.interface}} module, which describes the
49 interface to use if you need to create a custom method for storing
50 the state this library needs to maintain between requests.
51
52 In general, the second part is less important for users of the
53 library to know about, as several implementations are provided
54 which cover a wide variety of situations in which consumers may
55 use the library.
56
57 This module contains a class, C{L{Consumer}}, with methods
58 corresponding to the actions necessary in each of steps 2, 3, and
59 4 described in the overview. Use of this library should be as easy
60 as creating an C{L{Consumer}} instance and calling the methods
61 appropriate for the action the site wants to take.
62
63
64 SESSIONS, STORES, AND STATELESS MODE
65 ====================================
66
67 The C{L{Consumer}} object keeps track of two types of state:
68
69 1. State of the user's current authentication attempt. Things like
70 the identity URL, the list of endpoints discovered for that
71 URL, and in case where some endpoints are unreachable, the list
72 of endpoints already tried. This state needs to be held from
73 Consumer.begin() to Consumer.complete(), but it is only applicable
74 to a single session with a single user agent, and at the end of
75 the authentication process (i.e. when an OP replies with either
76 C{id_res} or C{cancel}) it may be discarded.
77
78 2. State of relationships with servers, i.e. shared secrets
79 (associations) with servers and nonces seen on signed messages.
80 This information should persist from one session to the next and
81 should not be bound to a particular user-agent.
82
83
84 These two types of storage are reflected in the first two arguments of
85 Consumer's constructor, C{session} and C{store}. C{session} is a
86 dict-like object and we hope your web framework provides you with one
87 of these bound to the user agent. C{store} is an instance of
88 L{openid.store.interface.OpenIDStore}.
89
90 Since the store does hold secrets shared between your application and the
91 OpenID provider, you should be careful about how you use it in a shared
92 hosting environment. If the filesystem or database permissions of your
93 web host allow strangers to read from them, do not store your data there!
94 If you have no safe place to store your data, construct your consumer
95 with C{None} for the store, and it will operate only in stateless mode.
96 Stateless mode may be slower, put more load on the OpenID provider, and
97 trusts the provider to keep you safe from replay attacks.
98
99
100 Several store implementation are provided, and the interface is
101 fully documented so that custom stores can be used as well. See
102 the documentation for the C{L{Consumer}} class for more
103 information on the interface for stores. The implementations that
104 are provided allow the consumer site to store the necessary data
105 in several different ways, including several SQL databases and
106 normal files on disk.
107
108
109 IMMEDIATE MODE
110 ==============
111
112 In the flow described above, the user may need to confirm to the
113 OpenID provider that it's ok to disclose his or her identity.
114 The provider may draw pages asking for information from the user
115 before it redirects the browser back to the consumer's site. This
116 is generally transparent to the consumer site, so it is typically
117 ignored as an implementation detail.
118
119 There can be times, however, where the consumer site wants to get
120 a response immediately. When this is the case, the consumer can
121 put the library in immediate mode. In immediate mode, there is an
122 extra response possible from the server, which is essentially the
123 server reporting that it doesn't have enough information to answer
124 the question yet.
125
126
127 USING THIS LIBRARY
128 ==================
129
130 Integrating this library into an application is usually a
131 relatively straightforward process. The process should basically
132 follow this plan:
133
134 Add an OpenID login field somewhere on your site. When an OpenID
135 is entered in that field and the form is submitted, it should make
136 a request to the your site which includes that OpenID URL.
137
138 First, the application should L{instantiate a Consumer<Consumer.__init__>}
139 with a session for per-user state and store for shared state.
140 using the store of choice.
141
142 Next, the application should call the 'C{L{begin<Consumer.begin>}}' method on the
143 C{L{Consumer}} instance. This method takes the OpenID URL. The
144 C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}}
145 object.
146
147 Next, the application should call the
148 C{L{redirectURL<AuthRequest.redirectURL>}} method on the
149 C{L{AuthRequest}} object. The parameter C{return_to} is the URL
150 that the OpenID server will send the user back to after attempting
151 to verify his or her identity. The C{realm} parameter is the
152 URL (or URL pattern) that identifies your web site to the user
153 when he or she is authorizing it. Send a redirect to the
154 resulting URL to the user's browser.
155
156 That's the first half of the authentication process. The second
157 half of the process is done after the user's OpenID Provider sends the
158 user's browser a redirect back to your site to complete their
159 login.
160
161 When that happens, the user will contact your site at the URL
162 given as the C{return_to} URL to the
163 C{L{redirectURL<AuthRequest.redirectURL>}} call made
164 above. The request will have several query parameters added to
165 the URL by the OpenID provider as the information necessary to
166 finish the request.
167
168 Get an C{L{Consumer}} instance with the same session and store as
169 before and call its C{L{complete<Consumer.complete>}} method,
170 passing in all the received query arguments.
171
172 There are multiple possible return types possible from that
173 method. These indicate the whether or not the login was
174 successful, and include any additional information appropriate for
175 their type.
176
177 @var SUCCESS: constant used as the status for
178 L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects.
179
180 @var FAILURE: constant used as the status for
181 L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects.
182
183 @var CANCEL: constant used as the status for
184 L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects.
185
186 @var SETUP_NEEDED: constant used as the status for
187 L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
188 objects.
189 """
190
191 import cgi
192 import copy
193 from urlparse import urlparse
194
195 from openid import fetchers
196
197 from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
198 DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
199 from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
200 IDENTIFIER_SELECT, no_default
201 from openid import cryptutil
202 from openid import oidutil
203 from openid.association import Association, default_negotiator, \
204 SessionNegotiator
205 from openid.dh import DiffieHellman
206 from openid.store.nonce import mkNonce, split as splitNonce
207 from openid.yadis.manager import Discovery
208
209
210 __all__ = ['AuthRequest', 'Consumer', 'SuccessResponse',
211 'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
212 'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
213 ]
214
215 -def makeKVPost(request_message, server_url):
216 """Make a Direct Request to an OpenID Provider and return the
217 result as a Message object.
218
219 @raises openid.fetchers.HTTPFetchingError: if an error is
220 encountered in making the HTTP post.
221
222 @rtype: L{openid.message.Message}
223 """
224
225 resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
226
227 response_message = Message.fromKVForm(resp.body)
228 if resp.status == 400:
229 raise ServerError.fromMessage(response_message)
230
231 elif resp.status != 200:
232 fmt = 'bad status code from server %s: %s'
233 error_message = fmt % (server_url, resp.status)
234 raise fetchers.HTTPFetchingError(error_message)
235
236 return response_message
237
238
240 """An OpenID consumer implementation that performs discovery and
241 does session management.
242
243 @ivar consumer: an instance of an object implementing the OpenID
244 protocol, but doing no discovery or session management.
245
246 @type consumer: GenericConsumer
247
248 @ivar session: A dictionary-like object representing the user's
249 session data. This is used for keeping state of the OpenID
250 transaction when the user is redirected to the server.
251
252 @cvar session_key_prefix: A string that is prepended to session
253 keys to ensure that they are unique. This variable may be
254 changed to suit your application.
255 """
256 session_key_prefix = "_openid_consumer_"
257
258 _token = 'last_token'
259
260 _discover = staticmethod(discover)
261
262 - def __init__(self, session, store, consumer_class=None):
263 """Initialize a Consumer instance.
264
265 You should create a new instance of the Consumer object with
266 every HTTP request that handles OpenID transactions.
267
268 @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>}
269
270 @param store: an object that implements the interface in
271 C{L{openid.store.interface.OpenIDStore}}. Several
272 implementations are provided, to cover common database
273 environments.
274
275 @type store: C{L{openid.store.interface.OpenIDStore}}
276
277 @see: L{openid.store.interface}
278 @see: L{openid.store}
279 """
280 self.session = session
281 if consumer_class is None:
282 consumer_class = GenericConsumer
283 self.consumer = consumer_class(store)
284 self._token_key = self.session_key_prefix + self._token
285
286 - def begin(self, user_url, anonymous=False):
287 """Start the OpenID authentication process. See steps 1-2 in
288 the overview at the top of this file.
289
290 @param user_url: Identity URL given by the user. This method
291 performs a textual transformation of the URL to try and
292 make sure it is normalized. For example, a user_url of
293 example.com will be normalized to http://example.com/
294 normalizing and resolving any redirects the server might
295 issue.
296
297 @type user_url: unicode
298
299 @param anonymous: Whether to make an anonymous request of the OpenID
300 provider. Such a request does not ask for an authorization
301 assertion for an OpenID identifier, but may be used with
302 extensions to pass other data. e.g. "I don't care who you are,
303 but I'd like to know your time zone."
304
305 @type anonymous: bool
306
307 @returns: An object containing the discovered information will
308 be returned, with a method for building a redirect URL to
309 the server, as described in step 3 of the overview. This
310 object may also be used to add extension arguments to the
311 request, using its
312 L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>}
313 method.
314
315 @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
316
317 @raises openid.consumer.discover.DiscoveryFailure: when I fail to
318 find an OpenID server for this URL. If the C{yadis} package
319 is available, L{openid.consumer.discover.DiscoveryFailure} is
320 an alias for C{yadis.discover.DiscoveryFailure}.
321 """
322 disco = Discovery(self.session, user_url, self.session_key_prefix)
323 try:
324 service = disco.getNextService(self._discover)
325 except fetchers.HTTPFetchingError, why:
326 raise DiscoveryFailure(
327 'Error fetching XRDS document: %s' % (why[0],), None)
328
329 if service is None:
330 raise DiscoveryFailure(
331 'No usable OpenID services found for %s' % (user_url,), None)
332 else:
333 return self.beginWithoutDiscovery(service, anonymous)
334
336 """Start OpenID verification without doing OpenID server
337 discovery. This method is used internally by Consumer.begin
338 after discovery is performed, and exists to provide an
339 interface for library users needing to perform their own
340 discovery.
341
342 @param service: an OpenID service endpoint descriptor. This
343 object and factories for it are found in the
344 L{openid.consumer.discover} module.
345
346 @type service:
347 L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
348
349 @returns: an OpenID authentication request object.
350
351 @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
352
353 @See: Openid.consumer.consumer.Consumer.begin
354 @see: openid.consumer.discover
355 """
356 auth_req = self.consumer.begin(service)
357 self.session[self._token_key] = auth_req.endpoint
358
359 try:
360 auth_req.setAnonymous(anonymous)
361 except ValueError, why:
362 raise ProtocolError(str(why))
363
364 return auth_req
365
366 - def complete(self, query, return_to=None):
367 """Called to interpret the server's response to an OpenID
368 request. It is called in step 4 of the flow described in the
369 consumer overview.
370
371 @param query: A dictionary of the query parameters for this
372 HTTP request.
373
374 @param return_to: The return URL used to invoke the
375 application. Extract the URL from your application's web
376 request framework and specify it here to have it checked
377 against the openid.return_to value in the response. If
378 the return_to URL check fails, the status of the
379 completion will be FAILURE.
380
381 @returns: a subclass of Response. The type of response is
382 indicated by the status attribute, which will be one of
383 SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
384
385 @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>}
386 @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>}
387 @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
388 @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>}
389 """
390
391 endpoint = self.session.get(self._token_key)
392 if endpoint is None:
393 response = FailureResponse(None, 'No session state found')
394 else:
395 message = Message.fromPostArgs(query)
396 response = self.consumer.complete(message, endpoint, return_to)
397 del self.session[self._token_key]
398
399 if (response.status in ['success', 'cancel'] and
400 response.identity_url is not None):
401
402 disco = Discovery(self.session,
403 response.identity_url,
404 self.session_key_prefix)
405
406
407 disco.cleanup()
408
409 return response
410
412 """Set the order in which association types/sessions should be
413 attempted. For instance, to only allow HMAC-SHA256
414 associations created with a DH-SHA256 association session:
415
416 >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
417
418 Any association type/association type pair that is not in this
419 list will not be attempted at all.
420
421 @param association_preferences: The list of allowed
422 (association type, association session type) pairs that
423 should be allowed for this consumer to use, in order from
424 most preferred to least preferred.
425 @type association_preferences: [(str, str)]
426
427 @returns: None
428
429 @see: C{L{openid.association.SessionNegotiator}}
430 """
431 self.consumer.negotiator = SessionNegotiator(association_preferences)
432
465
471
473 session_type = 'no-encryption'
474 allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
475
476 - def getRequest(self):
477 return {}
478
480 mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
481 return oidutil.fromBase64(mac_key64)
482
484 """Internally-used exception that indicates that an immediate-mode
485 request cancelled."""
486 - def __init__(self, user_setup_url=None):
487 Exception.__init__(self, user_setup_url)
488 self.user_setup_url = user_setup_url
489
491 """Exception that indicates that a message violated the
492 protocol. It is raised and caught internally to this file."""
493
495 """A protocol error arising from type URIs mismatching
496 """
497
499 """Exception that is raised when the server returns a 400 response
500 code to a direct request."""
501
502 - def __init__(self, error_text, error_code, message):
503 Exception.__init__(self, error_text)
504 self.error_text = error_text
505 self.error_code = error_code
506 self.message = message
507
509 """Generate a ServerError instance, extracting the error text
510 and the error code from the message."""
511 error_text = message.getArg(
512 OPENID_NS, 'error', '<no error message supplied>')
513 error_code = message.getArg(OPENID_NS, 'error_code')
514 return cls(error_text, error_code, message)
515
516 fromMessage = classmethod(fromMessage)
517
519 """This is the implementation of the common logic for OpenID
520 consumers. It is unaware of the application in which it is
521 running.
522
523 @ivar negotiator: An object that controls the kind of associations
524 that the consumer makes. It defaults to
525 C{L{openid.association.default_negotiator}}. Assign a
526 different negotiator to it if you have specific requirements
527 for how associations are made.
528 @type negotiator: C{L{openid.association.SessionNegotiator}}
529 """
530
531
532
533
534
535
536
537 openid1_nonce_query_arg_name = 'janrain_nonce'
538
539 session_types = {
540 'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
541 'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
542 'no-encryption':PlainTextConsumerSession,
543 }
544
545 _discover = staticmethod(discover)
546
550
551 - def begin(self, service_endpoint):
552 """Create an AuthRequest object for the specified
553 service_endpoint. This method will create an association if
554 necessary."""
555 if self.store is None:
556 assoc = None
557 else:
558 assoc = self._getAssociation(service_endpoint)
559
560 request = AuthRequest(service_endpoint, assoc)
561 request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
562 return request
563
564 - def complete(self, message, endpoint, return_to=None):
565 """Process the OpenID message, using the specified endpoint
566 and return_to URL as context. This method will handle any
567 OpenID message that is sent to the return_to URL.
568 """
569 mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
570
571 if return_to is not None:
572 if not self._checkReturnTo(message, return_to):
573 return FailureResponse(endpoint,
574 "openid.return_to does not match return URL")
575
576 if mode == 'cancel':
577 return CancelResponse(endpoint)
578 elif mode == 'error':
579 error = message.getArg(OPENID_NS, 'error')
580 contact = message.getArg(OPENID_NS, 'contact')
581 reference = message.getArg(OPENID_NS, 'reference')
582
583 return FailureResponse(endpoint, error, contact=contact,
584 reference=reference)
585 elif message.isOpenID2() and mode == 'setup_needed':
586 return SetupNeededResponse(endpoint)
587
588 elif mode == 'id_res':
589 try:
590 self._checkSetupNeeded(message)
591 except SetupNeededError, why:
592 return SetupNeededResponse(endpoint, why.user_setup_url)
593 else:
594 try:
595 return self._doIdRes(message, endpoint)
596 except (ProtocolError, DiscoveryFailure), why:
597 return FailureResponse(endpoint, why[0])
598 else:
599 return FailureResponse(endpoint,
600 'Invalid openid.mode: %r' % (mode,))
601
603 """Check an OpenID message and its openid.return_to value
604 against a return_to URL from an application. Return True on
605 success, False on failure.
606 """
607
608
609 try:
610 self._verifyReturnToArgs(message.toPostArgs())
611 except ProtocolError, why:
612 oidutil.log("Verifying return_to arguments: %s" % (why[0],))
613 return False
614
615
616 msg_return_to = message.getArg(OPENID_NS, 'return_to')
617
618
619
620 app_parts = urlparse(return_to)
621 msg_parts = urlparse(msg_return_to)
622
623
624
625 for part in range(0, 3):
626 if app_parts[part] != msg_parts[part]:
627 return False
628
629 return True
630
631 _makeKVPost = staticmethod(makeKVPost)
632
634 """Check an id_res message to see if it is a
635 checkid_immediate cancel response.
636
637 @raises SetupNeededError: if it is a checkid_immediate cancellation
638 """
639
640
641
642 if message.isOpenID1():
643 user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
644 if user_setup_url is not None:
645 raise SetupNeededError(user_setup_url)
646
648 """Handle id_res responses that are not cancellations of
649 immediate mode requests.
650
651 @param message: the response paramaters.
652 @param endpoint: the discovered endpoint object. May be None.
653
654 @raises ProtocolError: If the message contents are not
655 well-formed according to the OpenID specification. This
656 includes missing fields or not signing fields that should
657 be signed.
658
659 @raises DiscoveryFailure: If the subject of the id_res message
660 does not match the supplied endpoint, and discovery on the
661 identifier in the message fails (this should only happen
662 when using OpenID 2)
663
664 @returntype: L{Response}
665 """
666 signed_list_str = message.getArg(OPENID_NS, 'signed')
667 if signed_list_str is None:
668 raise ProtocolError("Response missing signed list")
669
670 signed_list = signed_list_str.split(',')
671
672
673
674 self._idResCheckForFields(message, signed_list)
675
676
677 endpoint = self._verifyDiscoveryResults(message, endpoint)
678
679 self._idResCheckSignature(message, endpoint.server_url)
680
681 response_identity = message.getArg(OPENID_NS, 'identity')
682
683
684 self._idResCheckNonce(message, endpoint)
685
686 signed_fields = ["openid." + s for s in signed_list]
687 return SuccessResponse(endpoint, message, signed_fields)
688
690 """Extract the nonce from an OpenID 1 response
691
692 See the openid1_nonce_query_arg_name class variable
693
694 @returns: The nonce as a string or None
695 """
696 return_to = message.getArg(OPENID1_NS, 'return_to', None)
697 if return_to is None:
698 return None
699
700 parsed_url = urlparse(return_to)
701 query = parsed_url[4]
702 for k, v in cgi.parse_qsl(query):
703 if k == self.openid1_nonce_query_arg_name:
704 return v
705
706 return None
707
728
756
758
759
760
761
762
763
764 basic_fields = ['return_to', 'assoc_handle', 'sig']
765 basic_sig_fields = ['return_to', 'identity']
766
767 require_fields = {
768 OPENID2_NS: basic_fields + ['op_endpoint'],
769 OPENID1_NS: basic_fields + ['identity'],
770 }
771
772 require_sigs = {
773 OPENID2_NS: basic_sig_fields + ['response_nonce',
774 'claimed_id',
775 'assoc_handle',],
776 OPENID1_NS: basic_sig_fields,
777 }
778
779 for field in require_fields[message.getOpenIDNamespace()]:
780 if not message.hasKey(OPENID_NS, field):
781 raise ProtocolError('Missing required field %r' % (field,))
782
783 for field in require_sigs[message.getOpenIDNamespace()]:
784
785 if message.hasKey(OPENID_NS, field) and field not in signed_list:
786 raise ProtocolError('"%s" not signed' % (field,))
787
788
790 """Verify that the arguments in the return_to URL are present in this
791 response.
792 """
793 message = Message.fromPostArgs(query)
794 return_to = message.getArg(OPENID_NS, 'return_to')
795
796
797 if not return_to:
798 raise ProtocolError("no openid.return_to in query %r" % (query,))
799 parsed_url = urlparse(return_to)
800 rt_query = parsed_url[4]
801 for rt_key, rt_value in cgi.parse_qsl(rt_query):
802 try:
803 value = query[rt_key]
804 if rt_value != value:
805 format = ("parameter %s value %r does not match "
806 "return_to's value %r")
807 raise ProtocolError(format % (rt_key, value, rt_value))
808 except KeyError:
809 format = "return_to parameter %s absent from query %r"
810 raise ProtocolError(format % (rt_key, query))
811
812 _verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
813
815 """
816 Extract the information from an OpenID assertion message and
817 verify it against the original
818
819 @param endpoint: The endpoint that resulted from doing discovery
820 @param resp_msg: The id_res message object
821 """
822 if resp_msg.getOpenIDNamespace() == OPENID2_NS:
823 return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
824 else:
825 return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
826
827
829 to_match = OpenIDServiceEndpoint()
830 to_match.type_uris = [OPENID_2_0_TYPE]
831 to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
832 to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
833
834
835 to_match.server_url = resp_msg.getArg(
836 OPENID2_NS, 'op_endpoint', no_default)
837
838
839
840 if (to_match.claimed_id is None and
841 to_match.local_id is not None):
842 raise ProtocolError(
843 'openid.identity is present without openid.claimed_id')
844
845 elif (to_match.claimed_id is not None and
846 to_match.local_id is None):
847 raise ProtocolError(
848 'openid.claimed_id is present without openid.identity')
849
850
851
852
853 elif to_match.claimed_id is None:
854 return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
855
856
857
858
859
860 elif not endpoint:
861 oidutil.log('No pre-discovered information supplied.')
862 return self._discoverAndVerify(to_match)
863
864 elif to_match.claimed_id != endpoint.claimed_id:
865 oidutil.log('Mismatched pre-discovered session data. '
866 'Claimed ID in session=%s, in assertion=%s' %
867 (endpoint.claimed_id, to_match.claimed_id))
868 return self._discoverAndVerify(to_match)
869
870
871
872
873 else:
874 self._verifyDiscoverySingle(endpoint, to_match)
875 return endpoint
876
878 if endpoint is None:
879 raise RuntimeError(
880 'When using OpenID 1, the claimed ID must be supplied, '
881 'either by passing it through as a return_to parameter '
882 'or by using a session, and supplied to the GenericConsumer '
883 'as the argument to complete()')
884
885 to_match = OpenIDServiceEndpoint()
886 to_match.type_uris = [OPENID_1_1_TYPE]
887 to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
888
889 to_match.claimed_id = endpoint.claimed_id
890
891 if to_match.local_id is None:
892 raise ProtocolError('Missing required field openid.identity')
893
894 to_match_1_0 = copy.copy(to_match)
895 to_match_1_0.type_uris = [OPENID_1_0_TYPE]
896
897 try:
898 self._verifyDiscoverySingle(endpoint, to_match)
899 except TypeURIMismatch:
900 self._verifyDiscoverySingle(endpoint, to_match_1_0)
901
902 return endpoint
903
905 """Verify that the given endpoint matches the information
906 extracted from the OpenID assertion, and raise an exception if
907 there is a mismatch.
908
909 @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
910 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
911
912 @rtype: NoneType
913
914 @raises ProtocolError: when the endpoint does not match the
915 discovered information.
916 """
917
918
919 for type_uri in to_match.type_uris:
920 if not endpoint.usesExtension(type_uri):
921 raise TypeURIMismatch(
922 'Required type %r not present' % (type_uri,))
923
924 if to_match.claimed_id != endpoint.claimed_id:
925 raise ProtocolError(
926 'Claimed ID does not match (different subjects!), '
927 'Expected %s, got %s' %
928 (to_match.claimed_id, endpoint.claimed_id))
929
930 if to_match.getLocalID() != endpoint.getLocalID():
931 raise ProtocolError('local_id mismatch. Expected %s, got %s' %
932 (to_match.getLocalID(), endpoint.getLocalID()))
933
934
935
936
937
938
939 if to_match.server_url is None:
940 assert to_match.preferredNamespace() == OPENID1_NS, (
941 """The code calling this must ensure that OpenID 2
942 responses have a non-none `openid.op_endpoint' and
943 that it is set as the `server_url' attribute of the
944 `to_match' endpoint.""")
945
946 elif to_match.server_url != endpoint.server_url:
947 raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
948 (to_match.server_url, endpoint.server_url))
949
951 """Given an endpoint object created from the information in an
952 OpenID response, perform discovery and verify the discovery
953 results, returning the matching endpoint that is the result of
954 doing that discovery.
955
956 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
957 @param to_match: The endpoint whose information we're confirming
958
959 @rtype: openid.consumer.discover.OpenIDServiceEndpoint
960 @returns: The result of performing discovery on the claimed
961 identifier in `to_match'
962
963 @raises ProtocolError: when discovery fails.
964 """
965 oidutil.log('Performing discovery on %s' % (to_match.claimed_id,))
966 _, services = self._discover(to_match.claimed_id)
967 if not services:
968 raise DiscoveryFailure('No OpenID information found at %s' %
969 (to_match.claimed_id,), None)
970
971
972
973 failure_messages = []
974 for endpoint in services:
975 try:
976 self._verifyDiscoverySingle(endpoint, to_match)
977 except ProtocolError, why:
978 failure_messages.append(why[0])
979 else:
980
981
982 return endpoint
983 else:
984 oidutil.log('Discovery verification failure for %s' %
985 (to_match.claimed_id,))
986 for failure_message in failure_messages:
987 oidutil.log(' * Endpoint mismatch: ' + failure_message)
988
989 raise DiscoveryFailure(
990 'No matching endpoint found after discovering %s'
991 % (to_match.claimed_id,), None)
992
994 oidutil.log('Using OpenID check_authentication')
995 request = self._createCheckAuthRequest(message)
996 if request is None:
997 return False
998 try:
999 response = self._makeKVPost(request, server_url)
1000 except (fetchers.HTTPFetchingError, ServerError), e:
1001 oidutil.log('check_authentication failed: %s' % (e[0],))
1002 return False
1003 else:
1004 return self._processCheckAuthResponse(response, server_url)
1005
1007 """Generate a check_authentication request message given an
1008 id_res message.
1009 """
1010
1011
1012 whitelist = ['assoc_handle', 'sig', 'signed', 'invalidate_handle']
1013
1014 check_args = {}
1015 for k in whitelist:
1016 val = message.getArg(OPENID_NS, k)
1017 if val is not None:
1018 check_args[k] = val
1019
1020 signed = message.getArg(OPENID_NS, 'signed')
1021 if signed:
1022 for k in signed.split(','):
1023 if k == 'ns':
1024 check_args['ns'] = message.getOpenIDNamespace()
1025 continue
1026
1027 val = message.getAliasedArg(k)
1028
1029
1030 if val is None:
1031 oidutil.log('Missing signed field %r' % (k,))
1032 return None
1033
1034 check_args[k] = val
1035
1036 check_args['mode'] = 'check_authentication'
1037 return Message.fromOpenIDArgs(check_args)
1038
1040 """Process the response message from a check_authentication
1041 request, invalidating associations if requested.
1042 """
1043 is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
1044
1045 invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
1046 if invalidate_handle is not None:
1047 oidutil.log(
1048 'Received "invalidate_handle" from server %s' % (server_url,))
1049 if self.store is None:
1050 oidutil.log('Unexpectedly got invalidate_handle without '
1051 'a store!')
1052 else:
1053 self.store.removeAssociation(server_url, invalidate_handle)
1054
1055 if is_valid == 'true':
1056 return True
1057 else:
1058 oidutil.log('Server responds that checkAuth call is not valid')
1059 return False
1060
1062 """Get an association for the endpoint's server_url.
1063
1064 First try seeing if we have a good association in the
1065 store. If we do not, then attempt to negotiate an association
1066 with the server.
1067
1068 If we negotiate a good association, it will get stored.
1069
1070 @returns: A valid association for the endpoint's server_url or None
1071 @rtype: openid.association.Association or NoneType
1072 """
1073 assoc = self.store.getAssociation(endpoint.server_url)
1074
1075 if assoc is None or assoc.expiresIn <= 0:
1076 assoc = self._negotiateAssociation(endpoint)
1077 if assoc is not None:
1078 self.store.storeAssociation(endpoint.server_url, assoc)
1079
1080 return assoc
1081
1083 """Make association requests to the server, attempting to
1084 create a new association.
1085
1086 @returns: a new association object
1087
1088 @rtype: openid.association.Association
1089
1090 @raises Exception: errors that the fetcher might raise. These are
1091 intended to be propagated up to the library's entrance point.
1092 """
1093
1094 assoc_type, session_type = self.negotiator.getAllowedType()
1095
1096 try:
1097 assoc = self._requestAssociation(
1098 endpoint, assoc_type, session_type)
1099 except ServerError, why:
1100
1101
1102 if why.error_code != 'unsupported-type' or \
1103 why.message.isOpenID1():
1104 oidutil.log(
1105 'Server error when requesting an association from %r: %s'
1106 % (endpoint.server_url, why.error_text))
1107 return None
1108
1109
1110
1111
1112 oidutil.log(
1113 'Unsupported association type %s: %s' % (assoc_type,
1114 why.error_text,))
1115
1116
1117
1118 assoc_type = why.message.getArg(OPENID_NS, 'assoc_type')
1119 session_type = why.message.getArg(OPENID_NS, 'session_type')
1120
1121 if assoc_type is None or session_type is None:
1122 oidutil.log('Server responded with unsupported association '
1123 'session but did not supply a fallback.')
1124 return None
1125 elif not self.negotiator.isAllowed(assoc_type, session_type):
1126 fmt = ('Server sent unsupported session/association type: '
1127 'session_type=%s, assoc_type=%s')
1128 oidutil.log(fmt % (session_type, assoc_type))
1129 return None
1130 else:
1131
1132
1133
1134 try:
1135 assoc = self._requestAssociation(
1136 endpoint, assoc_type, session_type)
1137 except ServerError, why:
1138
1139
1140 oidutil.log('Server %s refused its suggested association '
1141 'type: session_type=%s, assoc_type=%s'
1142 % (endpoint.server_url, session_type,
1143 assoc_type))
1144 return None
1145 else:
1146 return assoc
1147 else:
1148 return assoc
1149
1151 """Make and process one association request to this endpoint's
1152 OP endpoint URL.
1153
1154 @returns: An association object or None if the association
1155 processing failed.
1156
1157 @raises ServerError: when the remote OpenID server returns an error.
1158 """
1159 assoc_session, args = self._createAssociateRequest(
1160 endpoint, assoc_type, session_type)
1161
1162 try:
1163 response = self._makeKVPost(args, endpoint.server_url)
1164 except fetchers.HTTPFetchingError, why:
1165 oidutil.log('openid.associate request failed: %s' % (why[0],))
1166 return None
1167
1168 try:
1169 assoc = self._extractAssociation(response, assoc_session)
1170 except KeyError, why:
1171 oidutil.log('Missing required parameter in response from %s: %s'
1172 % (endpoint.server_url, why[0]))
1173 return None
1174 except ProtocolError, why:
1175 oidutil.log('Protocol error parsing response from %s: %s' % (
1176 endpoint.server_url, why[0]))
1177 return None
1178 else:
1179 return assoc
1180
1182 """Create an association request for the given assoc_type and
1183 session_type.
1184
1185 @param endpoint: The endpoint whose server_url will be
1186 queried. The important bit about the endpoint is whether
1187 it's in compatiblity mode (OpenID 1.1)
1188
1189 @param assoc_type: The association type that the request
1190 should ask for.
1191 @type assoc_type: str
1192
1193 @param session_type: The session type that should be used in
1194 the association request. The session_type is used to
1195 create an association session object, and that session
1196 object is asked for any additional fields that it needs to
1197 add to the request.
1198 @type session_type: str
1199
1200 @returns: a pair of the association session object and the
1201 request message that will be sent to the server.
1202 @rtype: (association session type (depends on session_type),
1203 openid.message.Message)
1204 """
1205 session_type_class = self.session_types[session_type]
1206 assoc_session = session_type_class()
1207
1208 args = {
1209 'mode': 'associate',
1210 'assoc_type': assoc_type,
1211 }
1212
1213 if not endpoint.compatibilityMode():
1214 args['ns'] = OPENID2_NS
1215
1216
1217
1218 if (not endpoint.compatibilityMode() or
1219 assoc_session.session_type != 'no-encryption'):
1220 args['session_type'] = assoc_session.session_type
1221
1222 args.update(assoc_session.getRequest())
1223 message = Message.fromOpenIDArgs(args)
1224 return assoc_session, message
1225
1227 """Given an association response message, extract the OpenID
1228 1.X session type.
1229
1230 This function mostly takes care of the 'no-encryption' default
1231 behavior in OpenID 1.
1232
1233 If the association type is plain-text, this function will
1234 return 'no-encryption'
1235
1236 @returns: The association type for this message
1237 @rtype: str
1238
1239 @raises KeyError: when the session_type field is absent.
1240 """
1241
1242
1243 session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
1244
1245
1246
1247
1248
1249
1250
1251 if session_type == 'no-encryption':
1252 oidutil.log('WARNING: OpenID server sent "no-encryption"'
1253 'for OpenID 1.X')
1254
1255
1256
1257
1258
1259 elif session_type == '' or session_type is None:
1260 session_type = 'no-encryption'
1261
1262 return session_type
1263
1265 """Attempt to extract an association from the response, given
1266 the association response message and the established
1267 association session.
1268
1269 @param assoc_response: The association response message from
1270 the server
1271 @type assoc_response: openid.message.Message
1272
1273 @param assoc_session: The association session object that was
1274 used when making the request
1275 @type assoc_session: depends on the session type of the request
1276
1277 @raises ProtocolError: when data is malformed
1278 @raises KeyError: when a field is missing
1279
1280 @rtype: openid.association.Association
1281 """
1282
1283
1284 assoc_type = assoc_response.getArg(
1285 OPENID_NS, 'assoc_type', no_default)
1286 assoc_handle = assoc_response.getArg(
1287 OPENID_NS, 'assoc_handle', no_default)
1288
1289
1290
1291
1292
1293 expires_in_str = assoc_response.getArg(
1294 OPENID_NS, 'expires_in', no_default)
1295 try:
1296 expires_in = int(expires_in_str)
1297 except ValueError, why:
1298 raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
1299
1300
1301 if assoc_response.isOpenID1():
1302 session_type = self._getOpenID1SessionType(assoc_response)
1303 else:
1304 session_type = assoc_response.getArg(
1305 OPENID2_NS, 'session_type', no_default)
1306
1307
1308 if assoc_session.session_type != session_type:
1309 if (assoc_response.isOpenID1() and
1310 session_type == 'no-encryption'):
1311
1312
1313
1314
1315
1316 assoc_session = PlainTextConsumerSession()
1317 else:
1318
1319
1320
1321 fmt = 'Session type mismatch. Expected %r, got %r'
1322 message = fmt % (assoc_session.session_type, session_type)
1323 raise ProtocolError(message)
1324
1325
1326 if assoc_type not in assoc_session.allowed_assoc_types:
1327 fmt = 'Unsupported assoc_type for session %s returned: %s'
1328 raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
1329
1330
1331
1332
1333 try:
1334 secret = assoc_session.extractSecret(assoc_response)
1335 except ValueError, why:
1336 fmt = 'Malformed response for %s session: %s'
1337 raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
1338
1339 return Association.fromExpiresIn(
1340 expires_in, assoc_handle, secret, assoc_type)
1341
1343 """An object that holds the state necessary for generating an
1344 OpenID authentication request. This object holds the association
1345 with the server and the discovered information with which the
1346 request will be made.
1347
1348 It is separate from the consumer because you may wish to add
1349 things to the request before sending it on its way to the
1350 server. It also has serialization options that let you encode the
1351 authentication request as a URL or as a form POST.
1352 """
1353
1355 """
1356 Creates a new AuthRequest object. This just stores each
1357 argument in an appropriately named field.
1358
1359 Users of this library should not create instances of this
1360 class. Instances of this class are created by the library
1361 when needed.
1362 """
1363 self.assoc = assoc
1364 self.endpoint = endpoint
1365 self.return_to_args = {}
1366 self.message = Message()
1367 self.message.setOpenIDNamespace(endpoint.preferredNamespace())
1368 self._anonymous = False
1369
1371 """Set whether this request should be made anonymously. If a
1372 request is anonymous, the identifier will not be sent in the
1373 request. This is only useful if you are making another kind of
1374 request with an extension in this request.
1375
1376 Anonymous requests are not allowed when the request is made
1377 with OpenID 1.
1378
1379 @raises ValueError: when attempting to set an OpenID1 request
1380 as anonymous
1381 """
1382 if is_anonymous and self.message.isOpenID1():
1383 raise ValueError('OpenID 1 requests MUST include the '
1384 'identifier in the request')
1385 else:
1386 self._anonymous = is_anonymous
1387
1389 """Add an extension to this checkid request.
1390
1391 @param extension_request: An object that implements the
1392 extension interface for adding arguments to an OpenID
1393 message.
1394 """
1395 extension_request.toMessage(self.message)
1396
1398 """Add an extension argument to this OpenID authentication
1399 request.
1400
1401 Use caution when adding arguments, because they will be
1402 URL-escaped and appended to the redirect URL, which can easily
1403 get quite long.
1404
1405 @param namespace: The namespace for the extension. For
1406 example, the simple registration extension uses the
1407 namespace C{sreg}.
1408
1409 @type namespace: str
1410
1411 @param key: The key within the extension namespace. For
1412 example, the nickname field in the simple registration
1413 extension's key is C{nickname}.
1414
1415 @type key: str
1416
1417 @param value: The value to provide to the server for this
1418 argument.
1419
1420 @type value: str
1421 """
1422 self.message.setArg(namespace, key, value)
1423
1424 - def getMessage(self, realm, return_to=None, immediate=False):
1425 """Produce a L{openid.message.Message} representing this request.
1426
1427 @param realm: The URL (or URL pattern) that identifies your
1428 web site to the user when she is authorizing it.
1429
1430 @type realm: str
1431
1432 @param return_to: The URL that the OpenID provider will send the
1433 user back to after attempting to verify her identity.
1434
1435 Not specifying a return_to URL means that the user will not
1436 be returned to the site issuing the request upon its
1437 completion.
1438
1439 @type return_to: str
1440
1441 @param immediate: If True, the OpenID provider is to send back
1442 a response immediately, useful for behind-the-scenes
1443 authentication attempts. Otherwise the OpenID provider
1444 may engage the user before providing a response. This is
1445 the default case, as the user may need to provide
1446 credentials or approve the request before a positive
1447 response can be sent.
1448
1449 @type immediate: bool
1450
1451 @returntype: L{openid.message.Message}
1452 """
1453 if return_to:
1454 return_to = oidutil.appendArgs(return_to, self.return_to_args)
1455 elif immediate:
1456 raise ValueError(
1457 '"return_to" is mandatory when using "checkid_immediate"')
1458 elif self.message.isOpenID1():
1459 raise ValueError('"return_to" is mandatory for OpenID 1 requests')
1460 elif self.return_to_args:
1461 raise ValueError('extra "return_to" arguments were specified, '
1462 'but no return_to was specified')
1463
1464 if immediate:
1465 mode = 'checkid_immediate'
1466 else:
1467 mode = 'checkid_setup'
1468
1469 message = self.message.copy()
1470 if message.isOpenID1():
1471 realm_key = 'trust_root'
1472 else:
1473 realm_key = 'realm'
1474
1475 message.updateArgs(OPENID_NS,
1476 {
1477 realm_key:realm,
1478 'mode':mode,
1479 'return_to':return_to,
1480 })
1481
1482 if not self._anonymous:
1483 if self.endpoint.isOPIdentifier():
1484
1485
1486
1487 claimed_id = request_identity = IDENTIFIER_SELECT
1488 else:
1489 request_identity = self.endpoint.getLocalID()
1490 claimed_id = self.endpoint.claimed_id
1491
1492
1493 message.setArg(OPENID_NS, 'identity', request_identity)
1494
1495 if message.isOpenID2():
1496 message.setArg(OPENID2_NS, 'claimed_id', claimed_id)
1497
1498 if self.assoc:
1499 message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle)
1500
1501 return message
1502
1503 - def redirectURL(self, realm, return_to=None, immediate=False):
1504 """Returns a URL with an encoded OpenID request.
1505
1506 The resulting URL is the OpenID provider's endpoint URL with
1507 parameters appended as query arguments. You should redirect
1508 the user agent to this URL.
1509
1510 OpenID 2.0 endpoints also accept POST requests, see
1511 C{L{shouldSendRedirect}} and C{L{formMarkup}}.
1512
1513 @param realm: The URL (or URL pattern) that identifies your
1514 web site to the user when she is authorizing it.
1515
1516 @type realm: str
1517
1518 @param return_to: The URL that the OpenID provider will send the
1519 user back to after attempting to verify her identity.
1520
1521 Not specifying a return_to URL means that the user will not
1522 be returned to the site issuing the request upon its
1523 completion.
1524
1525 @type return_to: str
1526
1527 @param immediate: If True, the OpenID provider is to send back
1528 a response immediately, useful for behind-the-scenes
1529 authentication attempts. Otherwise the OpenID provider
1530 may engage the user before providing a response. This is
1531 the default case, as the user may need to provide
1532 credentials or approve the request before a positive
1533 response can be sent.
1534
1535 @type immediate: bool
1536
1537 @returns: The URL to redirect the user agent to.
1538
1539 @returntype: str
1540 """
1541 message = self.getMessage(realm, return_to, immediate)
1542 return message.toURL(self.endpoint.server_url)
1543
1557
1559 """Should this OpenID authentication request be sent as a HTTP
1560 redirect or as a POST (form submission)?
1561
1562 @rtype: bool
1563 """
1564 return self.endpoint.compatibilityMode()
1565
1566 FAILURE = 'failure'
1567 SUCCESS = 'success'
1568 CANCEL = 'cancel'
1569 SETUP_NEEDED = 'setup_needed'
1570
1572 status = None
1573
1575 self.endpoint = endpoint
1576 if endpoint is None:
1577 self.identity_url = None
1578 else:
1579 self.identity_url = endpoint.claimed_id
1580
1582 """A response with a status of SUCCESS. Indicates that this request is a
1583 successful acknowledgement from the OpenID server that the
1584 supplied URL is, indeed controlled by the requesting agent.
1585
1586 @ivar identity_url: The identity URL that has been authenticated
1587
1588 @ivar endpoint: The endpoint that authenticated the identifier. You
1589 may access other discovered information related to this endpoint,
1590 such as the CanonicalID of an XRI, through this object.
1591 @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
1592
1593 @ivar signed_fields: The arguments in the server's response that
1594 were signed and verified.
1595
1596 @cvar status: SUCCESS
1597 """
1598
1599 status = SUCCESS
1600
1601 - def __init__(self, endpoint, message, signed_fields=None):
1602
1603
1604 self.endpoint = endpoint
1605 self.identity_url = endpoint.claimed_id
1606
1607 self.message = message
1608
1609 if signed_fields is None:
1610 signed_fields = []
1611 self.signed_fields = signed_fields
1612
1614 """Was this authentication response an OpenID 1 authentication
1615 response?
1616 """
1617 return self.message.isOpenID1()
1618
1620 """Return whether a particular key is signed, regardless of
1621 its namespace alias
1622 """
1623 return self.message.getKey(ns_uri, ns_key) in self.signed_fields
1624
1625 - def getSigned(self, ns_uri, ns_key, default=None):
1626 """Return the specified signed field if available,
1627 otherwise return default
1628 """
1629 if self.isSigned(ns_uri, ns_key):
1630 return self.message.getArg(ns_uri, ns_key, default)
1631 else:
1632 return default
1633
1635 """Get signed arguments from the response message. Return a
1636 dict of all arguments in the specified namespace. If any of
1637 the arguments are not signed, return None.
1638 """
1639 msg_args = self.message.getArgs(ns_uri)
1640
1641 for key in msg_args.iterkeys():
1642 if not self.isSigned(ns_uri, key):
1643 return None
1644
1645 return msg_args
1646
1648 """Return response arguments in the specified namespace.
1649
1650 @param namespace_uri: The namespace URI of the arguments to be
1651 returned.
1652
1653 @param require_signed: True if the arguments should be among
1654 those signed in the response, False if you don't care.
1655
1656 If require_signed is True and the arguments are not signed,
1657 return None.
1658 """
1659 if require_signed:
1660 return self.getSignedNS(namespace_uri)
1661 else:
1662 return self.message.getArgs(namespace_uri)
1663
1665 """Get the openid.return_to argument from this response.
1666
1667 This is useful for verifying that this request was initiated
1668 by this consumer.
1669
1670 @returns: The return_to URL supplied to the server on the
1671 initial request, or C{None} if the response did not contain
1672 an C{openid.return_to} argument.
1673
1674 @returntype: str
1675 """
1676 return self.getSigned(OPENID_NS, 'return_to')
1677
1678
1679
1681 """A response with a status of FAILURE. Indicates that the OpenID
1682 protocol has failed. This could be locally or remotely triggered.
1683
1684 @ivar identity_url: The identity URL for which authenitcation was
1685 attempted, if it can be determined. Otherwise, None.
1686
1687 @ivar message: A message indicating why the request failed, if one
1688 is supplied. otherwise, None.
1689
1690 @cvar status: FAILURE
1691 """
1692
1693 status = FAILURE
1694
1695 - def __init__(self, endpoint, message=None, contact=None,
1696 reference=None):
1697 self.setEndpoint(endpoint)
1698 self.message = message
1699 self.contact = contact
1700 self.reference = reference
1701
1703 return "<%s.%s id=%r message=%r>" % (
1704 self.__class__.__module__, self.__class__.__name__,
1705 self.identity_url, self.message)
1706
1707
1709 """A response with a status of CANCEL. Indicates that the user
1710 cancelled the OpenID authentication request.
1711
1712 @ivar identity_url: The identity URL for which authenitcation was
1713 attempted, if it can be determined. Otherwise, None.
1714
1715 @cvar status: CANCEL
1716 """
1717
1718 status = CANCEL
1719
1721 self.setEndpoint(endpoint)
1722
1724 """A response with a status of SETUP_NEEDED. Indicates that the
1725 request was in immediate mode, and the server is unable to
1726 authenticate the user without further interaction.
1727
1728 @ivar identity_url: The identity URL for which authenitcation was
1729 attempted.
1730
1731 @ivar setup_url: A URL that can be used to send the user to the
1732 server to set up for authentication. The user should be
1733 redirected in to the setup_url, either in the current window
1734 or in a new browser window. C{None} in OpenID 2.0.
1735
1736 @cvar status: SETUP_NEEDED
1737 """
1738
1739 status = SETUP_NEEDED
1740
1741 - def __init__(self, endpoint, setup_url=None):
1742 self.setEndpoint(endpoint)
1743 self.setup_url = setup_url
1744