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 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 a 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 whether or not the login was successful, 
 174      and include any additional information appropriate for their type. 
 175   
 176  @var SUCCESS: constant used as the status for 
 177      L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects. 
 178   
 179  @var FAILURE: constant used as the status for 
 180      L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects. 
 181   
 182  @var CANCEL: constant used as the status for 
 183      L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects. 
 184   
 185  @var SETUP_NEEDED: constant used as the status for 
 186      L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>} 
 187      objects. 
 188  """ 
 189   
 190  import cgi 
 191  import copy 
 192  from urlparse import urlparse, urldefrag 
 193   
 194  from openid import fetchers 
 195   
 196  from openid.consumer.discover import discover, OpenIDServiceEndpoint, \ 
 197       DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE 
 198  from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \ 
 199       IDENTIFIER_SELECT, no_default, BARE_NS 
 200  from openid import cryptutil 
 201  from openid import oidutil 
 202  from openid.association import Association, default_negotiator, \ 
 203       SessionNegotiator 
 204  from openid.dh import DiffieHellman 
 205  from openid.store.nonce import mkNonce, split as splitNonce 
 206  from openid.yadis.manager import Discovery 
 207  from openid import urinorm 
 208   
 209   
 210  __all__ = ['AuthRequest', 'Consumer', 'SuccessResponse', 
 211             'SetupNeededResponse', 'CancelResponse', 'FailureResponse', 
 212             'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED', 
 213             ] 
 214   
 215   
216 -def makeKVPost(request_message, server_url):
217 """Make a Direct Request to an OpenID Provider and return the 218 result as a Message object. 219 220 @raises openid.fetchers.HTTPFetchingError: if an error is 221 encountered in making the HTTP post. 222 223 @rtype: L{openid.message.Message} 224 """ 225 # XXX: TESTME 226 resp = fetchers.fetch(server_url, body=request_message.toURLEncoded()) 227 228 # Process response in separate function that can be shared by async code. 229 return _httpResponseToMessage(resp, server_url)
230 231
232 -def _httpResponseToMessage(response, server_url):
233 """Adapt a POST response to a Message. 234 235 @type response: L{openid.fetchers.HTTPResponse} 236 @param response: Result of a POST to an OpenID endpoint. 237 238 @rtype: L{openid.message.Message} 239 240 @raises openid.fetchers.HTTPFetchingError: if the server returned a 241 status of other than 200 or 400. 242 243 @raises ServerError: if the server returned an OpenID error. 244 """ 245 # Should this function be named Message.fromHTTPResponse instead? 246 response_message = Message.fromKVForm(response.body) 247 if response.status == 400: 248 raise ServerError.fromMessage(response_message) 249 250 elif response.status not in (200, 206): 251 fmt = 'bad status code from server %s: %s' 252 error_message = fmt % (server_url, response.status) 253 raise fetchers.HTTPFetchingError(error_message) 254 255 return response_message
256 257 258
259 -class Consumer(object):
260 """An OpenID consumer implementation that performs discovery and 261 does session management. 262 263 @ivar consumer: an instance of an object implementing the OpenID 264 protocol, but doing no discovery or session management. 265 266 @type consumer: GenericConsumer 267 268 @ivar session: A dictionary-like object representing the user's 269 session data. This is used for keeping state of the OpenID 270 transaction when the user is redirected to the server. 271 272 @cvar session_key_prefix: A string that is prepended to session 273 keys to ensure that they are unique. This variable may be 274 changed to suit your application. 275 """ 276 session_key_prefix = "_openid_consumer_" 277 278 _token = 'last_token' 279 280 _discover = staticmethod(discover) 281
282 - def __init__(self, session, store, consumer_class=None):
283 """Initialize a Consumer instance. 284 285 You should create a new instance of the Consumer object with 286 every HTTP request that handles OpenID transactions. 287 288 @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>} 289 290 @param store: an object that implements the interface in 291 C{L{openid.store.interface.OpenIDStore}}. Several 292 implementations are provided, to cover common database 293 environments. 294 295 @type store: C{L{openid.store.interface.OpenIDStore}} 296 297 @see: L{openid.store.interface} 298 @see: L{openid.store} 299 """ 300 self.session = session 301 if consumer_class is None: 302 consumer_class = GenericConsumer 303 self.consumer = consumer_class(store) 304 self._token_key = self.session_key_prefix + self._token
305
306 - def begin(self, user_url, anonymous=False):
307 """Start the OpenID authentication process. See steps 1-2 in 308 the overview at the top of this file. 309 310 @param user_url: Identity URL given by the user. This method 311 performs a textual transformation of the URL to try and 312 make sure it is normalized. For example, a user_url of 313 example.com will be normalized to http://example.com/ 314 normalizing and resolving any redirects the server might 315 issue. 316 317 @type user_url: unicode 318 319 @param anonymous: Whether to make an anonymous request of the OpenID 320 provider. Such a request does not ask for an authorization 321 assertion for an OpenID identifier, but may be used with 322 extensions to pass other data. e.g. "I don't care who you are, 323 but I'd like to know your time zone." 324 325 @type anonymous: bool 326 327 @returns: An object containing the discovered information will 328 be returned, with a method for building a redirect URL to 329 the server, as described in step 3 of the overview. This 330 object may also be used to add extension arguments to the 331 request, using its 332 L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>} 333 method. 334 335 @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>} 336 337 @raises openid.consumer.discover.DiscoveryFailure: when I fail to 338 find an OpenID server for this URL. If the C{yadis} package 339 is available, L{openid.consumer.discover.DiscoveryFailure} is 340 an alias for C{yadis.discover.DiscoveryFailure}. 341 """ 342 disco = Discovery(self.session, user_url, self.session_key_prefix) 343 try: 344 service = disco.getNextService(self._discover) 345 except fetchers.HTTPFetchingError, why: 346 raise DiscoveryFailure( 347 'Error fetching XRDS document: %s' % (why[0],), None) 348 349 if service is None: 350 raise DiscoveryFailure( 351 'No usable OpenID services found for %s' % (user_url,), None) 352 else: 353 return self.beginWithoutDiscovery(service, anonymous)
354
355 - def beginWithoutDiscovery(self, service, anonymous=False):
356 """Start OpenID verification without doing OpenID server 357 discovery. This method is used internally by Consumer.begin 358 after discovery is performed, and exists to provide an 359 interface for library users needing to perform their own 360 discovery. 361 362 @param service: an OpenID service endpoint descriptor. This 363 object and factories for it are found in the 364 L{openid.consumer.discover} module. 365 366 @type service: 367 L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>} 368 369 @returns: an OpenID authentication request object. 370 371 @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>} 372 373 @See: Openid.consumer.consumer.Consumer.begin 374 @see: openid.consumer.discover 375 """ 376 auth_req = self.consumer.begin(service) 377 self.session[self._token_key] = auth_req.endpoint 378 379 try: 380 auth_req.setAnonymous(anonymous) 381 except ValueError, why: 382 raise ProtocolError(str(why)) 383 384 return auth_req
385
386 - def complete(self, query, current_url):
387 """Called to interpret the server's response to an OpenID 388 request. It is called in step 4 of the flow described in the 389 consumer overview. 390 391 @param query: A dictionary of the query parameters for this 392 HTTP request. 393 394 @param current_url: The URL used to invoke the application. 395 Extract the URL from your application's web 396 request framework and specify it here to have it checked 397 against the openid.return_to value in the response. If 398 the return_to URL check fails, the status of the 399 completion will be FAILURE. 400 401 @returns: a subclass of Response. The type of response is 402 indicated by the status attribute, which will be one of 403 SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. 404 405 @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} 406 @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>} 407 @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>} 408 @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>} 409 """ 410 411 endpoint = self.session.get(self._token_key) 412 413 message = Message.fromPostArgs(query) 414 response = self.consumer.complete(message, endpoint, current_url) 415 416 try: 417 del self.session[self._token_key] 418 except KeyError: 419 pass 420 421 if (response.status in ['success', 'cancel'] and 422 response.identity_url is not None): 423 424 disco = Discovery(self.session, 425 response.identity_url, 426 self.session_key_prefix) 427 # This is OK to do even if we did not do discovery in 428 # the first place. 429 disco.cleanup(force=True) 430 431 return response
432
433 - def setAssociationPreference(self, association_preferences):
434 """Set the order in which association types/sessions should be 435 attempted. For instance, to only allow HMAC-SHA256 436 associations created with a DH-SHA256 association session: 437 438 >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')]) 439 440 Any association type/association type pair that is not in this 441 list will not be attempted at all. 442 443 @param association_preferences: The list of allowed 444 (association type, association session type) pairs that 445 should be allowed for this consumer to use, in order from 446 most preferred to least preferred. 447 @type association_preferences: [(str, str)] 448 449 @returns: None 450 451 @see: C{L{openid.association.SessionNegotiator}} 452 """ 453 self.consumer.negotiator = SessionNegotiator(association_preferences)
454
455 -class DiffieHellmanSHA1ConsumerSession(object):
456 session_type = 'DH-SHA1' 457 hash_func = staticmethod(cryptutil.sha1) 458 secret_size = 20 459 allowed_assoc_types = ['HMAC-SHA1'] 460
461 - def __init__(self, dh=None):
462 if dh is None: 463 dh = DiffieHellman.fromDefaults() 464 465 self.dh = dh
466
467 - def getRequest(self):
468 cpub = cryptutil.longToBase64(self.dh.public) 469 470 args = {'dh_consumer_public': cpub} 471 472 if not self.dh.usingDefaultValues(): 473 args.update({ 474 'dh_modulus': cryptutil.longToBase64(self.dh.modulus), 475 'dh_gen': cryptutil.longToBase64(self.dh.generator), 476 }) 477 478 return args
479
480 - def extractSecret(self, response):
481 dh_server_public64 = response.getArg( 482 OPENID_NS, 'dh_server_public', no_default) 483 enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default) 484 dh_server_public = cryptutil.base64ToLong(dh_server_public64) 485 enc_mac_key = oidutil.fromBase64(enc_mac_key64) 486 return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
487
488 -class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
489 session_type = 'DH-SHA256' 490 hash_func = staticmethod(cryptutil.sha256) 491 secret_size = 32 492 allowed_assoc_types = ['HMAC-SHA256']
493
494 -class PlainTextConsumerSession(object):
495 session_type = 'no-encryption' 496 allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] 497
498 - def getRequest(self):
499 return {}
500
501 - def extractSecret(self, response):
502 mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default) 503 return oidutil.fromBase64(mac_key64)
504
505 -class SetupNeededError(Exception):
506 """Internally-used exception that indicates that an immediate-mode 507 request cancelled."""
508 - def __init__(self, user_setup_url=None):
509 Exception.__init__(self, user_setup_url) 510 self.user_setup_url = user_setup_url
511
512 -class ProtocolError(ValueError):
513 """Exception that indicates that a message violated the 514 protocol. It is raised and caught internally to this file."""
515
516 -class TypeURIMismatch(ProtocolError):
517 """A protocol error arising from type URIs mismatching 518 """ 519
520 - def __init__(self, expected, endpoint):
521 ProtocolError.__init__(self, expected, endpoint) 522 self.expected = expected 523 self.endpoint = endpoint
524
525 - def __str__(self):
526 s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % ( 527 self.__class__.__module__, self.__class__.__name__, 528 self.expected, self.endpoint.type_uris, self.endpoint) 529 return s
530 531 532
533 -class ServerError(Exception):
534 """Exception that is raised when the server returns a 400 response 535 code to a direct request.""" 536
537 - def __init__(self, error_text, error_code, message):
538 Exception.__init__(self, error_text) 539 self.error_text = error_text 540 self.error_code = error_code 541 self.message = message
542
543 - def fromMessage(cls, message):
544 """Generate a ServerError instance, extracting the error text 545 and the error code from the message.""" 546 error_text = message.getArg( 547 OPENID_NS, 'error', '<no error message supplied>') 548 error_code = message.getArg(OPENID_NS, 'error_code') 549 return cls(error_text, error_code, message)
550 551 fromMessage = classmethod(fromMessage)
552
553 -class GenericConsumer(object):
554 """This is the implementation of the common logic for OpenID 555 consumers. It is unaware of the application in which it is 556 running. 557 558 @ivar negotiator: An object that controls the kind of associations 559 that the consumer makes. It defaults to 560 C{L{openid.association.default_negotiator}}. Assign a 561 different negotiator to it if you have specific requirements 562 for how associations are made. 563 @type negotiator: C{L{openid.association.SessionNegotiator}} 564 """ 565 566 # The name of the query parameter that gets added to the return_to 567 # URL when using OpenID1. You can change this value if you want or 568 # need a different name, but don't make it start with openid, 569 # because it's not a standard protocol thing for OpenID1. For 570 # OpenID2, the library will take care of the nonce using standard 571 # OpenID query parameter names. 572 openid1_nonce_query_arg_name = 'janrain_nonce' 573 574 # Another query parameter that gets added to the return_to for 575 # OpenID 1; if the user's session state is lost, use this claimed 576 # identifier to do discovery when verifying the response. 577 openid1_return_to_identifier_name = 'openid1_claimed_id' 578 579 session_types = { 580 'DH-SHA1':DiffieHellmanSHA1ConsumerSession, 581 'DH-SHA256':DiffieHellmanSHA256ConsumerSession, 582 'no-encryption':PlainTextConsumerSession, 583 } 584 585 _discover = staticmethod(discover) 586
587 - def __init__(self, store):
588 self.store = store 589 self.negotiator = default_negotiator.copy()
590
591 - def begin(self, service_endpoint):
592 """Create an AuthRequest object for the specified 593 service_endpoint. This method will create an association if 594 necessary.""" 595 if self.store is None: 596 assoc = None 597 else: 598 assoc = self._getAssociation(service_endpoint) 599 600 request = AuthRequest(service_endpoint, assoc) 601 request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce() 602 603 if request.message.isOpenID1(): 604 request.return_to_args[self.openid1_return_to_identifier_name] = \ 605 request.endpoint.claimed_id 606 607 return request
608
609 - def complete(self, message, endpoint, return_to):
610 """Process the OpenID message, using the specified endpoint 611 and return_to URL as context. This method will handle any 612 OpenID message that is sent to the return_to URL. 613 """ 614 mode = message.getArg(OPENID_NS, 'mode', '<No mode set>') 615 616 modeMethod = getattr(self, '_complete_' + mode, 617 self._completeInvalid) 618 619 return modeMethod(message, endpoint, return_to)
620
621 - def _complete_cancel(self, message, endpoint, _):
622 return CancelResponse(endpoint)
623
624 - def _complete_error(self, message, endpoint, _):
625 error = message.getArg(OPENID_NS, 'error') 626 contact = message.getArg(OPENID_NS, 'contact') 627 reference = message.getArg(OPENID_NS, 'reference') 628 629 return FailureResponse(endpoint, error, contact=contact, 630 reference=reference)
631
632 - def _complete_setup_needed(self, message, endpoint, _):
633 if not message.isOpenID2(): 634 return self._completeInvalid(message, endpoint, _) 635 636 user_setup_url = message.getArg(OPENID2_NS, 'user_setup_url') 637 return SetupNeededResponse(endpoint, user_setup_url)
638
639 - def _complete_id_res(self, message, endpoint, return_to):
640 try: 641 self._checkSetupNeeded(message) 642 except SetupNeededError, why: 643 return SetupNeededResponse(endpoint, why.user_setup_url) 644 else: 645 try: 646 return self._doIdRes(message, endpoint, return_to) 647 except (ProtocolError, DiscoveryFailure), why: 648 return FailureResponse(endpoint, why[0])
649
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,))
654
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. 659 """ 660 # Check the openid.return_to args against args in the original 661 # message. 662 try: 663 self._verifyReturnToArgs(message.toPostArgs()) 664 except ProtocolError, why: 665 oidutil.log("Verifying return_to arguments: %s" % (why[0],)) 666 return False 667 668 # Check the return_to base URL against the one in the message. 669 msg_return_to = message.getArg(OPENID_NS, 'return_to') 670 671 # The URL scheme, authority, and path MUST be the same between 672 # the two URLs. 673 app_parts = urlparse(urinorm.urinorm(return_to)) 674 msg_parts = urlparse(urinorm.urinorm(msg_return_to)) 675 676 # (addressing scheme, network location, path) must be equal in 677 # both URLs. 678 for part in range(0, 3): 679 if app_parts[part] != msg_parts[part]: 680 return False 681 682 return True
683 684 _makeKVPost = staticmethod(makeKVPost) 685
686 - def _checkSetupNeeded(self, message):
687 """Check an id_res message to see if it is a 688 checkid_immediate cancel response. 689 690 @raises SetupNeededError: if it is a checkid_immediate cancellation 691 """ 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 694 # parameter. 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)
699
700 - def _doIdRes(self, message, endpoint, return_to):
701 """Handle id_res responses that are not cancellations of 702 immediate mode requests. 703 704 @param message: the response paramaters. 705 @param endpoint: the discovered endpoint object. May be None. 706 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 710 be signed. 711 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 715 when using OpenID 2) 716 717 @returntype: L{Response} 718 """ 719 # Checks for presence of appropriate fields (and checks 720 # signed list fields) 721 self._idResCheckForFields(message) 722 723 if not self._checkReturnTo(message, return_to): 724 raise ProtocolError( 725 "return_to does not match return URL. Expected %r, got %r" 726 % (return_to, message.getArg(OPENID_NS, 'return_to'))) 727 728 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'))) 734 735 self._idResCheckSignature(message, endpoint.server_url) 736 737 # Will raise a ProtocolError if the nonce is bad 738 self._idResCheckNonce(message, endpoint) 739 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)
744
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 749 message. 750 751 See the openid1_nonce_query_arg_name class variable 752 753 @returns: The nonce as a string or None 754 """ 755 return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name)
756
757 - def _idResCheckNonce(self, message, endpoint):
758 if message.isOpenID1(): 759 # This indicates that the nonce was generated by the consumer 760 nonce = self._idResGetNonceOpenID1(message, endpoint) 761 server_url = '' 762 else: 763 nonce = message.getArg(OPENID2_NS, 'response_nonce') 764 server_url = endpoint.server_url 765 766 if nonce is None: 767 raise ProtocolError('Nonce missing from response') 768 769 try: 770 timestamp, salt = splitNonce(nonce) 771 except ValueError, why: 772 raise ProtocolError('Malformed nonce: %s' % (why[0],)) 773 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')
777
778 - def _idResCheckSignature(self, message, server_url):
779 assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') 780 if self.store is None: 781 assoc = None 782 else: 783 assoc = self.store.getAssociation(server_url, assoc_handle) 784 785 if assoc: 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) 792 raise ProtocolError( 793 'Association with %s expired' % (server_url,)) 794 795 if not assoc.checkMessageSignature(message): 796 raise ProtocolError('Bad signature') 797 798 else: 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 802 # _checkAuth. 803 if not self._checkAuth(message, server_url): 804 raise ProtocolError('Server denied check_authentication')
805
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 812 # checked somewhere. 813 basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed'] 814 basic_sig_fields = ['return_to', 'identity'] 815 816 require_fields = { 817 OPENID2_NS: basic_fields + ['op_endpoint'], 818 OPENID1_NS: basic_fields + ['identity'], 819 } 820 821 require_sigs = { 822 OPENID2_NS: basic_sig_fields + ['response_nonce', 823 'claimed_id', 824 'assoc_handle', 825 'op_endpoint',], 826 OPENID1_NS: basic_sig_fields, 827 } 828 829 for field in require_fields[message.getOpenIDNamespace()]: 830 if not message.hasKey(OPENID_NS, field): 831 raise ProtocolError('Missing required field %r' % (field,)) 832 833 signed_list_str = message.getArg(OPENID_NS, 'signed', no_default) 834 signed_list = signed_list_str.split(',') 835 836 for field in require_sigs[message.getOpenIDNamespace()]: 837 # Field is present and not in signed list 838 if message.hasKey(OPENID_NS, field) and field not in signed_list: 839 raise ProtocolError('"%s" not signed' % (field,))
840 841
842 - def _verifyReturnToArgs(query):
843 """Verify that the arguments in the return_to URL are present in this 844 response. 845 """ 846 message = Message.fromPostArgs(query) 847 return_to = message.getArg(OPENID_NS, 'return_to') 848 849 if return_to is None: 850 raise ProtocolError('Response has no return_to') 851 852 parsed_url = urlparse(return_to) 853 rt_query = parsed_url[4] 854 parsed_args = cgi.parse_qsl(rt_query) 855 856 for rt_key, rt_value in parsed_args: 857 try: 858 value = query[rt_key] 859 if rt_value != value: 860 format = ("parameter %s value %r does not match " 861 "return_to's value %r") 862 raise ProtocolError(format % (rt_key, value, rt_value)) 863 except KeyError: 864 format = "return_to parameter %s absent from query %r" 865 raise ProtocolError(format % (rt_key, query)) 866 867 # Make sure all non-OpenID arguments in the response are also 868 # in the signed return_to. 869 bare_args = message.getArgs(BARE_NS) 870 for pair in bare_args.iteritems(): 871 if pair not in parsed_args: 872 raise ProtocolError("Parameter %s not in return_to URL" % (pair[0],))
873 874 _verifyReturnToArgs = staticmethod(_verifyReturnToArgs) 875
876 - def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
877 """ 878 Extract the information from an OpenID assertion message and 879 verify it against the original 880 881 @param endpoint: The endpoint that resulted from doing discovery 882 @param resp_msg: The id_res message object 883 884 @returns: the verified endpoint 885 """ 886 if resp_msg.getOpenIDNamespace() == OPENID2_NS: 887 return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint) 888 else: 889 return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
890 891
892 - def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
893 to_match = OpenIDServiceEndpoint() 894 to_match.type_uris = [OPENID_2_0_TYPE] 895 to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id') 896 to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity') 897 898 # Raises a KeyError when the op_endpoint is not present 899 to_match.server_url = resp_msg.getArg( 900 OPENID2_NS, 'op_endpoint', no_default) 901 902 # claimed_id and identifier must both be present or both 903 # be absent 904 if (to_match.claimed_id is None and 905 to_match.local_id is not None): 906 raise ProtocolError( 907 'openid.identity is present without openid.claimed_id') 908 909 elif (to_match.claimed_id is not None and 910 to_match.local_id is None): 911 raise ProtocolError( 912 'openid.claimed_id is present without openid.identity') 913 914 # This is a response without identifiers, so there's really no 915 # checking that we can do, so return an endpoint that's for 916 # the specified `openid.op_endpoint' 917 elif to_match.claimed_id is None: 918 return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url) 919 920 # The claimed ID doesn't match, so we have to do discovery 921 # again. This covers not using sessions, OP identifier 922 # endpoints and responses that didn't match the original 923 # request. 924 if not endpoint: 925 oidutil.log('No pre-discovered information supplied.') 926 endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match]) 927 else: 928 # The claimed ID matches, so we use the endpoint that we 929 # discovered in initiation. This should be the most common 930 # case. 931 try: 932 self._verifyDiscoverySingle(endpoint, to_match) 933 except ProtocolError, e: 934 oidutil.log( 935 "Error attempting to use stored discovery information: " + 936 str(e)) 937 oidutil.log("Attempting discovery to verify endpoint") 938 endpoint = self._discoverAndVerify( 939 to_match.claimed_id, [to_match]) 940 941 # The endpoint we return should have the claimed ID from the 942 # message we just verified, fragment and all. 943 if endpoint.claimed_id != to_match.claimed_id: 944 endpoint = copy.copy(endpoint) 945 endpoint.claimed_id = to_match.claimed_id 946 return endpoint
947
948 - def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
949 claimed_id = resp_msg.getArg(BARE_NS, self.openid1_return_to_identifier_name) 950 951 if endpoint is None and claimed_id is None: 952 raise RuntimeError( 953 'When using OpenID 1, the claimed ID must be supplied, ' 954 'either by passing it through as a return_to parameter ' 955 'or by using a session, and supplied to the GenericConsumer ' 956 'as the argument to complete()') 957 elif endpoint is not None and claimed_id is None: 958 claimed_id = endpoint.claimed_id 959 960 to_match = OpenIDServiceEndpoint() 961 to_match.type_uris = [OPENID_1_1_TYPE] 962 to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity') 963 # Restore delegate information from the initiation phase 964 to_match.claimed_id = claimed_id 965 966 if to_match.local_id is None: 967 raise ProtocolError('Missing required field openid.identity') 968 969 to_match_1_0 = copy.copy(to_match) 970 to_match_1_0.type_uris = [OPENID_1_0_TYPE] 971 972 if endpoint is not None: 973 try: 974 try: 975 self._verifyDiscoverySingle(endpoint, to_match) 976 except TypeURIMismatch: 977 self._verifyDiscoverySingle(endpoint, to_match_1_0) 978 except ProtocolError, e: 979 oidutil.log("Error attempting to use stored discovery information: " + 980 str(e)) 981 oidutil.log("Attempting discovery to verify endpoint") 982 else: 983 return endpoint 984 985 # Endpoint is either bad (failed verification) or None 986 return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0])
987
988 - def _verifyDiscoverySingle(self, endpoint, to_match):
989 """Verify that the given endpoint matches the information 990 extracted from the OpenID assertion, and raise an exception if 991 there is a mismatch. 992 993 @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint 994 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint 995 996 @rtype: NoneType 997 998 @raises ProtocolError: when the endpoint does not match the 999 discovered information. 1000 """ 1001 # Every type URI that's in the to_match endpoint has to be 1002 # present in the discovered endpoint. 1003 for type_uri in to_match.type_uris: 1004 if not endpoint.usesExtension(type_uri): 1005 raise TypeURIMismatch(type_uri, endpoint) 1006 1007 # Fragments do not influence discovery, so we can't compare a 1008 # claimed identifier with a fragment to discovered information. 1009 defragged_claimed_id, _ = urldefrag(to_match.claimed_id) 1010 if defragged_claimed_id != endpoint.claimed_id: 1011 raise ProtocolError( 1012 'Claimed ID does not match (different subjects!), ' 1013 'Expected %s, got %s' % 1014 (defragged_claimed_id, endpoint.claimed_id)) 1015 1016 if to_match.getLocalID() != endpoint.getLocalID(): 1017 raise ProtocolError('local_id mismatch. Expected %s, got %s' % 1018 (to_match.getLocalID(), endpoint.getLocalID())) 1019 1020 # If the server URL is None, this must be an OpenID 1 1021 # response, because op_endpoint is a required parameter in 1022 # OpenID 2. In that case, we don't actually care what the 1023 # discovered server_url is, because signature checking or 1024 # check_auth should take care of that check for us. 1025 if to_match.server_url is None: 1026 assert to_match.preferredNamespace() == OPENID1_NS, ( 1027 """The code calling this must ensure that OpenID 2 1028 responses have a non-none `openid.op_endpoint' and 1029 that it is set as the `server_url' attribute of the 1030 `to_match' endpoint.""") 1031 1032 elif to_match.server_url != endpoint.server_url: 1033 raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' % 1034 (to_match.server_url, endpoint.server_url))
1035
1036 - def _discoverAndVerify(self, claimed_id, to_match_endpoints):
1037 """Given an endpoint object created from the information in an 1038 OpenID response, perform discovery and verify the discovery 1039 results, returning the matching endpoint that is the result of 1040 doing that discovery. 1041 1042 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint 1043 @param to_match: The endpoint whose information we're confirming 1044 1045 @rtype: openid.consumer.discover.OpenIDServiceEndpoint 1046 @returns: The result of performing discovery on the claimed 1047 identifier in `to_match' 1048 1049 @raises DiscoveryFailure: when discovery fails. 1050 """ 1051 oidutil.log('Performing discovery on %s' % (claimed_id,)) 1052 _, services = self._discover(claimed_id) 1053 if not services: 1054 raise DiscoveryFailure('No OpenID information found at %s' % 1055 (claimed_id,), None) 1056 return self._verifyDiscoveredServices(claimed_id, services, 1057 to_match_endpoints)
1058 1059
1060 - def _verifyDiscoveredServices(self, claimed_id, services, to_match_endpoints):
1061 """See @L{_discoverAndVerify}""" 1062 1063 # Search the services resulting from discovery to find one 1064 # that matches the information from the assertion 1065 failure_messages = [] 1066 for endpoint in services: 1067 for to_match_endpoint in to_match_endpoints: 1068 try: 1069 self._verifyDiscoverySingle( 1070 endpoint, to_match_endpoint) 1071 except ProtocolError, why: 1072 failure_messages.append(str(why)) 1073 else: 1074 # It matches, so discover verification has 1075 # succeeded. Return this endpoint. 1076 return endpoint 1077 else: 1078 oidutil.log('Discovery verification failure for %s' % 1079 (claimed_id,)) 1080 for failure_message in failure_messages: 1081 oidutil.log(' * Endpoint mismatch: ' + failure_message) 1082 1083 raise DiscoveryFailure( 1084 'No matching endpoint found after discovering %s' 1085 % (claimed_id,), None)
1086
1087 - def _checkAuth(self, message, server_url):
1088 """Make a check_authentication request to verify this message. 1089 1090 @returns: True if the request is valid. 1091 @rtype: bool 1092 """ 1093 oidutil.log('Using OpenID check_authentication') 1094 request = self._createCheckAuthRequest(message) 1095 if request is None: 1096 return False 1097 try: 1098 response = self._makeKVPost(request, server_url) 1099 except (fetchers.HTTPFetchingError, ServerError), e: 1100 oidutil.log('check_authentication failed: %s' % (e[0],)) 1101 return False 1102 else: 1103 return self._processCheckAuthResponse(response, server_url)
1104
1105 - def _createCheckAuthRequest(self, message):
1106 """Generate a check_authentication request message given an 1107 id_res message. 1108 """ 1109 signed = message.getArg(OPENID_NS, 'signed') 1110 if signed: 1111 for k in signed.split(','): 1112 oidutil.log(k) 1113 val = message.getAliasedArg(k) 1114 1115 # Signed value is missing 1116 if val is None: 1117 oidutil.log('Missing signed field %r' % (k,)) 1118 return None 1119 1120 check_auth_message = message.copy() 1121 check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication') 1122 return check_auth_message
1123
1124 - def _processCheckAuthResponse(self, response, server_url):
1125 """Process the response message from a check_authentication 1126 request, invalidating associations if requested. 1127 """ 1128 is_valid = response.getArg(OPENID_NS, 'is_valid', 'false') 1129 1130 invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle') 1131 if invalidate_handle is not None: 1132 oidutil.log( 1133 'Received "invalidate_handle" from server %s' % (server_url,)) 1134 if self.store is None: 1135 oidutil.log('Unexpectedly got invalidate_handle without ' 1136 'a store!') 1137 else: 1138 self.store.removeAssociation(server_url, invalidate_handle) 1139 1140 if is_valid == 'true': 1141 return True 1142 else: 1143 oidutil.log('Server responds that checkAuth call is not valid') 1144 return False
1145
1146 - def _getAssociation(self, endpoint):
1147 """Get an association for the endpoint's server_url. 1148 1149 First try seeing if we have a good association in the 1150 store. If we do not, then attempt to negotiate an association 1151 with the server. 1152 1153 If we negotiate a good association, it will get stored. 1154 1155 @returns: A valid association for the endpoint's server_url or None 1156 @rtype: openid.association.Association or NoneType 1157 """ 1158 assoc = self.store.getAssociation(endpoint.server_url) 1159 1160 if assoc is None or assoc.expiresIn <= 0: 1161 assoc = self._negotiateAssociation(endpoint) 1162 if assoc is not None: 1163 self.store.storeAssociation(endpoint.server_url, assoc) 1164 1165 return assoc
1166
1167 - def _negotiateAssociation(self, endpoint):
1168 """Make association requests to the server, attempting to 1169 create a new association. 1170 1171 @returns: a new association object 1172 1173 @rtype: L{openid.association.Association} 1174 """ 1175 # Get our preferred session/association type from the negotiatior. 1176 assoc_type, session_type = self.negotiator.getAllowedType() 1177 1178 try: 1179 assoc = self._requestAssociation( 1180 endpoint, assoc_type, session_type) 1181 except ServerError, why: 1182 supportedTypes = self._extractSupportedAssociationType(why, 1183 endpoint, 1184 assoc_type) 1185 if supportedTypes is not None: 1186 assoc_type, session_type = supportedTypes 1187 # Attempt to create an association from the assoc_type 1188 # and session_type that the server told us it 1189 # supported. 1190 try: 1191 assoc = self._requestAssociation( 1192 endpoint, assoc_type, session_type) 1193 except ServerError, why: 1194 # Do not keep trying, since it rejected the 1195 # association type that it told us to use. 1196 oidutil.log('Server %s refused its suggested association ' 1197 'type: session_type=%s, assoc_type=%s' 1198 % (endpoint.server_url, session_type, 1199 assoc_type)) 1200 return None 1201 else: 1202 return assoc 1203 else: 1204 return assoc
1205
1206 - def _extractSupportedAssociationType(self, server_error, endpoint, 1207 assoc_type):
1208 """Handle ServerErrors resulting from association requests. 1209 1210 @returns: If server replied with an C{unsupported-type} error, 1211 return a tuple of supported C{association_type}, C{session_type}. 1212 Otherwise logs the error and returns None. 1213 @rtype: tuple or None 1214 """ 1215 # Any error message whose code is not 'unsupported-type' 1216 # should be considered a total failure. 1217 if server_error.error_code != 'unsupported-type' or \ 1218 server_error.message.isOpenID1(): 1219 oidutil.log( 1220 'Server error when requesting an association from %r: %s' 1221 % (endpoint.server_url, server_error.error_text)) 1222 return None 1223 1224 # The server didn't like the association/session type 1225 # that we sent, and it sent us back a message that 1226 # might tell us how to handle it. 1227 oidutil.log( 1228 'Unsupported association type %s: %s' % (assoc_type, 1229 server_error.error_text,)) 1230 1231 # Extract the session_type and assoc_type from the 1232 # error message 1233 assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type') 1234 session_type = server_error.message.getArg(OPENID_NS, 'session_type') 1235 1236 if assoc_type is None or session_type is None: 1237 oidutil.log('Server responded with unsupported association ' 1238 'session but did not supply a fallback.') 1239 return None 1240 elif not self.negotiator.isAllowed(assoc_type, session_type): 1241 fmt = ('Server sent unsupported session/association type: ' 1242 'session_type=%s, assoc_type=%s') 1243 oidutil.log(fmt % (session_type, assoc_type)) 1244 return None 1245 else: 1246 return assoc_type, session_type
1247 1248
1249 - def _requestAssociation(self, endpoint, assoc_type, session_type):
1250 """Make and process one association request to this endpoint's 1251 OP endpoint URL. 1252 1253 @returns: An association object or None if the association 1254 processing failed. 1255 1256 @raises ServerError: when the remote OpenID server returns an error. 1257 """ 1258 assoc_session, args = self._createAssociateRequest( 1259 endpoint, assoc_type, session_type) 1260 1261 try: 1262 response = self._makeKVPost(args, endpoint.server_url) 1263 except fetchers.HTTPFetchingError, why: 1264 oidutil.log('openid.associate request failed: %s' % (why[0],)) 1265 return None 1266 1267 try: 1268 assoc = self._extractAssociation(response, assoc_session) 1269 except KeyError, why: 1270 oidutil.log('Missing required parameter in response from %s: %s' 1271 % (endpoint.server_url, why[0])) 1272 return None 1273 except ProtocolError, why: 1274 oidutil.log('Protocol error parsing response from %s: %s' % ( 1275 endpoint.server_url, why[0])) 1276 return None 1277 else: 1278 return assoc
1279
1280 - def _createAssociateRequest(self, endpoint, assoc_type, session_type):
1281 """Create an association request for the given assoc_type and 1282 session_type. 1283 1284 @param endpoint: The endpoint whose server_url will be 1285 queried. The important bit about the endpoint is whether 1286 it's in compatiblity mode (OpenID 1.1) 1287 1288 @param assoc_type: The association type that the request 1289 should ask for. 1290 @type assoc_type: str 1291 1292 @param session_type: The session type that should be used in 1293 the association request. The session_type is used to 1294 create an association session object, and that session 1295 object is asked for any additional fields that it needs to 1296 add to the request. 1297 @type session_type: str 1298 1299 @returns: a pair of the association session object and the 1300 request message that will be sent to the server. 1301 @rtype: (association session type (depends on session_type), 1302 openid.message.Message) 1303 """ 1304 session_type_class = self.session_types[session_type] 1305 assoc_session = session_type_class() 1306 1307 args = { 1308 'mode': 'associate', 1309 'assoc_type': assoc_type, 1310 } 1311 1312 if not endpoint.compatibilityMode(): 1313 args['ns'] = OPENID2_NS 1314 1315 # Leave out the session type if we're in compatibility mode 1316 # *and* it's no-encryption. 1317 if (not endpoint.compatibilityMode() or 1318 assoc_session.session_type != 'no-encryption'): 1319 args['session_type'] = assoc_session.session_type 1320 1321 args.update(assoc_session.getRequest()) 1322 message = Message.fromOpenIDArgs(args) 1323 return assoc_session, message
1324
1325 - def _getOpenID1SessionType(self, assoc_response):
1326 """Given an association response message, extract the OpenID 1327 1.X session type. 1328 1329 This function mostly takes care of the 'no-encryption' default 1330 behavior in OpenID 1. 1331 1332 If the association type is plain-text, this function will 1333 return 'no-encryption' 1334 1335 @returns: The association type for this message 1336 @rtype: str 1337 1338 @raises KeyError: when the session_type field is absent. 1339 """ 1340 # If it's an OpenID 1 message, allow session_type to default 1341 # to None (which signifies "no-encryption") 1342 session_type = assoc_response.getArg(OPENID1_NS, 'session_type') 1343 1344 # Handle the differences between no-encryption association 1345 # respones in OpenID 1 and 2: 1346 1347 # no-encryption is not really a valid session type for 1348 # OpenID 1, but we'll accept it anyway, while issuing a 1349 # warning. 1350 if session_type == 'no-encryption': 1351 oidutil.log('WARNING: OpenID server sent "no-encryption"' 1352 'for OpenID 1.X') 1353 1354 # Missing or empty session type is the way to flag a 1355 # 'no-encryption' response. Change the session type to 1356 # 'no-encryption' so that it can be handled in the same 1357 # way as OpenID 2 'no-encryption' respones. 1358 elif session_type == '' or session_type is None: 1359 session_type = 'no-encryption' 1360 1361 return session_type
1362
1363 - def _extractAssociation(self, assoc_response, assoc_session):
1364 """Attempt to extract an association from the response, given 1365 the association response message and the established 1366 association session. 1367 1368 @param assoc_response: The association response message from 1369 the server 1370 @type assoc_response: openid.message.Message 1371 1372 @param assoc_session: The association session object that was 1373 used when making the request 1374 @type assoc_session: depends on the session type of the request 1375 1376 @raises ProtocolError: when data is malformed 1377 @raises KeyError: when a field is missing 1378 1379 @rtype: openid.association.Association 1380 """ 1381 # Extract the common fields from the response, raising an 1382 # exception if they are not found 1383 assoc_type = assoc_response.getArg( 1384 OPENID_NS, 'assoc_type', no_default) 1385 assoc_handle = assoc_response.getArg( 1386 OPENID_NS, 'assoc_handle', no_default) 1387 1388 # expires_in is a base-10 string. The Python parsing will 1389 # accept literals that have whitespace around them and will 1390 # accept negative values. Neither of these are really in-spec, 1391 # but we think it's OK to accept them. 1392 expires_in_str = assoc_response.getArg( 1393 OPENID_NS, 'expires_in', no_default) 1394 try: 1395 expires_in = int(expires_in_str) 1396 except ValueError, why: 1397 raise ProtocolError('Invalid expires_in field: %s' % (why[0],)) 1398 1399 # OpenID 1 has funny association session behaviour. 1400 if assoc_response.isOpenID1(): 1401 session_type = self._getOpenID1SessionType(assoc_response) 1402 else: 1403 session_type = assoc_response.getArg( 1404 OPENID2_NS, 'session_type', no_default) 1405 1406 # Session type mismatch 1407 if assoc_session.session_type != session_type: 1408 if (assoc_response.isOpenID1() and 1409 session_type == 'no-encryption'): 1410 # In OpenID 1, any association request can result in a 1411 # 'no-encryption' association response. Setting 1412 # assoc_session to a new no-encryption session should 1413 # make the rest of this function work properly for 1414 # that case. 1415 assoc_session = PlainTextConsumerSession() 1416 else: 1417 # Any other mismatch, regardless of protocol version 1418 # results in the failure of the association session 1419 # altogether. 1420 fmt = 'Session type mismatch. Expected %r, got %r' 1421 message = fmt % (assoc_session.session_type, session_type) 1422 raise ProtocolError(message) 1423 1424 # Make sure assoc_type is valid for session_type 1425 if assoc_type not in assoc_session.allowed_assoc_types: 1426 fmt = 'Unsupported assoc_type for session %s returned: %s' 1427 raise ProtocolError(fmt % (assoc_session.session_type, assoc_type)) 1428 1429 # Delegate to the association session to extract the secret 1430 # from the response, however is appropriate for that session 1431 # type. 1432 try: 1433 secret = assoc_session.extractSecret(assoc_response) 1434 except ValueError, why: 1435 fmt = 'Malformed response for %s session: %s' 1436 raise ProtocolError(fmt % (assoc_session.session_type, why[0])) 1437 1438 return Association.fromExpiresIn( 1439 expires_in, assoc_handle, secret, assoc_type)
1440
1441 -class AuthRequest(object):
1442 """An object that holds the state necessary for generating an 1443 OpenID authentication request. This object holds the association 1444 with the server and the discovered information with which the 1445 request will be made. 1446 1447 It is separate from the consumer because you may wish to add 1448 things to the request before sending it on its way to the 1449 server. It also has serialization options that let you encode the 1450 authentication request as a URL or as a form POST. 1451 """ 1452
1453 - def __init__(self, endpoint, assoc):
1454 """ 1455 Creates a new AuthRequest object. This just stores each 1456 argument in an appropriately named field. 1457 1458 Users of this library should not create instances of this 1459 class. Instances of this class are created by the library 1460 when needed. 1461 """ 1462 self.assoc = assoc 1463 self.endpoint = endpoint 1464 self.return_to_args = {} 1465 self.message = Message(endpoint.preferredNamespace()) 1466 self._anonymous = False
1467
1468 - def setAnonymous(self, is_anonymous):
1469 """Set whether this request should be made anonymously. If a 1470 request is anonymous, the identifier will not be sent in the 1471 request. This is only useful if you are making another kind of 1472 request with an extension in this request. 1473 1474 Anonymous requests are not allowed when the request is made 1475 with OpenID 1. 1476 1477 @raises ValueError: when attempting to set an OpenID1 request 1478 as anonymous 1479 """ 1480 if is_anonymous and self.message.isOpenID1(): 1481 raise ValueError('OpenID 1 requests MUST include the ' 1482 'identifier in the request') 1483 else: 1484 self._anonymous = is_anonymous
1485
1486 - def addExtension(self, extension_request):
1487 """Add an extension to this checkid request. 1488 1489 @param extension_request: An object that implements the 1490 extension interface for adding arguments to an OpenID 1491 message. 1492 """ 1493 extension_request.toMessage(self.message)
1494
1495 - def addExtensionArg(self, namespace, key, value):
1496 """Add an extension argument to this OpenID authentication 1497 request. 1498 1499 Use caution when adding arguments, because they will be 1500 URL-escaped and appended to the redirect URL, which can easily 1501 get quite long. 1502 1503 @param namespace: The namespace for the extension. For 1504 example, the simple registration extension uses the 1505 namespace C{sreg}. 1506 1507 @type namespace: str 1508 1509 @param key: The key within the extension namespace. For 1510 example, the nickname field in the simple registration 1511 extension's key is C{nickname}. 1512 1513 @type key: str 1514 1515 @param value: The value to provide to the server for this 1516 argument. 1517 1518 @type value: str 1519 """ 1520 self.message.setArg(namespace, key, value)
1521
1522 - def getMessage(self, realm, return_to=None, immediate=False):
1523 """Produce a L{openid.message.Message} representing this request. 1524 1525 @param realm: The URL (or URL pattern) that identifies your 1526 web site to the user when she is authorizing it. 1527 1528 @type realm: str 1529 1530 @param return_to: The URL that the OpenID provider will send the 1531 user back to after attempting to verify her identity. 1532 1533 Not specifying a return_to URL means that the user will not 1534 be returned to the site issuing the request upon its 1535 completion. 1536 1537 @type return_to: str 1538 1539 @param immediate: If True, the OpenID provider is to send back 1540 a response immediately, useful for behind-the-scenes 1541 authentication attempts. Otherwise the OpenID provider 1542 may engage the user before providing a response. This is 1543 the default case, as the user may need to provide 1544 credentials or approve the request before a positive 1545 response can be sent. 1546 1547 @type immediate: bool 1548 1549 @returntype: L{openid.message.Message} 1550 """ 1551 if return_to: 1552 return_to = oidutil.appendArgs(return_to, self.return_to_args) 1553 elif immediate: 1554 raise ValueError( 1555 '"return_to" is mandatory when using "checkid_immediate"') 1556 elif self.message.isOpenID1(): 1557 raise ValueError('"return_to" is mandatory for OpenID 1 requests') 1558 elif self.return_to_args: 1559 raise ValueError('extra "return_to" arguments were specified, ' 1560 'but no return_to was specified') 1561 1562 if immediate: 1563 mode = 'checkid_immediate' 1564 else: 1565 mode = 'checkid_setup' 1566 1567 message = self.message.copy() 1568 if message.isOpenID1(): 1569 realm_key = 'trust_root' 1570 else: 1571 realm_key = 'realm' 1572 1573 message.updateArgs(OPENID_NS, 1574 { 1575 realm_key:realm, 1576 'mode':mode, 1577 'return_to':return_to, 1578 }) 1579 1580 if not self._anonymous: 1581 if self.endpoint.isOPIdentifier(): 1582 # This will never happen when we're in compatibility 1583 # mode, as long as isOPIdentifier() returns False 1584 # whenever preferredNamespace() returns OPENID1_NS. 1585 claimed_id = request_identity = IDENTIFIER_SELECT 1586 else: 1587 request_identity = self.endpoint.getLocalID() 1588 claimed_id = self.endpoint.claimed_id 1589 1590 # This is true for both OpenID 1 and 2 1591 message.setArg(OPENID_NS, 'identity', request_identity) 1592 1593 if message.isOpenID2(): 1594 message.setArg(OPENID2_NS, 'claimed_id', claimed_id) 1595 1596 if self.assoc: 1597 message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle) 1598 assoc_log_msg = 'with assocication %s' % (self.assoc.handle,) 1599 else: 1600 assoc_log_msg = 'using stateless mode.' 1601 1602 oidutil.log("Generated %s request to %s %s" % 1603 (mode, self.endpoint.server_url, assoc_log_msg)) 1604 1605 return message
1606
1607 - def redirectURL(self, realm, return_to=None, immediate=False):
1608 """Returns a URL with an encoded OpenID request. 1609 1610 The resulting URL is the OpenID provider's endpoint URL with 1611 parameters appended as query arguments. You should redirect 1612 the user agent to this URL. 1613 1614 OpenID 2.0 endpoints also accept POST requests, see 1615 C{L{shouldSendRedirect}} and C{L{formMarkup}}. 1616 1617 @param realm: The URL (or URL pattern) that identifies your 1618 web site to the user when she is authorizing it. 1619 1620 @type realm: str 1621 1622 @param return_to: The URL that the OpenID provider will send the 1623 user back to after attempting to verify her identity. 1624 1625 Not specifying a return_to URL means that the user will not 1626 be returned to the site issuing the request upon its 1627 completion. 1628 1629 @type return_to: str 1630 1631 @param immediate: If True, the OpenID provider is to send back 1632 a response immediately, useful for behind-the-scenes 1633 authentication attempts. Otherwise the OpenID provider 1634 may engage the user before providing a response. This is 1635 the default case, as the user may need to provide 1636 credentials or approve the request before a positive 1637 response can be sent. 1638 1639 @type immediate: bool 1640 1641 @returns: The URL to redirect the user agent to. 1642 1643 @returntype: str 1644 """ 1645 message = self.getMessage(realm, return_to, immediate) 1646 return message.toURL(self.endpoint.server_url)
1647
1648 - def formMarkup(self, realm, return_to=None, immediate=False, 1649 form_tag_attrs=None):
1650 """Get html for a form to submit this request to the IDP. 1651 1652 @param form_tag_attrs: Dictionary of attributes to be added to 1653 the form tag. 'accept-charset' and 'enctype' have defaults 1654 that can be overridden. If a value is supplied for 1655 'action' or 'method', it will be replaced. 1656 @type form_tag_attrs: {unicode: unicode} 1657 """ 1658 message = self.getMessage(realm, return_to, immediate) 1659 return message.toFormMarkup(self.endpoint.server_url, 1660 form_tag_attrs)
1661
1662 - def htmlMarkup(self, realm, return_to=None, immediate=False, 1663 form_tag_attrs=None):
1664 """Get an autosubmitting HTML page that submits this request to the 1665 IDP. This is just a wrapper for formMarkup. 1666 1667 @see: formMarkup 1668 1669 @returns: str 1670 """ 1671 return oidutil.autoSubmitHTML(self.formMarkup(realm, 1672 return_to, 1673 immediate, 1674 form_tag_attrs))
1675
1676 - def shouldSendRedirect(self):
1677 """Should this OpenID authentication request be sent as a HTTP 1678 redirect or as a POST (form submission)? 1679 1680 @rtype: bool 1681 """ 1682 return self.endpoint.compatibilityMode()
1683 1684 FAILURE = 'failure' 1685 SUCCESS = 'success' 1686 CANCEL = 'cancel' 1687 SETUP_NEEDED = 'setup_needed' 1688
1689 -class Response(object):
1690 status = None 1691
1692 - def setEndpoint(self, endpoint):
1693 self.endpoint = endpoint 1694 if endpoint is None: 1695 self.identity_url = None 1696 else: 1697 self.identity_url = endpoint.claimed_id
1698
1699 - def getDisplayIdentifier(self):
1700 """Return the display identifier for this response. 1701 1702 The display identifier is related to the Claimed Identifier, but the 1703 two are not always identical. The display identifier is something the 1704 user should recognize as what they entered, whereas the response's 1705 claimed identifier (in the L{identity_url} attribute) may have extra 1706 information for better persistence. 1707 1708 URLs will be stripped of their fragments for display. XRIs will 1709 display the human-readable identifier (i-name) instead of the 1710 persistent identifier (i-number). 1711 1712 Use the display identifier in your user interface. Use 1713 L{identity_url} for querying your database or authorization server. 1714 """ 1715 if self.endpoint is not None: 1716 return self.endpoint.getDisplayIdentifier() 1717 return None
1718
1719 -class SuccessResponse(Response):
1720 """A response with a status of SUCCESS. Indicates that this request is a 1721 successful acknowledgement from the OpenID server that the 1722 supplied URL is, indeed controlled by the requesting agent. 1723 1724 @ivar identity_url: The identity URL that has been authenticated; the Claimed Identifier. 1725 See also L{getDisplayIdentifier}. 1726 1727 @ivar endpoint: The endpoint that authenticated the identifier. You 1728 may access other discovered information related to this endpoint, 1729 such as the CanonicalID of an XRI, through this object. 1730 @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>} 1731 1732 @ivar signed_fields: The arguments in the server's response that 1733 were signed and verified. 1734 1735 @cvar status: SUCCESS 1736 """ 1737 1738 status = SUCCESS 1739
1740 - def __init__(self, endpoint, message, signed_fields=None):
1741 # Don't use setEndpoint, because endpoint should never be None 1742 # for a successfull transaction. 1743 self.endpoint = endpoint 1744 self.identity_url = endpoint.claimed_id 1745 1746 self.message = message 1747 1748 if signed_fields is None: 1749 signed_fields = [] 1750 self.signed_fields = signed_fields
1751
1752 - def isOpenID1(self):
1753 """Was this authentication response an OpenID 1 authentication 1754 response? 1755 """ 1756 return self.message.isOpenID1()
1757
1758 - def isSigned(self, ns_uri, ns_key):
1759 """Return whether a particular key is signed, regardless of 1760 its namespace alias 1761 """ 1762 return self.message.getKey(ns_uri, ns_key) in self.signed_fields
1763
1764 - def getSigned(self, ns_uri, ns_key, default=None):
1765 """Return the specified signed field if available, 1766 otherwise return default 1767 """ 1768 if self.isSigned(ns_uri, ns_key): 1769 return self.message.getArg(ns_uri, ns_key, default) 1770 else: 1771 return default
1772
1773 - def getSignedNS(self, ns_uri):
1774 """Get signed arguments from the response message. Return a 1775 dict of all arguments in the specified namespace. If any of 1776 the arguments are not signed, return None. 1777 """ 1778 msg_args = self.message.getArgs(ns_uri) 1779 1780 for key in msg_args.iterkeys(): 1781 if not self.isSigned(ns_uri, key): 1782 oidutil.log("SuccessResponse.getSignedNS: (%s, %s) not signed." 1783 % (ns_uri, key)) 1784 return None 1785 1786 return msg_args
1787
1788 - def extensionResponse(self, namespace_uri, require_signed):
1789 """Return response arguments in the specified namespace. 1790 1791 @param namespace_uri: The namespace URI of the arguments to be 1792 returned. 1793 1794 @param require_signed: True if the arguments should be among 1795 those signed in the response, False if you don't care. 1796 1797 If require_signed is True and the arguments are not signed, 1798 return None. 1799 """ 1800 if require_signed: 1801 return self.getSignedNS(namespace_uri) 1802 else: 1803 return self.message.getArgs(namespace_uri)
1804
1805 - def getReturnTo(self):
1806 """Get the openid.return_to argument from this response. 1807 1808 This is useful for verifying that this request was initiated 1809 by this consumer. 1810 1811 @returns: The return_to URL supplied to the server on the 1812 initial request, or C{None} if the response did not contain 1813 an C{openid.return_to} argument. 1814 1815 @returntype: str 1816 """ 1817 return self.getSigned(OPENID_NS, 'return_to')
1818
1819 - def __eq__(self, other):
1820 return ( 1821 (self.endpoint == other.endpoint) and 1822 (self.identity_url == other.identity_url) and 1823 (self.message == other.message) and 1824 (self.signed_fields == other.signed_fields) and 1825 (self.status == other.status))
1826
1827 - def __ne__(self, other):
1828 return not (self == other)
1829
1830 - def __repr__(self):
1831 return '<%s.%s id=%r signed=%r>' % ( 1832 self.__class__.__module__, 1833 self.__class__.__name__, 1834 self.identity_url, self.signed_fields)
1835 1836
1837 -class FailureResponse(Response):
1838 """A response with a status of FAILURE. Indicates that the OpenID 1839 protocol has failed. This could be locally or remotely triggered. 1840 1841 @ivar identity_url: The identity URL for which authenitcation was 1842 attempted, if it can be determined. Otherwise, None. 1843 1844 @ivar message: A message indicating why the request failed, if one 1845 is supplied. otherwise, None. 1846 1847 @cvar status: FAILURE 1848 """ 1849 1850 status = FAILURE 1851
1852 - def __init__(self, endpoint, message=None, contact=None, 1853 reference=None):
1854 self.setEndpoint(endpoint) 1855 self.message = message 1856 self.contact = contact 1857 self.reference = reference
1858
1859 - def __repr__(self):
1860 return "<%s.%s id=%r message=%r>" % ( 1861 self.__class__.__module__, self.__class__.__name__, 1862 self.identity_url, self.message)
1863 1864
1865 -class CancelResponse(Response):
1866 """A response with a status of CANCEL. Indicates that the user 1867 cancelled the OpenID authentication request. 1868 1869 @ivar identity_url: The identity URL for which authenitcation was 1870 attempted, if it can be determined. Otherwise, None. 1871 1872 @cvar status: CANCEL 1873 """ 1874 1875 status = CANCEL 1876
1877 - def __init__(self, endpoint):
1878 self.setEndpoint(endpoint)
1879
1880 -class SetupNeededResponse(Response):
1881 """A response with a status of SETUP_NEEDED. Indicates that the 1882 request was in immediate mode, and the server is unable to 1883 authenticate the user without further interaction. 1884 1885 @ivar identity_url: The identity URL for which authenitcation was 1886 attempted. 1887 1888 @ivar setup_url: A URL that can be used to send the user to the 1889 server to set up for authentication. The user should be 1890 redirected in to the setup_url, either in the current window 1891 or in a new browser window. C{None} in OpenID 2.0. 1892 1893 @cvar status: SETUP_NEEDED 1894 """ 1895 1896 status = SETUP_NEEDED 1897
1898 - def __init__(self, endpoint, setup_url=None):
1899 self.setEndpoint(endpoint) 1900 self.setup_url = setup_url
1901