Package openid :: Package consumer :: Module consumer
[frames] | no frames]

Source Code for Module openid.consumer.consumer

   1  # -*- test-case-name: openid.test.test_consumer -*- 
   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 # XXX: TESTME 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
239 -class Consumer(object):
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
335 - def beginWithoutDiscovery(self, service, anonymous=False):
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 # This is OK to do even if we did not do discovery in 406 # the first place. 407 disco.cleanup() 408 409 return response
410
411 - def setAssociationPreference(self, association_preferences):
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
433 -class DiffieHellmanSHA1ConsumerSession(object):
434 session_type = 'DH-SHA1' 435 hash_func = staticmethod(cryptutil.sha1) 436 secret_size = 20 437 allowed_assoc_types = ['HMAC-SHA1'] 438
439 - def __init__(self, dh=None):
440 if dh is None: 441 dh = DiffieHellman.fromDefaults() 442 443 self.dh = dh
444
445 - def getRequest(self):
446 cpub = cryptutil.longToBase64(self.dh.public) 447 448 args = {'dh_consumer_public': cpub} 449 450 if not self.dh.usingDefaultValues(): 451 args.update({ 452 'dh_modulus': cryptutil.longToBase64(self.dh.modulus), 453 'dh_gen': cryptutil.longToBase64(self.dh.generator), 454 }) 455 456 return args
457
458 - def extractSecret(self, response):
459 dh_server_public64 = response.getArg( 460 OPENID_NS, 'dh_server_public', no_default) 461 enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default) 462 dh_server_public = cryptutil.base64ToLong(dh_server_public64) 463 enc_mac_key = oidutil.fromBase64(enc_mac_key64) 464 return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
465
466 -class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
467 session_type = 'DH-SHA256' 468 hash_func = staticmethod(cryptutil.sha256) 469 secret_size = 32 470 allowed_assoc_types = ['HMAC-SHA256']
471
472 -class PlainTextConsumerSession(object):
473 session_type = 'no-encryption' 474 allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] 475
476 - def getRequest(self):
477 return {}
478
479 - def extractSecret(self, response):
480 mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default) 481 return oidutil.fromBase64(mac_key64)
482
483 -class SetupNeededError(Exception):
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
490 -class ProtocolError(ValueError):
491 """Exception that indicates that a message violated the 492 protocol. It is raised and caught internally to this file."""
493
494 -class TypeURIMismatch(ProtocolError):
495 """A protocol error arising from type URIs mismatching 496 """
497
498 -class ServerError(Exception):
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
508 - def fromMessage(cls, message):
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
518 -class GenericConsumer(object):
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 # The name of the query parameter that gets added to the return_to 532 # URL when using OpenID1. You can change this value if you want or 533 # need a different name, but don't make it start with openid, 534 # because it's not a standard protocol thing for OpenID1. For 535 # OpenID2, the library will take care of the nonce using standard 536 # OpenID query parameter names. 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
547 - def __init__(self, store):
548 self.store = store 549 self.negotiator = default_negotiator.copy()
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
602 - def _checkReturnTo(self, message, return_to):
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 # Check the openid.return_to args against args in the original 608 # message. 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 # Check the return_to base URL against the one in the message. 616 msg_return_to = message.getArg(OPENID_NS, 'return_to') 617 618 # The URL scheme, authority, and path MUST be the same between 619 # the two URLs. 620 app_parts = urlparse(return_to) 621 msg_parts = urlparse(msg_return_to) 622 623 # (addressing scheme, network location, path) must be equal in 624 # both URLs. 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
633 - def _checkSetupNeeded(self, message):
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 # In OpenID 1, we check to see if this is a cancel from 640 # immediate mode by the presence of the user_setup_url 641 # parameter. 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
647 - def _doIdRes(self, message, endpoint):
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 # Checks for presence of appropriate fields (and checks 673 # signed list fields) 674 self._idResCheckForFields(message, signed_list) 675 676 # Verify discovery information: 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 # Will raise a ProtocolError if the nonce is bad 684 self._idResCheckNonce(message, endpoint) 685 686 signed_fields = ["openid." + s for s in signed_list] 687 return SuccessResponse(endpoint, message, signed_fields)
688
689 - def _idResGetNonceOpenID1(self, message, endpoint):
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
708 - def _idResCheckNonce(self, message, endpoint):
709 if message.isOpenID1(): 710 # This indicates that the nonce was generated by the consumer 711 nonce = self._idResGetNonceOpenID1(message, endpoint) 712 server_url = '' 713 else: 714 nonce = message.getArg(OPENID2_NS, 'response_nonce') 715 server_url = endpoint.server_url 716 717 if nonce is None: 718 raise ProtocolError('Nonce missing from response') 719 720 try: 721 timestamp, salt = splitNonce(nonce) 722 except ValueError, why: 723 raise ProtocolError('Malformed nonce: %s' % (why[0],)) 724 725 if (self.store is not None and 726 not self.store.useNonce(server_url, timestamp, salt)): 727 raise ProtocolError('Nonce already used or out of range')
728
729 - def _idResCheckSignature(self, message, server_url):
730 assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') 731 if self.store is None: 732 assoc = None 733 else: 734 assoc = self.store.getAssociation(server_url, assoc_handle) 735 736 if assoc: 737 if assoc.getExpiresIn() <= 0: 738 # XXX: It might be a good idea sometimes to re-start the 739 # authentication with a new association. Doing it 740 # automatically opens the possibility for 741 # denial-of-service by a server that just returns expired 742 # associations (or really short-lived associations) 743 raise ProtocolError( 744 'Association with %s expired' % (server_url,)) 745 746 if not assoc.checkMessageSignature(message): 747 raise ProtocolError('Bad signature') 748 749 else: 750 # It's not an association we know about. Stateless mode is our 751 # only possible path for recovery. 752 # XXX - async framework will not want to block on this call to 753 # _checkAuth. 754 if not self._checkAuth(message, server_url): 755 raise ProtocolError('Server denied check_authentication')
756
757 - def _idResCheckForFields(self, message, signed_list):
758 # XXX: this should be handled by the code that processes the 759 # response (that is, if a field is missing, we should not have 760 # to explicitly check that it's present, just make sure that 761 # the fields are actually being used by the rest of the code 762 # in tests). Although, which fields are signed does need to be 763 # checked somewhere. 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 # Field is present and not in signed list 785 if message.hasKey(OPENID_NS, field) and field not in signed_list: 786 raise ProtocolError('"%s" not signed' % (field,))
787 788
789 - def _verifyReturnToArgs(query):
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 # XXX: this should be checked by _idResCheckForFields 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
814 - def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
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
828 - def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
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 # Raises a KeyError when the op_endpoint is not present 835 to_match.server_url = resp_msg.getArg( 836 OPENID2_NS, 'op_endpoint', no_default) 837 838 # claimed_id and identifier must both be present or both 839 # be absent 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 # This is a response without identifiers, so there's really no 851 # checking that we can do, so return an endpoint that's for 852 # the specified `openid.op_endpoint' 853 elif to_match.claimed_id is None: 854 return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url) 855 856 # The claimed ID doesn't match, so we have to do discovery 857 # again. This covers not using sessions, OP identifier 858 # endpoints and responses that didn't match the original 859 # request. 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 # The claimed ID matches, so we use the endpoint that we 871 # discovered in initiation. This should be the most common 872 # case. 873 else: 874 self._verifyDiscoverySingle(endpoint, to_match) 875 return endpoint
876
877 - def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
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 # Restore delegate information from the initiation phase 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
904 - def _verifyDiscoverySingle(self, endpoint, to_match):
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 # Every type URI that's in the to_match endpoint has to be 918 # present in the discovered endpoint. 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 # If the server URL is None, this must be an OpenID 1 935 # response, because op_endpoint is a required parameter in 936 # OpenID 2. In that case, we don't actually care what the 937 # discovered server_url is, because signature checking or 938 # check_auth should take care of that check for us. 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
950 - def _discoverAndVerify(self, to_match):
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 # Search the services resulting from discovery to find one 972 # that matches the information from the assertion 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 # It matches, so discover verification has 981 # succeeded. Return this endpoint. 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
993 - def _checkAuth(self, message, server_url):
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
1006 - def _createCheckAuthRequest(self, message):
1007 """Generate a check_authentication request message given an 1008 id_res message. 1009 """ 1010 # Arguments that are always passed to the server and not 1011 # included in the signature. 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 # Signed value is missing 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
1039 - def _processCheckAuthResponse(self, response, server_url):
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
1061 - def _getAssociation(self, endpoint):
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
1082 - def _negotiateAssociation(self, endpoint):
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 # Get our preferred session/association type from the negotiatior. 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 # Any error message whose code is not 'unsupported-type' 1101 # should be considered a total failure. 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 # The server didn't like the association/session type 1110 # that we sent, and it sent us back a message that 1111 # might tell us how to handle it. 1112 oidutil.log( 1113 'Unsupported association type %s: %s' % (assoc_type, 1114 why.error_text,)) 1115 1116 # Extract the session_type and assoc_type from the 1117 # error message 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 # Attempt to create an association from the assoc_type 1132 # and session_type that the server told us it 1133 # supported. 1134 try: 1135 assoc = self._requestAssociation( 1136 endpoint, assoc_type, session_type) 1137 except ServerError, why: 1138 # Do not keep trying, since it rejected the 1139 # association type that it told us to use. 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
1150 - def _requestAssociation(self, endpoint, assoc_type, session_type):
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
1181 - def _createAssociateRequest(self, endpoint, assoc_type, session_type):
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 # Leave out the session type if we're in compatibility mode 1217 # *and* it's no-encryption. 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
1226 - def _getOpenID1SessionType(self, assoc_response):
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 # If it's an OpenID 1 message, allow session_type to default 1242 # to None (which signifies "no-encryption") 1243 session_type = assoc_response.getArg(OPENID1_NS, 'session_type') 1244 1245 # Handle the differences between no-encryption association 1246 # respones in OpenID 1 and 2: 1247 1248 # no-encryption is not really a valid session type for 1249 # OpenID 1, but we'll accept it anyway, while issuing a 1250 # warning. 1251 if session_type == 'no-encryption': 1252 oidutil.log('WARNING: OpenID server sent "no-encryption"' 1253 'for OpenID 1.X') 1254 1255 # Missing or empty session type is the way to flag a 1256 # 'no-encryption' response. Change the session type to 1257 # 'no-encryption' so that it can be handled in the same 1258 # way as OpenID 2 'no-encryption' respones. 1259 elif session_type == '' or session_type is None: 1260 session_type = 'no-encryption' 1261 1262 return session_type
1263
1264 - def _extractAssociation(self, assoc_response, assoc_session):
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 # Extract the common fields from the response, raising an 1283 # exception if they are not found 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 # expires_in is a base-10 string. The Python parsing will 1290 # accept literals that have whitespace around them and will 1291 # accept negative values. Neither of these are really in-spec, 1292 # but we think it's OK to accept them. 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 # OpenID 1 has funny association session behaviour. 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 # Session type mismatch 1308 if assoc_session.session_type != session_type: 1309 if (assoc_response.isOpenID1() and 1310 session_type == 'no-encryption'): 1311 # In OpenID 1, any association request can result in a 1312 # 'no-encryption' association response. Setting 1313 # assoc_session to a new no-encryption session should 1314 # make the rest of this function work properly for 1315 # that case. 1316 assoc_session = PlainTextConsumerSession() 1317 else: 1318 # Any other mismatch, regardless of protocol version 1319 # results in the failure of the association session 1320 # altogether. 1321 fmt = 'Session type mismatch. Expected %r, got %r' 1322 message = fmt % (assoc_session.session_type, session_type) 1323 raise ProtocolError(message) 1324 1325 # Make sure assoc_type is valid for session_type 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 # Delegate to the association session to extract the secret 1331 # from the response, however is appropriate for that session 1332 # type. 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
1342 -class AuthRequest(object):
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
1354 - def __init__(self, endpoint, assoc):
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
1370 - def setAnonymous(self, is_anonymous):
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
1388 - def addExtension(self, extension_request):
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
1397 - def addExtensionArg(self, namespace, key, value):
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 # This will never happen when we're in compatibility 1485 # mode, as long as isOPIdentifier() returns False 1486 # whenever preferredNamespace() returns OPENID1_NS. 1487 claimed_id = request_identity = IDENTIFIER_SELECT 1488 else: 1489 request_identity = self.endpoint.getLocalID() 1490 claimed_id = self.endpoint.claimed_id 1491 1492 # This is true for both OpenID 1 and 2 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
1544 - def formMarkup(self, realm, return_to=None, immediate=False, 1545 form_tag_attrs=None):
1546 """Get html for a form to submit this request to the IDP. 1547 1548 @param form_tag_attrs: Dictionary of attributes to be added to 1549 the form tag. 'accept-charset' and 'enctype' have defaults 1550 that can be overridden. If a value is supplied for 1551 'action' or 'method', it will be replaced. 1552 @type form_tag_attrs: {unicode: unicode} 1553 """ 1554 message = self.getMessage(realm, return_to, immediate) 1555 return message.toFormMarkup(self.endpoint.server_url, 1556 form_tag_attrs)
1557
1558 - def shouldSendRedirect(self):
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
1571 -class Response(object):
1572 status = None 1573
1574 - def setEndpoint(self, endpoint):
1575 self.endpoint = endpoint 1576 if endpoint is None: 1577 self.identity_url = None 1578 else: 1579 self.identity_url = endpoint.claimed_id
1580
1581 -class SuccessResponse(Response):
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 # Don't use setEndpoint, because endpoint should never be None 1603 # for a successfull transaction. 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
1613 - def isOpenID1(self):
1614 """Was this authentication response an OpenID 1 authentication 1615 response? 1616 """ 1617 return self.message.isOpenID1()
1618
1619 - def isSigned(self, ns_uri, ns_key):
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
1634 - def getSignedNS(self, ns_uri):
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
1647 - def extensionResponse(self, namespace_uri, require_signed):
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
1664 - def getReturnTo(self):
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
1680 -class FailureResponse(Response):
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
1702 - def __repr__(self):
1703 return "<%s.%s id=%r message=%r>" % ( 1704 self.__class__.__module__, self.__class__.__name__, 1705 self.identity_url, self.message)
1706 1707
1708 -class CancelResponse(Response):
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
1720 - def __init__(self, endpoint):
1721 self.setEndpoint(endpoint)
1722
1723 -class SetupNeededResponse(Response):
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