1 # -*- test-case-name: openid.test.test_consumer -*-
2 """OpenID support for Relying Parties (aka Consumers).
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
16 The OpenID identity verification process most commonly uses the
17 following steps, as visible to the user of this library:
19 1. The user enters their OpenID into a field on the consumer's
20 site, and hits a login button.
22 2. The consumer site discovers the user's OpenID provider using
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.
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.
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
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
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.
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
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.
64 SESSIONS, STORES, AND STATELESS MODE
65 ====================================
67 The C{L{Consumer}} object keeps track of two types of state:
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.
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.
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}.
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.
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.
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.
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
130 Integrating this library into an application is usually a
131 relatively straightforward process. The process should basically
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.
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.
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}}
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.
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
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
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.
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
177 @var SUCCESS: constant used as the status for
178 L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects.
180 @var FAILURE: constant used as the status for
181 L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects.
183 @var CANCEL: constant used as the status for
184 L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects.
186 @var SETUP_NEEDED: constant used as the status for
187 L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
193 from urlparse
import urlparse
, urldefrag
195 from openid
import fetchers
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
, BARE_NS
201 from openid
import cryptutil
202 from openid
import oidutil
203 from openid
.association
import Association
, default_negotiator
, \
205 from openid
.dh
import DiffieHellman
206 from openid
.store
.nonce
import mkNonce
, split
as splitNonce
207 from openid
.yadis
.manager
import Discovery
208 from openid
import urinorm
211 __all__
= ['AuthRequest', 'Consumer', 'SuccessResponse',
212 'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
213 'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
217 def makeKVPost(request_message
, server_url
):
218 """Make a Direct Request to an OpenID Provider and return the
219 result as a Message object.
221 @raises openid.fetchers.HTTPFetchingError: if an error is
222 encountered in making the HTTP post.
224 @rtype: L{openid.message.Message}
227 resp
= fetchers
.fetch(server_url
, body
=request_message
.toURLEncoded())
229 # Process response in separate function that can be shared by async code.
230 return _httpResponseToMessage(resp
, server_url
)
233 def _httpResponseToMessage(response
, server_url
):
234 """Adapt a POST response to a Message.
236 @type response: L{openid.fetchers.HTTPResponse}
237 @param response: Result of a POST to an OpenID endpoint.
239 @rtype: L{openid.message.Message}
241 @raises openid.fetchers.HTTPFetchingError: if the server returned a
242 status of other than 200 or 400.
244 @raises ServerError: if the server returned an OpenID error.
246 # Should this function be named Message.fromHTTPResponse instead?
247 response_message
= Message
.fromKVForm(response
.body
)
248 if response
.status
== 400:
249 raise ServerError
.fromMessage(response_message
)
251 elif response
.status
not in (200, 206):
252 fmt
= 'bad status code from server %s: %s'
253 error_message
= fmt
% (server_url
, response
.status
)
254 raise fetchers
.HTTPFetchingError(error_message
)
256 return response_message
260 class Consumer(object):
261 """An OpenID consumer implementation that performs discovery and
262 does session management.
264 @ivar consumer: an instance of an object implementing the OpenID
265 protocol, but doing no discovery or session management.
267 @type consumer: GenericConsumer
269 @ivar session: A dictionary-like object representing the user's
270 session data. This is used for keeping state of the OpenID
271 transaction when the user is redirected to the server.
273 @cvar session_key_prefix: A string that is prepended to session
274 keys to ensure that they are unique. This variable may be
275 changed to suit your application.
277 session_key_prefix
= "_openid_consumer_"
279 _token
= 'last_token'
281 _discover
= staticmethod(discover
)
283 def __init__(self
, session
, store
, consumer_class
=None):
284 """Initialize a Consumer instance.
286 You should create a new instance of the Consumer object with
287 every HTTP request that handles OpenID transactions.
289 @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>}
291 @param store: an object that implements the interface in
292 C{L{openid.store.interface.OpenIDStore}}. Several
293 implementations are provided, to cover common database
296 @type store: C{L{openid.store.interface.OpenIDStore}}
298 @see: L{openid.store.interface}
299 @see: L{openid.store}
301 self
.session
= session
302 if consumer_class
is None:
303 consumer_class
= GenericConsumer
304 self
.consumer
= consumer_class(store
)
305 self
._token
_key
= self
.session_key_prefix
+ self
._token
307 def begin(self
, user_url
, anonymous
=False):
308 """Start the OpenID authentication process. See steps 1-2 in
309 the overview at the top of this file.
311 @param user_url: Identity URL given by the user. This method
312 performs a textual transformation of the URL to try and
313 make sure it is normalized. For example, a user_url of
314 example.com will be normalized to http://example.com/
315 normalizing and resolving any redirects the server might
318 @type user_url: unicode
320 @param anonymous: Whether to make an anonymous request of the OpenID
321 provider. Such a request does not ask for an authorization
322 assertion for an OpenID identifier, but may be used with
323 extensions to pass other data. e.g. "I don't care who you are,
324 but I'd like to know your time zone."
326 @type anonymous: bool
328 @returns: An object containing the discovered information will
329 be returned, with a method for building a redirect URL to
330 the server, as described in step 3 of the overview. This
331 object may also be used to add extension arguments to the
333 L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>}
336 @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
338 @raises openid.consumer.discover.DiscoveryFailure: when I fail to
339 find an OpenID server for this URL. If the C{yadis} package
340 is available, L{openid.consumer.discover.DiscoveryFailure} is
341 an alias for C{yadis.discover.DiscoveryFailure}.
343 disco
= Discovery(self
.session
, user_url
, self
.session_key_prefix
)
345 service
= disco
.getNextService(self
._discover
)
346 except fetchers
.HTTPFetchingError
, why
:
347 raise DiscoveryFailure(
348 'Error fetching XRDS document: %s' % (why
[0],), None)
351 raise DiscoveryFailure(
352 'No usable OpenID services found for %s' % (user_url
,), None)
354 return self
.beginWithoutDiscovery(service
, anonymous
)
356 def beginWithoutDiscovery(self
, service
, anonymous
=False):
357 """Start OpenID verification without doing OpenID server
358 discovery. This method is used internally by Consumer.begin
359 after discovery is performed, and exists to provide an
360 interface for library users needing to perform their own
363 @param service: an OpenID service endpoint descriptor. This
364 object and factories for it are found in the
365 L{openid.consumer.discover} module.
368 L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
370 @returns: an OpenID authentication request object.
372 @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
374 @See: Openid.consumer.consumer.Consumer.begin
375 @see: openid.consumer.discover
377 auth_req
= self
.consumer
.begin(service
)
378 self
.session
[self
._token
_key
] = auth_req
.endpoint
381 auth_req
.setAnonymous(anonymous
)
382 except ValueError, why
:
383 raise ProtocolError(str(why
))
387 def complete(self
, query
, current_url
):
388 """Called to interpret the server's response to an OpenID
389 request. It is called in step 4 of the flow described in the
392 @param query: A dictionary of the query parameters for this
395 @param current_url: The URL used to invoke the application.
396 Extract the URL from your application's web
397 request framework and specify it here to have it checked
398 against the openid.return_to value in the response. If
399 the return_to URL check fails, the status of the
400 completion will be FAILURE.
402 @returns: a subclass of Response. The type of response is
403 indicated by the status attribute, which will be one of
404 SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
406 @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>}
407 @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>}
408 @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
409 @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>}
412 endpoint
= self
.session
.get(self
._token
_key
)
414 message
= Message
.fromPostArgs(query
)
415 response
= self
.consumer
.complete(message
, endpoint
, current_url
)
418 del self
.session
[self
._token
_key
]
422 if (response
.status
in ['success', 'cancel'] and
423 response
.identity_url
is not None):
425 disco
= Discovery(self
.session
,
426 response
.identity_url
,
427 self
.session_key_prefix
)
428 # This is OK to do even if we did not do discovery in
430 disco
.cleanup(force
=True)
434 def setAssociationPreference(self
, association_preferences
):
435 """Set the order in which association types/sessions should be
436 attempted. For instance, to only allow HMAC-SHA256
437 associations created with a DH-SHA256 association session:
439 >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
441 Any association type/association type pair that is not in this
442 list will not be attempted at all.
444 @param association_preferences: The list of allowed
445 (association type, association session type) pairs that
446 should be allowed for this consumer to use, in order from
447 most preferred to least preferred.
448 @type association_preferences: [(str, str)]
452 @see: C{L{openid.association.SessionNegotiator}}
454 self
.consumer
.negotiator
= SessionNegotiator(association_preferences
)
456 class DiffieHellmanSHA1ConsumerSession(object):
457 session_type
= 'DH-SHA1'
458 hash_func
= staticmethod(cryptutil
.sha1
)
460 allowed_assoc_types
= ['HMAC-SHA1']
462 def __init__(self
, dh
=None):
464 dh
= DiffieHellman
.fromDefaults()
468 def getRequest(self
):
469 cpub
= cryptutil
.longToBase64(self
.dh
.public
)
471 args
= {'dh_consumer_public': cpub
}
473 if not self
.dh
.usingDefaultValues():
475 'dh_modulus': cryptutil
.longToBase64(self
.dh
.modulus
),
476 'dh_gen': cryptutil
.longToBase64(self
.dh
.generator
),
481 def extractSecret(self
, response
):
482 dh_server_public64
= response
.getArg(
483 OPENID_NS
, 'dh_server_public', no_default
)
484 enc_mac_key64
= response
.getArg(OPENID_NS
, 'enc_mac_key', no_default
)
485 dh_server_public
= cryptutil
.base64ToLong(dh_server_public64
)
486 enc_mac_key
= oidutil
.fromBase64(enc_mac_key64
)
487 return self
.dh
.xorSecret(dh_server_public
, enc_mac_key
, self
.hash_func
)
489 class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession
):
490 session_type
= 'DH-SHA256'
491 hash_func
= staticmethod(cryptutil
.sha256
)
493 allowed_assoc_types
= ['HMAC-SHA256']
495 class PlainTextConsumerSession(object):
496 session_type
= 'no-encryption'
497 allowed_assoc_types
= ['HMAC-SHA1', 'HMAC-SHA256']
499 def getRequest(self
):
502 def extractSecret(self
, response
):
503 mac_key64
= response
.getArg(OPENID_NS
, 'mac_key', no_default
)
504 return oidutil
.fromBase64(mac_key64
)
506 class SetupNeededError(Exception):
507 """Internally-used exception that indicates that an immediate-mode
508 request cancelled."""
509 def __init__(self
, user_setup_url
=None):
510 Exception.__init
__(self
, user_setup_url
)
511 self
.user_setup_url
= user_setup_url
513 class ProtocolError(ValueError):
514 """Exception that indicates that a message violated the
515 protocol. It is raised and caught internally to this file."""
517 class TypeURIMismatch(ProtocolError
):
518 """A protocol error arising from type URIs mismatching
521 def __init__(self
, expected
, endpoint
):
522 ProtocolError
.__init
__(self
, expected
, endpoint
)
523 self
.expected
= expected
524 self
.endpoint
= endpoint
527 s
= '<%s.%s: Required type %s not found in %s for endpoint %s>' % (
528 self
.__class
__.__module
__, self
.__class
__.__name
__,
529 self
.expected
, self
.endpoint
.type_uris
, self
.endpoint
)
534 class ServerError(Exception):
535 """Exception that is raised when the server returns a 400 response
536 code to a direct request."""
538 def __init__(self
, error_text
, error_code
, message
):
539 Exception.__init
__(self
, error_text
)
540 self
.error_text
= error_text
541 self
.error_code
= error_code
542 self
.message
= message
544 def fromMessage(cls
, message
):
545 """Generate a ServerError instance, extracting the error text
546 and the error code from the message."""
547 error_text
= message
.getArg(
548 OPENID_NS
, 'error', '<no error message supplied>')
549 error_code
= message
.getArg(OPENID_NS
, 'error_code')
550 return cls(error_text
, error_code
, message
)
552 fromMessage
= classmethod(fromMessage
)
554 class GenericConsumer(object):
555 """This is the implementation of the common logic for OpenID
556 consumers. It is unaware of the application in which it is
559 @ivar negotiator: An object that controls the kind of associations
560 that the consumer makes. It defaults to
561 C{L{openid.association.default_negotiator}}. Assign a
562 different negotiator to it if you have specific requirements
563 for how associations are made.
564 @type negotiator: C{L{openid.association.SessionNegotiator}}
567 # The name of the query parameter that gets added to the return_to
568 # URL when using OpenID1. You can change this value if you want or
569 # need a different name, but don't make it start with openid,
570 # because it's not a standard protocol thing for OpenID1. For
571 # OpenID2, the library will take care of the nonce using standard
572 # OpenID query parameter names.
573 openid1_nonce_query_arg_name
= 'janrain_nonce'
575 # Another query parameter that gets added to the return_to for
576 # OpenID 1; if the user's session state is lost, use this claimed
577 # identifier to do discovery when verifying the response.
578 openid1_return_to_identifier_name
= 'openid1_claimed_id'
581 'DH-SHA1':DiffieHellmanSHA1ConsumerSession
,
582 'DH-SHA256':DiffieHellmanSHA256ConsumerSession
,
583 'no-encryption':PlainTextConsumerSession
,
586 _discover
= staticmethod(discover
)
588 def __init__(self
, store
):
590 self
.negotiator
= default_negotiator
.copy()
592 def begin(self
, service_endpoint
):
593 """Create an AuthRequest object for the specified
594 service_endpoint. This method will create an association if
596 if self
.store
is None:
599 assoc
= self
._getAssociation
(service_endpoint
)
601 request
= AuthRequest(service_endpoint
, assoc
)
602 request
.return_to_args
[self
.openid1_nonce_query_arg_name
] = mkNonce()
604 if request
.message
.isOpenID1():
605 request
.return_to_args
[self
.openid1_return_to_identifier_name
] = \
606 request
.endpoint
.claimed_id
610 def complete(self
, message
, endpoint
, return_to
):
611 """Process the OpenID message, using the specified endpoint
612 and return_to URL as context. This method will handle any
613 OpenID message that is sent to the return_to URL.
615 mode
= message
.getArg(OPENID_NS
, 'mode', '<No mode set>')
617 modeMethod
= getattr(self
, '_complete_' + mode
,
618 self
._completeInvalid
)
620 return modeMethod(message
, endpoint
, return_to
)
622 def _complete_cancel(self
, message
, endpoint
, _
):
623 return CancelResponse(endpoint
)
625 def _complete_error(self
, message
, endpoint
, _
):
626 error
= message
.getArg(OPENID_NS
, 'error')
627 contact
= message
.getArg(OPENID_NS
, 'contact')
628 reference
= message
.getArg(OPENID_NS
, 'reference')
630 return FailureResponse(endpoint
, error
, contact
=contact
,
633 def _complete_setup_needed(self
, message
, endpoint
, _
):
634 if not message
.isOpenID2():
635 return self
._completeInvalid
(message
, endpoint
, _
)
637 return SetupNeededResponse(endpoint
)
639 def _complete_id_res(self
, message
, endpoint
, return_to
):
641 self
._checkSetupNeeded
(message
)
642 except SetupNeededError
, why
:
643 return SetupNeededResponse(endpoint
, why
.user_setup_url
)
646 return self
._doIdRes
(message
, endpoint
, return_to
)
647 except (ProtocolError
, DiscoveryFailure
), why
:
648 return FailureResponse(endpoint
, why
[0])
650 def _completeInvalid(self
, message
, endpoint
, _
):
651 mode
= message
.getArg(OPENID_NS
, 'mode', '<No mode set>')
652 return FailureResponse(endpoint
,
653 'Invalid openid.mode: %r' % (mode
,))
655 def _checkReturnTo(self
, message
, return_to
):
656 """Check an OpenID message and its openid.return_to value
657 against a return_to URL from an application. Return True on
658 success, False on failure.
660 # Check the openid.return_to args against args in the original
663 self
._verifyReturnToArgs
(message
.toPostArgs())
664 except ProtocolError
, why
:
665 oidutil
.log("Verifying return_to arguments: %s" % (why
[0],))
668 # Check the return_to base URL against the one in the message.
669 msg_return_to
= message
.getArg(OPENID_NS
, 'return_to')
671 # The URL scheme, authority, and path MUST be the same between
673 app_parts
= urlparse(urinorm
.urinorm(return_to
))
674 msg_parts
= urlparse(urinorm
.urinorm(msg_return_to
))
676 # (addressing scheme, network location, path) must be equal in
678 for part
in range(0, 3):
679 if app_parts
[part
] != msg_parts
[part
]:
684 _makeKVPost
= staticmethod(makeKVPost
)
686 def _checkSetupNeeded(self
, message
):
687 """Check an id_res message to see if it is a
688 checkid_immediate cancel response.
690 @raises SetupNeededError: if it is a checkid_immediate cancellation
692 # In OpenID 1, we check to see if this is a cancel from
693 # immediate mode by the presence of the user_setup_url
695 if message
.isOpenID1():
696 user_setup_url
= message
.getArg(OPENID1_NS
, 'user_setup_url')
697 if user_setup_url
is not None:
698 raise SetupNeededError(user_setup_url
)
700 def _doIdRes(self
, message
, endpoint
, return_to
):
701 """Handle id_res responses that are not cancellations of
702 immediate mode requests.
704 @param message: the response paramaters.
705 @param endpoint: the discovered endpoint object. May be None.
707 @raises ProtocolError: If the message contents are not
708 well-formed according to the OpenID specification. This
709 includes missing fields or not signing fields that should
712 @raises DiscoveryFailure: If the subject of the id_res message
713 does not match the supplied endpoint, and discovery on the
714 identifier in the message fails (this should only happen
717 @returntype: L{Response}
719 # Checks for presence of appropriate fields (and checks
720 # signed list fields)
721 self
._idResCheckForFields
(message
)
723 if not self
._checkReturnTo
(message
, return_to
):
725 "return_to does not match return URL. Expected %r, got %r"
726 % (return_to
, message
.getArg(OPENID_NS
, 'return_to')))
729 # Verify discovery information:
730 endpoint
= self
._verifyDiscoveryResults
(message
, endpoint
)
731 oidutil
.log("Received id_res response from %s using association %s" %
732 (endpoint
.server_url
,
733 message
.getArg(OPENID_NS
, 'assoc_handle')))
735 self
._idResCheckSignature
(message
, endpoint
.server_url
)
737 # Will raise a ProtocolError if the nonce is bad
738 self
._idResCheckNonce
(message
, endpoint
)
740 signed_list_str
= message
.getArg(OPENID_NS
, 'signed', no_default
)
741 signed_list
= signed_list_str
.split(',')
742 signed_fields
= ["openid." + s
for s
in signed_list
]
743 return SuccessResponse(endpoint
, message
, signed_fields
)
745 def _idResGetNonceOpenID1(self
, message
, endpoint
):
746 """Extract the nonce from an OpenID 1 response. Return the
747 nonce from the BARE_NS since we independently check the
748 return_to arguments are the same as those in the response
751 See the openid1_nonce_query_arg_name class variable
753 @returns: The nonce as a string or None
755 return message
.getArg(BARE_NS
, self
.openid1_nonce_query_arg_name
)
757 def _idResCheckNonce(self
, message
, endpoint
):
758 if message
.isOpenID1():
759 # This indicates that the nonce was generated by the consumer
760 nonce
= self
._idResGetNonceOpenID
1(message
, endpoint
)
763 nonce
= message
.getArg(OPENID2_NS
, 'response_nonce')
764 server_url
= endpoint
.server_url
767 raise ProtocolError('Nonce missing from response')
770 timestamp
, salt
= splitNonce(nonce
)
771 except ValueError, why
:
772 raise ProtocolError('Malformed nonce: %s' % (why
[0],))
774 if (self
.store
is not None and
775 not self
.store
.useNonce(server_url
, timestamp
, salt
)):
776 raise ProtocolError('Nonce already used or out of range')
778 def _idResCheckSignature(self
, message
, server_url
):
779 assoc_handle
= message
.getArg(OPENID_NS
, 'assoc_handle')
780 if self
.store
is None:
783 assoc
= self
.store
.getAssociation(server_url
, assoc_handle
)
786 if assoc
.getExpiresIn() <= 0:
787 # XXX: It might be a good idea sometimes to re-start the
788 # authentication with a new association. Doing it
789 # automatically opens the possibility for
790 # denial-of-service by a server that just returns expired
791 # associations (or really short-lived associations)
793 'Association with %s expired' % (server_url
,))
795 if not assoc
.checkMessageSignature(message
):
796 raise ProtocolError('Bad signature')
799 # It's not an association we know about. Stateless mode is our
800 # only possible path for recovery.
801 # XXX - async framework will not want to block on this call to
803 if not self
._checkAuth
(message
, server_url
):
804 raise ProtocolError('Server denied check_authentication')
806 def _idResCheckForFields(self
, message
):
807 # XXX: this should be handled by the code that processes the
808 # response (that is, if a field is missing, we should not have
809 # to explicitly check that it's present, just make sure that
810 # the fields are actually being used by the rest of the code
811 # in tests). Although, which fields are signed does need to be
813 basic_fields
= ['return_to', 'assoc_handle', 'sig', 'signed']
814 basic_sig_fields
= ['return_to', 'identity']
817 OPENID2_NS
: basic_fields
+ ['op_endpoint'],
818 OPENID1_NS
: basic_fields
+ ['identity'],
822 OPENID2_NS
: basic_sig_fields
+ ['response_nonce',
825 OPENID1_NS
: basic_sig_fields
,
828 for field
in require_fields
[message
.getOpenIDNamespace()]:
829 if not message
.hasKey(OPENID_NS
, field
):
830 raise ProtocolError('Missing required field %r' % (field
,))
832 signed_list_str
= message
.getArg(OPENID_NS
, 'signed', no_default
)
833 signed_list
= signed_list_str
.split(',')
835 for field
in require_sigs
[message
.getOpenIDNamespace()]:
836 # Field is present and not in signed list
837 if message
.hasKey(OPENID_NS
, field
) and field
not in signed_list
:
838 raise ProtocolError('"%s" not signed' % (field
,))
841 def _verifyReturnToArgs(query
):
842 """Verify that the arguments in the return_to URL are present in this
845 message
= Message
.fromPostArgs(query
)
846 return_to
= message
.getArg(OPENID_NS
, 'return_to')
848 if return_to
is None:
849 raise ProtocolError('Response has no return_to')
851 parsed_url
= urlparse(return_to
)
852 rt_query
= parsed_url
[4]
853 parsed_args
= cgi
.parse_qsl(rt_query
)
855 for rt_key
, rt_value
in parsed_args
:
857 value
= query
[rt_key
]
858 if rt_value
!= value
:
859 format
= ("parameter %s value %r does not match "
860 "return_to's value %r")
861 raise ProtocolError(format
% (rt_key
, value
, rt_value
))
863 format
= "return_to parameter %s absent from query %r"
864 raise ProtocolError(format
% (rt_key
, query
))
866 # Make sure all non-OpenID arguments in the response are also
867 # in the signed return_to.
868 bare_args
= message
.getArgs(BARE_NS
)
869 for pair
in bare_args
.iteritems():
870 if pair
not in parsed_args
:
871 raise ProtocolError("Parameter %s not in return_to URL" % (pair
[0],))
873 _verifyReturnToArgs
= staticmethod(_verifyReturnToArgs
)
875 def _verifyDiscoveryResults(self
, resp_msg
, endpoint
=None):
877 Extract the information from an OpenID assertion message and
878 verify it against the original
880 @param endpoint: The endpoint that resulted from doing discovery
881 @param resp_msg: The id_res message object
883 @returns: the verified endpoint
885 if resp_msg
.getOpenIDNamespace() == OPENID2_NS
:
886 return self
._verifyDiscoveryResultsOpenID
2(resp_msg
, endpoint
)
888 return self
._verifyDiscoveryResultsOpenID
1(resp_msg
, endpoint
)
891 def _verifyDiscoveryResultsOpenID2(self
, resp_msg
, endpoint
):
892 to_match
= OpenIDServiceEndpoint()
893 to_match
.type_uris
= [OPENID_2_0_TYPE
]
894 to_match
.claimed_id
= resp_msg
.getArg(OPENID2_NS
, 'claimed_id')
895 to_match
.local_id
= resp_msg
.getArg(OPENID2_NS
, 'identity')
897 # Raises a KeyError when the op_endpoint is not present
898 to_match
.server_url
= resp_msg
.getArg(
899 OPENID2_NS
, 'op_endpoint', no_default
)
901 # claimed_id and identifier must both be present or both
903 if (to_match
.claimed_id
is None and
904 to_match
.local_id
is not None):
906 'openid.identity is present without openid.claimed_id')
908 elif (to_match
.claimed_id
is not None and
909 to_match
.local_id
is None):
911 'openid.claimed_id is present without openid.identity')
913 # This is a response without identifiers, so there's really no
914 # checking that we can do, so return an endpoint that's for
915 # the specified `openid.op_endpoint'
916 elif to_match
.claimed_id
is None:
917 return OpenIDServiceEndpoint
.fromOPEndpointURL(to_match
.server_url
)
919 # The claimed ID doesn't match, so we have to do discovery
920 # again. This covers not using sessions, OP identifier
921 # endpoints and responses that didn't match the original
924 oidutil
.log('No pre-discovered information supplied.')
925 endpoint
= self
._discoverAndVerify
(to_match
.claimed_id
, [to_match
])
927 # The claimed ID matches, so we use the endpoint that we
928 # discovered in initiation. This should be the most common
931 self
._verifyDiscoverySingle
(endpoint
, to_match
)
932 except ProtocolError
, e
:
934 "Error attempting to use stored discovery information: " +
936 oidutil
.log("Attempting discovery to verify endpoint")
937 endpoint
= self
._discoverAndVerify
(
938 to_match
.claimed_id
, [to_match
])
940 # The endpoint we return should have the claimed ID from the
941 # message we just verified, fragment and all.
942 if endpoint
.claimed_id
!= to_match
.claimed_id
:
943 endpoint
= copy
.copy(endpoint
)
944 endpoint
.claimed_id
= to_match
.claimed_id
947 def _verifyDiscoveryResultsOpenID1(self
, resp_msg
, endpoint
):
948 claimed_id
= resp_msg
.getArg(BARE_NS
, self
.openid1_return_to_identifier_name
)
950 if endpoint
is None and claimed_id
is None:
952 'When using OpenID 1, the claimed ID must be supplied, '
953 'either by passing it through as a return_to parameter '
954 'or by using a session, and supplied to the GenericConsumer '
955 'as the argument to complete()')
956 elif endpoint
is not None and claimed_id
is None:
957 claimed_id
= endpoint
.claimed_id
959 to_match
= OpenIDServiceEndpoint()
960 to_match
.type_uris
= [OPENID_1_1_TYPE
]
961 to_match
.local_id
= resp_msg
.getArg(OPENID1_NS
, 'identity')
962 # Restore delegate information from the initiation phase
963 to_match
.claimed_id
= claimed_id
965 if to_match
.local_id
is None:
966 raise ProtocolError('Missing required field openid.identity')
968 to_match_1_0
= copy
.copy(to_match
)
969 to_match_1_0
.type_uris
= [OPENID_1_0_TYPE
]
971 if endpoint
is not None:
974 self
._verifyDiscoverySingle
(endpoint
, to_match
)
975 except TypeURIMismatch
:
976 self
._verifyDiscoverySingle
(endpoint
, to_match_1_0
)
977 except ProtocolError
, e
:
978 oidutil
.log("Error attempting to use stored discovery information: " +
980 oidutil
.log("Attempting discovery to verify endpoint")
984 # Endpoint is either bad (failed verification) or None
985 return self
._discoverAndVerify
(claimed_id
, [to_match
, to_match_1_0
])
987 def _verifyDiscoverySingle(self
, endpoint
, to_match
):
988 """Verify that the given endpoint matches the information
989 extracted from the OpenID assertion, and raise an exception if
992 @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
993 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
997 @raises ProtocolError: when the endpoint does not match the
998 discovered information.
1000 # Every type URI that's in the to_match endpoint has to be
1001 # present in the discovered endpoint.
1002 for type_uri
in to_match
.type_uris
:
1003 if not endpoint
.usesExtension(type_uri
):
1004 raise TypeURIMismatch(type_uri
, endpoint
)
1006 # Fragments do not influence discovery, so we can't compare a
1007 # claimed identifier with a fragment to discovered information.
1008 defragged_claimed_id
, _
= urldefrag(to_match
.claimed_id
)
1009 if defragged_claimed_id
!= endpoint
.claimed_id
:
1010 raise ProtocolError(
1011 'Claimed ID does not match (different subjects!), '
1012 'Expected %s, got %s' %
1013 (defragged_claimed_id
, endpoint
.claimed_id
))
1015 if to_match
.getLocalID() != endpoint
.getLocalID():
1016 raise ProtocolError('local_id mismatch. Expected %s, got %s' %
1017 (to_match
.getLocalID(), endpoint
.getLocalID()))
1019 # If the server URL is None, this must be an OpenID 1
1020 # response, because op_endpoint is a required parameter in
1021 # OpenID 2. In that case, we don't actually care what the
1022 # discovered server_url is, because signature checking or
1023 # check_auth should take care of that check for us.
1024 if to_match
.server_url
is None:
1025 assert to_match
.preferredNamespace() == OPENID1_NS
, (
1026 """The code calling this must ensure that OpenID 2
1027 responses have a non-none `openid.op_endpoint' and
1028 that it is set as the `server_url' attribute of the
1029 `to_match' endpoint.""")
1031 elif to_match
.server_url
!= endpoint
.server_url
:
1032 raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
1033 (to_match
.server_url
, endpoint
.server_url
))
1035 def _discoverAndVerify(self
, claimed_id
, to_match_endpoints
):
1036 """Given an endpoint object created from the information in an
1037 OpenID response, perform discovery and verify the discovery
1038 results, returning the matching endpoint that is the result of
1039 doing that discovery.
1041 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
1042 @param to_match: The endpoint whose information we're confirming
1044 @rtype: openid.consumer.discover.OpenIDServiceEndpoint
1045 @returns: The result of performing discovery on the claimed
1046 identifier in `to_match'
1048 @raises DiscoveryFailure: when discovery fails.
1050 oidutil
.log('Performing discovery on %s' % (claimed_id
,))
1051 _
, services
= self
._discover
(claimed_id
)
1053 raise DiscoveryFailure('No OpenID information found at %s' %
1054 (claimed_id
,), None)
1055 return self
._verifyDiscoveredServices
(claimed_id
, services
,
1059 def _verifyDiscoveredServices(self
, claimed_id
, services
, to_match_endpoints
):
1060 """See @L{_discoverAndVerify}"""
1062 # Search the services resulting from discovery to find one
1063 # that matches the information from the assertion
1064 failure_messages
= []
1065 for endpoint
in services
:
1066 for to_match_endpoint
in to_match_endpoints
:
1068 self
._verifyDiscoverySingle
(
1069 endpoint
, to_match_endpoint
)
1070 except ProtocolError
, why
:
1071 failure_messages
.append(str(why
))
1073 # It matches, so discover verification has
1074 # succeeded. Return this endpoint.
1077 oidutil
.log('Discovery verification failure for %s' %
1079 for failure_message
in failure_messages
:
1080 oidutil
.log(' * Endpoint mismatch: ' + failure_message
)
1082 raise DiscoveryFailure(
1083 'No matching endpoint found after discovering %s'
1084 % (claimed_id
,), None)
1086 def _checkAuth(self
, message
, server_url
):
1087 """Make a check_authentication request to verify this message.
1089 @returns: True if the request is valid.
1092 oidutil
.log('Using OpenID check_authentication')
1093 request
= self
._createCheckAuthRequest
(message
)
1097 response
= self
._makeKVPost
(request
, server_url
)
1098 except (fetchers
.HTTPFetchingError
, ServerError
), e
:
1099 oidutil
.log('check_authentication failed: %s' % (e
[0],))
1102 return self
._processCheckAuthResponse
(response
, server_url
)
1104 def _createCheckAuthRequest(self
, message
):
1105 """Generate a check_authentication request message given an
1108 signed
= message
.getArg(OPENID_NS
, 'signed')
1110 for k
in signed
.split(','):
1112 val
= message
.getAliasedArg(k
)
1114 # Signed value is missing
1116 oidutil
.log('Missing signed field %r' % (k
,))
1119 check_auth_message
= message
.copy()
1120 check_auth_message
.setArg(OPENID_NS
, 'mode', 'check_authentication')
1121 return check_auth_message
1123 def _processCheckAuthResponse(self
, response
, server_url
):
1124 """Process the response message from a check_authentication
1125 request, invalidating associations if requested.
1127 is_valid
= response
.getArg(OPENID_NS
, 'is_valid', 'false')
1129 invalidate_handle
= response
.getArg(OPENID_NS
, 'invalidate_handle')
1130 if invalidate_handle
is not None:
1132 'Received "invalidate_handle" from server %s' % (server_url
,))
1133 if self
.store
is None:
1134 oidutil
.log('Unexpectedly got invalidate_handle without '
1137 self
.store
.removeAssociation(server_url
, invalidate_handle
)
1139 if is_valid
== 'true':
1142 oidutil
.log('Server responds that checkAuth call is not valid')
1145 def _getAssociation(self
, endpoint
):
1146 """Get an association for the endpoint's server_url.
1148 First try seeing if we have a good association in the
1149 store. If we do not, then attempt to negotiate an association
1152 If we negotiate a good association, it will get stored.
1154 @returns: A valid association for the endpoint's server_url or None
1155 @rtype: openid.association.Association or NoneType
1157 assoc
= self
.store
.getAssociation(endpoint
.server_url
)
1159 if assoc
is None or assoc
.expiresIn
<= 0:
1160 assoc
= self
._negotiateAssociation
(endpoint
)
1161 if assoc
is not None:
1162 self
.store
.storeAssociation(endpoint
.server_url
, assoc
)
1166 def _negotiateAssociation(self
, endpoint
):
1167 """Make association requests to the server, attempting to
1168 create a new association.
1170 @returns: a new association object
1172 @rtype: L{openid.association.Association}
1174 # Get our preferred session/association type from the negotiatior.
1175 assoc_type
, session_type
= self
.negotiator
.getAllowedType()
1178 assoc
= self
._requestAssociation
(
1179 endpoint
, assoc_type
, session_type
)
1180 except ServerError
, why
:
1181 supportedTypes
= self
._extractSupportedAssociationType
(why
,
1184 if supportedTypes
is not None:
1185 assoc_type
, session_type
= supportedTypes
1186 # Attempt to create an association from the assoc_type
1187 # and session_type that the server told us it
1190 assoc
= self
._requestAssociation
(
1191 endpoint
, assoc_type
, session_type
)
1192 except ServerError
, why
:
1193 # Do not keep trying, since it rejected the
1194 # association type that it told us to use.
1195 oidutil
.log('Server %s refused its suggested association '
1196 'type: session_type=%s, assoc_type=%s'
1197 % (endpoint
.server_url
, session_type
,
1205 def _extractSupportedAssociationType(self
, server_error
, endpoint
,
1207 """Handle ServerErrors resulting from association requests.
1209 @returns: If server replied with an C{unsupported-type} error,
1210 return a tuple of supported C{association_type}, C{session_type}.
1211 Otherwise logs the error and returns None.
1212 @rtype: tuple or None
1214 # Any error message whose code is not 'unsupported-type'
1215 # should be considered a total failure.
1216 if server_error
.error_code
!= 'unsupported-type' or \
1217 server_error
.message
.isOpenID1():
1219 'Server error when requesting an association from %r: %s'
1220 % (endpoint
.server_url
, server_error
.error_text
))
1223 # The server didn't like the association/session type
1224 # that we sent, and it sent us back a message that
1225 # might tell us how to handle it.
1227 'Unsupported association type %s: %s' % (assoc_type
,
1228 server_error
.error_text
,))
1230 # Extract the session_type and assoc_type from the
1232 assoc_type
= server_error
.message
.getArg(OPENID_NS
, 'assoc_type')
1233 session_type
= server_error
.message
.getArg(OPENID_NS
, 'session_type')
1235 if assoc_type
is None or session_type
is None:
1236 oidutil
.log('Server responded with unsupported association '
1237 'session but did not supply a fallback.')
1239 elif not self
.negotiator
.isAllowed(assoc_type
, session_type
):
1240 fmt
= ('Server sent unsupported session/association type: '
1241 'session_type=%s, assoc_type=%s')
1242 oidutil
.log(fmt
% (session_type
, assoc_type
))
1245 return assoc_type
, session_type
1248 def _requestAssociation(self
, endpoint
, assoc_type
, session_type
):
1249 """Make and process one association request to this endpoint's
1252 @returns: An association object or None if the association
1255 @raises ServerError: when the remote OpenID server returns an error.
1257 assoc_session
, args
= self
._createAssociateRequest
(
1258 endpoint
, assoc_type
, session_type
)
1261 response
= self
._makeKVPost
(args
, endpoint
.server_url
)
1262 except fetchers
.HTTPFetchingError
, why
:
1263 oidutil
.log('openid.associate request failed: %s' % (why
[0],))
1267 assoc
= self
._extractAssociation
(response
, assoc_session
)
1268 except KeyError, why
:
1269 oidutil
.log('Missing required parameter in response from %s: %s'
1270 % (endpoint
.server_url
, why
[0]))
1272 except ProtocolError
, why
:
1273 oidutil
.log('Protocol error parsing response from %s: %s' % (
1274 endpoint
.server_url
, why
[0]))
1279 def _createAssociateRequest(self
, endpoint
, assoc_type
, session_type
):
1280 """Create an association request for the given assoc_type and
1283 @param endpoint: The endpoint whose server_url will be
1284 queried. The important bit about the endpoint is whether
1285 it's in compatiblity mode (OpenID 1.1)
1287 @param assoc_type: The association type that the request
1289 @type assoc_type: str
1291 @param session_type: The session type that should be used in
1292 the association request. The session_type is used to
1293 create an association session object, and that session
1294 object is asked for any additional fields that it needs to
1296 @type session_type: str
1298 @returns: a pair of the association session object and the
1299 request message that will be sent to the server.
1300 @rtype: (association session type (depends on session_type),
1301 openid.message.Message)
1303 session_type_class
= self
.session_types
[session_type
]
1304 assoc_session
= session_type_class()
1307 'mode': 'associate',
1308 'assoc_type': assoc_type
,
1311 if not endpoint
.compatibilityMode():
1312 args
['ns'] = OPENID2_NS
1314 # Leave out the session type if we're in compatibility mode
1315 # *and* it's no-encryption.
1316 if (not endpoint
.compatibilityMode() or
1317 assoc_session
.session_type
!= 'no-encryption'):
1318 args
['session_type'] = assoc_session
.session_type
1320 args
.update(assoc_session
.getRequest())
1321 message
= Message
.fromOpenIDArgs(args
)
1322 return assoc_session
, message
1324 def _getOpenID1SessionType(self
, assoc_response
):
1325 """Given an association response message, extract the OpenID
1328 This function mostly takes care of the 'no-encryption' default
1329 behavior in OpenID 1.
1331 If the association type is plain-text, this function will
1332 return 'no-encryption'
1334 @returns: The association type for this message
1337 @raises KeyError: when the session_type field is absent.
1339 # If it's an OpenID 1 message, allow session_type to default
1340 # to None (which signifies "no-encryption")
1341 session_type
= assoc_response
.getArg(OPENID1_NS
, 'session_type')
1343 # Handle the differences between no-encryption association
1344 # respones in OpenID 1 and 2:
1346 # no-encryption is not really a valid session type for
1347 # OpenID 1, but we'll accept it anyway, while issuing a
1349 if session_type
== 'no-encryption':
1350 oidutil
.log('WARNING: OpenID server sent "no-encryption"'
1353 # Missing or empty session type is the way to flag a
1354 # 'no-encryption' response. Change the session type to
1355 # 'no-encryption' so that it can be handled in the same
1356 # way as OpenID 2 'no-encryption' respones.
1357 elif session_type
== '' or session_type
is None:
1358 session_type
= 'no-encryption'
1362 def _extractAssociation(self
, assoc_response
, assoc_session
):
1363 """Attempt to extract an association from the response, given
1364 the association response message and the established
1365 association session.
1367 @param assoc_response: The association response message from
1369 @type assoc_response: openid.message.Message
1371 @param assoc_session: The association session object that was
1372 used when making the request
1373 @type assoc_session: depends on the session type of the request
1375 @raises ProtocolError: when data is malformed
1376 @raises KeyError: when a field is missing
1378 @rtype: openid.association.Association
1380 # Extract the common fields from the response, raising an
1381 # exception if they are not found
1382 assoc_type
= assoc_response
.getArg(
1383 OPENID_NS
, 'assoc_type', no_default
)
1384 assoc_handle
= assoc_response
.getArg(
1385 OPENID_NS
, 'assoc_handle', no_default
)
1387 # expires_in is a base-10 string. The Python parsing will
1388 # accept literals that have whitespace around them and will
1389 # accept negative values. Neither of these are really in-spec,
1390 # but we think it's OK to accept them.
1391 expires_in_str
= assoc_response
.getArg(
1392 OPENID_NS
, 'expires_in', no_default
)
1394 expires_in
= int(expires_in_str
)
1395 except ValueError, why
:
1396 raise ProtocolError('Invalid expires_in field: %s' % (why
[0],))
1398 # OpenID 1 has funny association session behaviour.
1399 if assoc_response
.isOpenID1():
1400 session_type
= self
._getOpenID
1SessionType
(assoc_response
)
1402 session_type
= assoc_response
.getArg(
1403 OPENID2_NS
, 'session_type', no_default
)
1405 # Session type mismatch
1406 if assoc_session
.session_type
!= session_type
:
1407 if (assoc_response
.isOpenID1() and
1408 session_type
== 'no-encryption'):
1409 # In OpenID 1, any association request can result in a
1410 # 'no-encryption' association response. Setting
1411 # assoc_session to a new no-encryption session should
1412 # make the rest of this function work properly for
1414 assoc_session
= PlainTextConsumerSession()
1416 # Any other mismatch, regardless of protocol version
1417 # results in the failure of the association session
1419 fmt
= 'Session type mismatch. Expected %r, got %r'
1420 message
= fmt
% (assoc_session
.session_type
, session_type
)
1421 raise ProtocolError(message
)
1423 # Make sure assoc_type is valid for session_type
1424 if assoc_type
not in assoc_session
.allowed_assoc_types
:
1425 fmt
= 'Unsupported assoc_type for session %s returned: %s'
1426 raise ProtocolError(fmt
% (assoc_session
.session_type
, assoc_type
))
1428 # Delegate to the association session to extract the secret
1429 # from the response, however is appropriate for that session
1432 secret
= assoc_session
.extractSecret(assoc_response
)
1433 except ValueError, why
:
1434 fmt
= 'Malformed response for %s session: %s'
1435 raise ProtocolError(fmt
% (assoc_session
.session_type
, why
[0]))
1437 return Association
.fromExpiresIn(
1438 expires_in
, assoc_handle
, secret
, assoc_type
)
1440 class AuthRequest(object):
1441 """An object that holds the state necessary for generating an
1442 OpenID authentication request. This object holds the association
1443 with the server and the discovered information with which the
1444 request will be made.
1446 It is separate from the consumer because you may wish to add
1447 things to the request before sending it on its way to the
1448 server. It also has serialization options that let you encode the
1449 authentication request as a URL or as a form POST.
1452 def __init__(self
, endpoint
, assoc
):
1454 Creates a new AuthRequest object. This just stores each
1455 argument in an appropriately named field.
1457 Users of this library should not create instances of this
1458 class. Instances of this class are created by the library
1462 self
.endpoint
= endpoint
1463 self
.return_to_args
= {}
1464 self
.message
= Message(endpoint
.preferredNamespace())
1465 self
._anonymous
= False
1467 def setAnonymous(self
, is_anonymous
):
1468 """Set whether this request should be made anonymously. If a
1469 request is anonymous, the identifier will not be sent in the
1470 request. This is only useful if you are making another kind of
1471 request with an extension in this request.
1473 Anonymous requests are not allowed when the request is made
1476 @raises ValueError: when attempting to set an OpenID1 request
1479 if is_anonymous
and self
.message
.isOpenID1():
1480 raise ValueError('OpenID 1 requests MUST include the '
1481 'identifier in the request')
1483 self
._anonymous
= is_anonymous
1485 def addExtension(self
, extension_request
):
1486 """Add an extension to this checkid request.
1488 @param extension_request: An object that implements the
1489 extension interface for adding arguments to an OpenID
1492 extension_request
.toMessage(self
.message
)
1494 def addExtensionArg(self
, namespace
, key
, value
):
1495 """Add an extension argument to this OpenID authentication
1498 Use caution when adding arguments, because they will be
1499 URL-escaped and appended to the redirect URL, which can easily
1502 @param namespace: The namespace for the extension. For
1503 example, the simple registration extension uses the
1506 @type namespace: str
1508 @param key: The key within the extension namespace. For
1509 example, the nickname field in the simple registration
1510 extension's key is C{nickname}.
1514 @param value: The value to provide to the server for this
1519 self
.message
.setArg(namespace
, key
, value
)
1521 def getMessage(self
, realm
, return_to
=None, immediate
=False):
1522 """Produce a L{openid.message.Message} representing this request.
1524 @param realm: The URL (or URL pattern) that identifies your
1525 web site to the user when she is authorizing it.
1529 @param return_to: The URL that the OpenID provider will send the
1530 user back to after attempting to verify her identity.
1532 Not specifying a return_to URL means that the user will not
1533 be returned to the site issuing the request upon its
1536 @type return_to: str
1538 @param immediate: If True, the OpenID provider is to send back
1539 a response immediately, useful for behind-the-scenes
1540 authentication attempts. Otherwise the OpenID provider
1541 may engage the user before providing a response. This is
1542 the default case, as the user may need to provide
1543 credentials or approve the request before a positive
1544 response can be sent.
1546 @type immediate: bool
1548 @returntype: L{openid.message.Message}
1551 return_to
= oidutil
.appendArgs(return_to
, self
.return_to_args
)
1554 '"return_to" is mandatory when using "checkid_immediate"')
1555 elif self
.message
.isOpenID1():
1556 raise ValueError('"return_to" is mandatory for OpenID 1 requests')
1557 elif self
.return_to_args
:
1558 raise ValueError('extra "return_to" arguments were specified, '
1559 'but no return_to was specified')
1562 mode
= 'checkid_immediate'
1564 mode
= 'checkid_setup'
1566 message
= self
.message
.copy()
1567 if message
.isOpenID1():
1568 realm_key
= 'trust_root'
1572 message
.updateArgs(OPENID_NS
,
1576 'return_to':return_to
,
1579 if not self
._anonymous
:
1580 if self
.endpoint
.isOPIdentifier():
1581 # This will never happen when we're in compatibility
1582 # mode, as long as isOPIdentifier() returns False
1583 # whenever preferredNamespace() returns OPENID1_NS.
1584 claimed_id
= request_identity
= IDENTIFIER_SELECT
1586 request_identity
= self
.endpoint
.getLocalID()
1587 claimed_id
= self
.endpoint
.claimed_id
1589 # This is true for both OpenID 1 and 2
1590 message
.setArg(OPENID_NS
, 'identity', request_identity
)
1592 if message
.isOpenID2():
1593 message
.setArg(OPENID2_NS
, 'claimed_id', claimed_id
)
1596 message
.setArg(OPENID_NS
, 'assoc_handle', self
.assoc
.handle
)
1597 assoc_log_msg
= 'with assocication %s' % (self
.assoc
.handle
,)
1599 assoc_log_msg
= 'using stateless mode.'
1601 oidutil
.log("Generated %s request to %s %s" %
1602 (mode
, self
.endpoint
.server_url
, assoc_log_msg
))
1606 def redirectURL(self
, realm
, return_to
=None, immediate
=False):
1607 """Returns a URL with an encoded OpenID request.
1609 The resulting URL is the OpenID provider's endpoint URL with
1610 parameters appended as query arguments. You should redirect
1611 the user agent to this URL.
1613 OpenID 2.0 endpoints also accept POST requests, see
1614 C{L{shouldSendRedirect}} and C{L{formMarkup}}.
1616 @param realm: The URL (or URL pattern) that identifies your
1617 web site to the user when she is authorizing it.
1621 @param return_to: The URL that the OpenID provider will send the
1622 user back to after attempting to verify her identity.
1624 Not specifying a return_to URL means that the user will not
1625 be returned to the site issuing the request upon its
1628 @type return_to: str
1630 @param immediate: If True, the OpenID provider is to send back
1631 a response immediately, useful for behind-the-scenes
1632 authentication attempts. Otherwise the OpenID provider
1633 may engage the user before providing a response. This is
1634 the default case, as the user may need to provide
1635 credentials or approve the request before a positive
1636 response can be sent.
1638 @type immediate: bool
1640 @returns: The URL to redirect the user agent to.
1644 message
= self
.getMessage(realm
, return_to
, immediate
)
1645 return message
.toURL(self
.endpoint
.server_url
)
1647 def formMarkup(self
, realm
, return_to
=None, immediate
=False,
1648 form_tag_attrs
=None):
1649 """Get html for a form to submit this request to the IDP.
1651 @param form_tag_attrs: Dictionary of attributes to be added to
1652 the form tag. 'accept-charset' and 'enctype' have defaults
1653 that can be overridden. If a value is supplied for
1654 'action' or 'method', it will be replaced.
1655 @type form_tag_attrs: {unicode: unicode}
1657 message
= self
.getMessage(realm
, return_to
, immediate
)
1658 return message
.toFormMarkup(self
.endpoint
.server_url
,
1661 def htmlMarkup(self
, realm
, return_to
=None, immediate
=False,
1662 form_tag_attrs
=None):
1663 """Get an autosubmitting HTML page that submits this request to the
1664 IDP. This is just a wrapper for formMarkup.
1670 return oidutil
.autoSubmitHTML(self
.formMarkup(realm
,
1675 def shouldSendRedirect(self
):
1676 """Should this OpenID authentication request be sent as a HTTP
1677 redirect or as a POST (form submission)?
1681 return self
.endpoint
.compatibilityMode()
1686 SETUP_NEEDED
= 'setup_needed'
1688 class Response(object):
1691 def setEndpoint(self
, endpoint
):
1692 self
.endpoint
= endpoint
1693 if endpoint
is None:
1694 self
.identity_url
= None
1696 self
.identity_url
= endpoint
.claimed_id
1698 def getDisplayIdentifier(self
):
1699 """Return the display identifier for this response.
1701 The display identifier is related to the Claimed Identifier, but the
1702 two are not always identical. The display identifier is something the
1703 user should recognize as what they entered, whereas the response's
1704 claimed identifier (in the L{identity_url} attribute) may have extra
1705 information for better persistence.
1707 URLs will be stripped of their fragments for display. XRIs will
1708 display the human-readable identifier (i-name) instead of the
1709 persistent identifier (i-number).
1711 Use the display identifier in your user interface. Use
1712 L{identity_url} for querying your database or authorization server.
1714 if self
.endpoint
is not None:
1715 return self
.endpoint
.getDisplayIdentifier()
1718 class SuccessResponse(Response
):
1719 """A response with a status of SUCCESS. Indicates that this request is a
1720 successful acknowledgement from the OpenID server that the
1721 supplied URL is, indeed controlled by the requesting agent.
1723 @ivar identity_url: The identity URL that has been authenticated; the Claimed Identifier.
1724 See also L{getDisplayIdentifier}.
1726 @ivar endpoint: The endpoint that authenticated the identifier. You
1727 may access other discovered information related to this endpoint,
1728 such as the CanonicalID of an XRI, through this object.
1729 @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
1731 @ivar signed_fields: The arguments in the server's response that
1732 were signed and verified.
1734 @cvar status: SUCCESS
1739 def __init__(self
, endpoint
, message
, signed_fields
=None):
1740 # Don't use setEndpoint, because endpoint should never be None
1741 # for a successfull transaction.
1742 self
.endpoint
= endpoint
1743 self
.identity_url
= endpoint
.claimed_id
1745 self
.message
= message
1747 if signed_fields
is None:
1749 self
.signed_fields
= signed_fields
1751 def isOpenID1(self
):
1752 """Was this authentication response an OpenID 1 authentication
1755 return self
.message
.isOpenID1()
1757 def isSigned(self
, ns_uri
, ns_key
):
1758 """Return whether a particular key is signed, regardless of
1761 return self
.message
.getKey(ns_uri
, ns_key
) in self
.signed_fields
1763 def getSigned(self
, ns_uri
, ns_key
, default
=None):
1764 """Return the specified signed field if available,
1765 otherwise return default
1767 if self
.isSigned(ns_uri
, ns_key
):
1768 return self
.message
.getArg(ns_uri
, ns_key
, default
)
1772 def getSignedNS(self
, ns_uri
):
1773 """Get signed arguments from the response message. Return a
1774 dict of all arguments in the specified namespace. If any of
1775 the arguments are not signed, return None.
1777 msg_args
= self
.message
.getArgs(ns_uri
)
1779 for key
in msg_args
.iterkeys():
1780 if not self
.isSigned(ns_uri
, key
):
1781 oidutil
.log("SuccessResponse.getSignedNS: (%s, %s) not signed."
1787 def extensionResponse(self
, namespace_uri
, require_signed
):
1788 """Return response arguments in the specified namespace.
1790 @param namespace_uri: The namespace URI of the arguments to be
1793 @param require_signed: True if the arguments should be among
1794 those signed in the response, False if you don't care.
1796 If require_signed is True and the arguments are not signed,
1800 return self
.getSignedNS(namespace_uri
)
1802 return self
.message
.getArgs(namespace_uri
)
1804 def getReturnTo(self
):
1805 """Get the openid.return_to argument from this response.
1807 This is useful for verifying that this request was initiated
1810 @returns: The return_to URL supplied to the server on the
1811 initial request, or C{None} if the response did not contain
1812 an C{openid.return_to} argument.
1816 return self
.getSigned(OPENID_NS
, 'return_to')
1818 def __eq__(self
, other
):
1820 (self
.endpoint
== other
.endpoint
) and
1821 (self
.identity_url
== other
.identity_url
) and
1822 (self
.message
== other
.message
) and
1823 (self
.signed_fields
== other
.signed_fields
) and
1824 (self
.status
== other
.status
))
1826 def __ne__(self
, other
):
1827 return not (self
== other
)
1830 return '<%s.%s id=%r signed=%r>' % (
1831 self
.__class
__.__module
__,
1832 self
.__class
__.__name
__,
1833 self
.identity_url
, self
.signed_fields
)
1836 class FailureResponse(Response
):
1837 """A response with a status of FAILURE. Indicates that the OpenID
1838 protocol has failed. This could be locally or remotely triggered.
1840 @ivar identity_url: The identity URL for which authenitcation was
1841 attempted, if it can be determined. Otherwise, None.
1843 @ivar message: A message indicating why the request failed, if one
1844 is supplied. otherwise, None.
1846 @cvar status: FAILURE
1851 def __init__(self
, endpoint
, message
=None, contact
=None,
1853 self
.setEndpoint(endpoint
)
1854 self
.message
= message
1855 self
.contact
= contact
1856 self
.reference
= reference
1859 return "<%s.%s id=%r message=%r>" % (
1860 self
.__class
__.__module
__, self
.__class
__.__name
__,
1861 self
.identity_url
, self
.message
)
1864 class CancelResponse(Response
):
1865 """A response with a status of CANCEL. Indicates that the user
1866 cancelled the OpenID authentication request.
1868 @ivar identity_url: The identity URL for which authenitcation was
1869 attempted, if it can be determined. Otherwise, None.
1871 @cvar status: CANCEL
1876 def __init__(self
, endpoint
):
1877 self
.setEndpoint(endpoint
)
1879 class SetupNeededResponse(Response
):
1880 """A response with a status of SETUP_NEEDED. Indicates that the
1881 request was in immediate mode, and the server is unable to
1882 authenticate the user without further interaction.
1884 @ivar identity_url: The identity URL for which authenitcation was
1887 @ivar setup_url: A URL that can be used to send the user to the
1888 server to set up for authentication. The user should be
1889 redirected in to the setup_url, either in the current window
1890 or in a new browser window. C{None} in OpenID 2.0.
1892 @cvar status: SETUP_NEEDED
1895 status
= SETUP_NEEDED
1897 def __init__(self
, endpoint
, setup_url
=None):
1898 self
.setEndpoint(endpoint
)
1899 self
.setup_url
= setup_url