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

Source Code for Module openid.consumer.consumer

  1  # -*- test-case-name: openid.test.consumer -*- 
  2  """ 
  3  This module documents the main interface with the OpenID consumer 
  4  library.  The only part of the library which has to be used and isn't 
  5  documented in full here is the store required to create an 
  6  C{L{Consumer}} instance.  More on the abstract store type and 
  7  concrete implementations of it that are provided in the documentation 
  8  for the C{L{__init__<Consumer.__init__>}} method of the 
  9  C{L{Consumer}} class. 
 10   
 11   
 12  OVERVIEW 
 13  ======== 
 14   
 15      The OpenID identity verification process most commonly uses the 
 16      following steps, as visible to the user of this library: 
 17   
 18          1. The user enters their OpenID into a field on the consumer's 
 19             site, and hits a login button. 
 20   
 21          2. The consumer site discovers the user's OpenID server using 
 22             the YADIS protocol. 
 23   
 24          3. The consumer site sends the browser a redirect to the 
 25             identity server.  This is the authentication request as 
 26             described in the OpenID specification. 
 27   
 28          4. The identity server's site sends the browser a redirect 
 29             back to the consumer site.  This redirect contains the 
 30             server's response to the authentication request. 
 31   
 32      The most important part of the flow to note is the consumer's site 
 33      must handle two separate HTTP requests in order to perform the 
 34      full identity check. 
 35   
 36   
 37  LIBRARY DESIGN 
 38  ============== 
 39   
 40      This consumer library is designed with that flow in mind.  The 
 41      goal is to make it as easy as possible to perform the above steps 
 42      securely. 
 43   
 44      At a high level, there are two important parts in the consumer 
 45      library.  The first important part is this module, which contains 
 46      the interface to actually use this library.  The second is the 
 47      C{L{openid.store.interface}} module, which describes the 
 48      interface to use if you need to create a custom method for storing 
 49      the state this library needs to maintain between requests. 
 50   
 51      In general, the second part is less important for users of the 
 52      library to know about, as several implementations are provided 
 53      which cover a wide variety of situations in which consumers may 
 54      use the library. 
 55   
 56      This module contains a class, C{L{Consumer}}, with methods 
 57      corresponding to the actions necessary in each of steps 2, 3, and 
 58      4 described in the overview.  Use of this library should be as easy 
 59      as creating an C{L{Consumer}} instance and calling the methods 
 60      appropriate for the action the site wants to take. 
 61   
 62   
 63  STORES AND DUMB MODE 
 64  ==================== 
 65   
 66      OpenID is a protocol that works best when the consumer site is 
 67      able to store some state.  This is the normal mode of operation 
 68      for the protocol, and is sometimes referred to as smart mode. 
 69      There is also a fallback mode, known as dumb mode, which is 
 70      available when the consumer site is not able to store state.  This 
 71      mode should be avoided when possible, as it leaves the 
 72      implementation more vulnerable to replay attacks. 
 73   
 74      The mode the library works in for normal operation is determined 
 75      by the store that it is given.  The store is an abstraction that 
 76      handles the data that the consumer needs to manage between http 
 77      requests in order to operate efficiently and securely. 
 78   
 79      Several store implementation are provided, and the interface is 
 80      fully documented so that custom stores can be used as well.  See 
 81      the documentation for the C{L{Consumer}} class for more 
 82      information on the interface for stores.  The implementations that 
 83      are provided allow the consumer site to store the necessary data 
 84      in several different ways, including several SQL databases and 
 85      normal files on disk. 
 86   
 87      There is an additional concrete store provided that puts the 
 88      system in dumb mode.  This is not recommended, as it removes the 
 89      library's ability to stop replay attacks reliably.  It still uses 
 90      time-based checking to make replay attacks only possible within a 
 91      small window, but they remain possible within that window.  This 
 92      store should only be used if the consumer site has no way to 
 93      retain data between requests at all. 
 94   
 95   
 96  IMMEDIATE MODE 
 97  ============== 
 98   
 99      In the flow described above, the user may need to confirm to the 
100      identity server that it's ok to authorize his or her identity. 
101      The server may draw pages asking for information from the user 
102      before it redirects the browser back to the consumer's site.  This 
103      is generally transparent to the consumer site, so it is typically 
104      ignored as an implementation detail. 
105   
106      There can be times, however, where the consumer site wants to get 
107      a response immediately.  When this is the case, the consumer can 
108      put the library in immediate mode.  In immediate mode, there is an 
109      extra response possible from the server, which is essentially the 
110      server reporting that it doesn't have enough information to answer 
111      the question yet.  In addition to saying that, the identity server 
112      provides a URL to which the user can be sent to provide the needed 
113      information and let the server finish handling the original 
114      request. 
115   
116   
117  USING THIS LIBRARY 
118  ================== 
119   
120      Integrating this library into an application is usually a 
121      relatively straightforward process.  The process should basically 
122      follow this plan: 
123   
124      Add an OpenID login field somewhere on your site.  When an OpenID 
125      is entered in that field and the form is submitted, it should make 
126      a request to the your site which includes that OpenID URL. 
127   
128      First, the application should instantiate the C{L{Consumer}} class 
129      using the store of choice.  If the application has any sort of 
130      session framework that provides per-client state management, a 
131      dict-like object to access the session should be passed as the 
132      optional second parameter.  The library just expects the session 
133      object to support a C{dict}-like interface, if it is provided. 
134   
135      Next, the application should call the 'begin' method on the 
136      C{L{Consumer}} instance.  This method takes the OpenID URL.  The 
137      C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}} 
138      object. 
139   
140      Next, the application should call the 
141      C{L{redirectURL<AuthRequest.redirectURL>}} method on the 
142      C{L{AuthRequest}} object.  The parameter C{return_to} is the URL 
143      that the OpenID server will send the user back to after attempting 
144      to verify his or her identity.  The C{trust_root} parameter is the 
145      URL (or URL pattern) that identifies your web site to the user 
146      when he or she is authorizing it.  Send a redirect to the 
147      resulting URL to the user's browser. 
148   
149      That's the first half of the authentication process.  The second 
150      half of the process is done after the user's ID server sends the 
151      user's browser a redirect back to your site to complete their 
152      login. 
153   
154      When that happens, the user will contact your site at the URL 
155      given as the C{return_to} URL to the 
156      C{L{redirectURL<AuthRequest.redirectURL>}} call made 
157      above.  The request will have several query parameters added to 
158      the URL by the identity server as the information necessary to 
159      finish the request. 
160   
161      Get an C{L{Consumer}} instance, and call its 
162      C{L{complete<Consumer.complete>}} method, passing in all the 
163      received query arguments. 
164   
165      There are multiple possible return types possible from that 
166      method. These indicate the whether or not the login was 
167      successful, and include any additional information appropriate for 
168      their type. 
169   
170  @var SUCCESS: constant used as the status for 
171      L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects. 
172   
173  @var FAILURE: constant used as the status for 
174      L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects. 
175   
176  @var CANCEL: constant used as the status for 
177      L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects. 
178   
179  @var SETUP_NEEDED: constant used as the status for 
180      L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>} 
181      objects. 
182  """ 
183   
184  import string 
185  import time 
186  import urllib 
187  import cgi 
188  from urlparse import urlparse 
189   
190  from urljr import fetchers 
191   
192  from openid.consumer.discover import discover as openIDDiscover 
193  from openid.consumer.discover import discoverXRI 
194  from openid.consumer.discover import yadis_available, DiscoveryFailure 
195  from openid import cryptutil 
196  from openid import kvform 
197  from openid import oidutil 
198  from openid.association import Association 
199  from openid.dh import DiffieHellman 
200   
201  __all__ = ['AuthRequest', 'Consumer', 'SuccessResponse', 
202             'SetupNeededResponse', 'CancelResponse', 'FailureResponse', 
203             'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED', 
204             ] 
205   
206  if yadis_available: 
207      from yadis.manager import Discovery 
208      from yadis import xri 
209   
210 -class Consumer(object):
211 """An OpenID consumer implementation that performs discovery and 212 does session management. 213 214 @ivar consumer: an instance of an object implementing the OpenID 215 protocol, but doing no discovery or session management. 216 217 @type consumer: GenericConsumer 218 219 @ivar session: A dictionary-like object representing the user's 220 session data. This is used for keeping state of the OpenID 221 transaction when the user is redirected to the server. 222 223 @cvar session_key_prefix: A string that is prepended to session 224 keys to ensure that they are unique. This variable may be 225 changed to suit your application. 226 """ 227 session_key_prefix = "_openid_consumer_" 228 229 _token = 'last_token' 230
231 - def __init__(self, session, store):
232 """Initialize a Consumer instance. 233 234 You should create a new instance of the Consumer object with 235 every HTTP request that handles OpenID transactions. 236 237 @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>} 238 239 @param store: an object that implements the interface in 240 C{L{openid.store.interface.OpenIDStore}}. Several 241 implementations are provided, to cover common database 242 environments. 243 244 @type store: C{L{openid.store.interface.OpenIDStore}} 245 246 @see: L{openid.store.interface} 247 @see: L{openid.store} 248 """ 249 self.session = session 250 self.consumer = GenericConsumer(store) 251 self._token_key = self.session_key_prefix + self._token
252
253 - def begin(self, user_url):
254 """Start the OpenID authentication process. See steps 1-2 in 255 the overview at the top of this file. 256 257 @param user_url: Identity URL given by the user. This method 258 performs a textual transformation of the URL to try and 259 make sure it is normalized. For example, a user_url of 260 example.com will be normalized to http://example.com/ 261 normalizing and resolving any redirects the server might 262 issue. 263 264 @type user_url: str 265 266 @returns: An object containing the discovered information will 267 be returned, with a method for building a redirect URL to 268 the server, as described in step 3 of the overview. This 269 object may also be used to add extension arguments to the 270 request, using its 271 L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>} 272 method. 273 274 @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>} 275 276 @raises openid.consumer.discover.DiscoveryFailure: when I fail to 277 find an OpenID server for this URL. If the C{yadis} package 278 is available, L{openid.consumer.discover.DiscoveryFailure} is 279 an alias for C{yadis.discover.DiscoveryFailure}. 280 """ 281 if yadis_available and xri.identifierScheme(user_url) == "XRI": 282 discoverMethod = discoverXRI 283 openid_url = user_url 284 else: 285 discoverMethod = openIDDiscover 286 openid_url = oidutil.normalizeUrl(user_url) 287 288 if yadis_available: 289 try: 290 disco = Discovery(self.session, 291 openid_url, 292 self.session_key_prefix) 293 service = disco.getNextService(discoverMethod) 294 except fetchers.HTTPFetchingError, e: 295 raise DiscoveryFailure('Error fetching XRDS document', e) 296 else: 297 # XXX - Untested branch! 298 _, services = openIDDiscover(user_url) 299 if not services: 300 service = None 301 else: 302 service = services[0] 303 304 if service is None: 305 raise DiscoveryFailure( 306 'No usable OpenID services found for %s' % (openid_url,), None) 307 else: 308 return self.beginWithoutDiscovery(service)
309
310 - def beginWithoutDiscovery(self, service):
311 """Start OpenID verification without doing OpenID server 312 discovery. This method is used internally by Consumer.begin 313 after discovery is performed, and exists to provide an 314 interface for library users needing to perform their own 315 discovery. 316 317 @param service: an OpenID service endpoint descriptor. This 318 object and factories for it are found in the 319 L{openid.consumer.discover} module. 320 321 @type service: 322 L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>} 323 324 @returns: an OpenID authentication request object. 325 326 @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>} 327 328 @See: Openid.consumer.consumer.Consumer.begin 329 @see: openid.consumer.discover 330 """ 331 auth_req = self.consumer.begin(service) 332 self.session[self._token_key] = auth_req.endpoint 333 return auth_req
334
335 - def complete(self, query):
336 """Called to interpret the server's response to an OpenID 337 request. It is called in step 4 of the flow described in the 338 consumer overview. 339 340 @param query: A dictionary of the query parameters for this 341 HTTP request. 342 343 @returns: a subclass of Response. The type of response is 344 indicated by the status attribute, which will be one of 345 SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. 346 347 @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} 348 @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>} 349 @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>} 350 @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>} 351 """ 352 353 endpoint = self.session.get(self._token_key) 354 if endpoint is None: 355 response = FailureResponse(None, 'No session state found') 356 else: 357 response = self.consumer.complete(query, endpoint) 358 del self.session[self._token_key] 359 360 if (response.status in ['success', 'cancel'] and 361 yadis_available and 362 response.identity_url is not None): 363 364 disco = Discovery(self.session, 365 response.identity_url, 366 self.session_key_prefix) 367 # This is OK to do even if we did not do discovery in 368 # the first place. 369 disco.cleanup() 370 371 return response
372
373 -class DiffieHellmanConsumerSession(object):
374 session_type = 'DH-SHA1' 375
376 - def __init__(self, dh=None):
377 if dh is None: 378 dh = DiffieHellman.fromDefaults() 379 380 self.dh = dh
381
382 - def getRequest(self):
383 cpub = cryptutil.longToBase64(self.dh.public) 384 385 args = {'openid.dh_consumer_public': cpub} 386 387 if not self.dh.usingDefaultValues(): 388 args.update({ 389 'openid.dh_modulus': cryptutil.longToBase64(self.dh.modulus), 390 'openid.dh_gen': cryptutil.longToBase64(self.dh.generator), 391 }) 392 393 return args
394
395 - def extractSecret(self, response):
396 spub = cryptutil.base64ToLong(response['dh_server_public']) 397 enc_mac_key = oidutil.fromBase64(response['enc_mac_key']) 398 return self.dh.xorSecret(spub, enc_mac_key)
399
400 -class PlainTextConsumerSession(object):
401 session_type = None 402
403 - def getRequest(self):
404 return {}
405
406 - def extractSecret(self, response):
407 return oidutil.fromBase64(response['mac_key'])
408
409 -class GenericConsumer(object):
410 """This is the implementation of the common logic for OpenID 411 consumers. It is unaware of the application in which it is 412 running. 413 """ 414 415 NONCE_LEN = 8 416 NONCE_CHRS = string.ascii_letters + string.digits 417
418 - def __init__(self, store):
419 self.store = store
420
421 - def begin(self, service_endpoint):
422 nonce = self._createNonce() 423 assoc = self._getAssociation(service_endpoint.server_url) 424 request = AuthRequest(service_endpoint, assoc) 425 request.return_to_args['nonce'] = nonce 426 return request
427
428 - def complete(self, query, endpoint):
429 mode = query.get('openid.mode', '<no mode specified>') 430 431 if isinstance(mode, list): 432 raise TypeError("query dict must have one value for each key, " 433 "not lists of values. Query is %r" % (query,)) 434 435 if mode == 'cancel': 436 return CancelResponse(endpoint) 437 elif mode == 'error': 438 error = query.get('openid.error') 439 return FailureResponse(endpoint, error) 440 elif mode == 'id_res': 441 if endpoint.identity_url is None: 442 return FailureResponse(endpoint, 'No session state found') 443 try: 444 response = self._doIdRes(query, endpoint) 445 except fetchers.HTTPFetchingError, why: 446 message = 'HTTP request failed: %s' % (str(why),) 447 return FailureResponse(endpoint, message) 448 else: 449 if response.status == 'success': 450 return self._checkNonce(response, query.get('nonce')) 451 else: 452 return response 453 else: 454 return FailureResponse(endpoint, 455 'Invalid openid.mode: %r' % (mode,))
456
457 - def _checkNonce(self, response, nonce):
458 parsed_url = urlparse(response.getReturnTo()) 459 query = parsed_url[4] 460 for k, v in cgi.parse_qsl(query): 461 if k == 'nonce': 462 if v != nonce: 463 return FailureResponse(response, 'Nonce mismatch') 464 else: 465 break 466 else: 467 return FailureResponse(response, 'Nonce missing from return_to: %r' 468 % (response.getReturnTo())) 469 470 # The nonce matches the signed nonce in the openid.return_to 471 # response parameter 472 if not self.store.useNonce(nonce): 473 return FailureResponse(response, 474 'Nonce missing from store') 475 476 # If the nonce check succeeded, return the original success 477 # response 478 return response
479
480 - def _createNonce(self):
481 nonce = cryptutil.randomString(self.NONCE_LEN, self.NONCE_CHRS) 482 self.store.storeNonce(nonce) 483 return nonce
484
485 - def _makeKVPost(self, args, server_url):
486 mode = args['openid.mode'] 487 body = urllib.urlencode(args) 488 489 resp = fetchers.fetch(server_url, body=body) 490 if resp is None: 491 fmt = 'openid.mode=%s: failed to fetch URL: %s' 492 oidutil.log(fmt % (mode, server_url)) 493 return None 494 495 response = kvform.kvToDict(resp.body) 496 if resp.status == 400: 497 server_error = response.get('error', '<no message from server>') 498 fmt = 'openid.mode=%s: error returned from server %s: %s' 499 oidutil.log(fmt % (mode, server_url, server_error)) 500 return None 501 elif resp.status != 200: 502 fmt = 'openid.mode=%s: bad status code from server %s: %s' 503 oidutil.log(fmt % (mode, server_url, resp.status)) 504 return None 505 506 return response
507
508 - def _doIdRes(self, query, endpoint):
509 """Handle id_res responses. 510 511 @param query: the response paramaters. 512 @param consumer_id: The normalized Claimed Identifier. 513 @param server_id: The Delegate Identifier. 514 @param server_url: OpenID server endpoint URL. 515 516 @returntype: L{Response} 517 """ 518 user_setup_url = query.get('openid.user_setup_url') 519 if user_setup_url is not None: 520 return SetupNeededResponse(endpoint, user_setup_url) 521 522 return_to = query.get('openid.return_to') 523 server_id2 = query.get('openid.identity') 524 assoc_handle = query.get('openid.assoc_handle') 525 526 if return_to is None or server_id2 is None or assoc_handle is None: 527 return FailureResponse(endpoint, 'Missing required field') 528 529 if endpoint.getServerID() != server_id2: 530 return FailureResponse(endpoint, 'Server ID (delegate) mismatch') 531 532 signed = query.get('openid.signed') 533 534 assoc = self.store.getAssociation(endpoint.server_url, assoc_handle) 535 536 if assoc is None: 537 # It's not an association we know about. Dumb mode is our 538 # only possible path for recovery. 539 if self._checkAuth(query, endpoint.server_url): 540 return SuccessResponse.fromQuery(endpoint, query, signed) 541 else: 542 return FailureResponse(endpoint, 543 'Server denied check_authentication') 544 545 if assoc.expiresIn <= 0: 546 # XXX: It might be a good idea sometimes to re-start the 547 # authentication with a new association. Doing it 548 # automatically opens the possibility for 549 # denial-of-service by a server that just returns expired 550 # associations (or really short-lived associations) 551 msg = 'Association with %s expired' % (endpoint.server_url,) 552 return FailureResponse(endpoint, msg) 553 554 # Check the signature 555 sig = query.get('openid.sig') 556 if sig is None or signed is None: 557 return FailureResponse(endpoint, 'Missing argument signature') 558 559 signed_list = signed.split(',') 560 561 # Fail if the identity field is present but not signed 562 if endpoint.identity_url is not None and 'identity' not in signed_list: 563 msg = '"openid.identity" not signed' 564 return FailureResponse(endpoint, msg) 565 566 v_sig = assoc.signDict(signed_list, query) 567 568 if v_sig != sig: 569 return FailureResponse(endpoint, 'Bad signature') 570 571 return SuccessResponse.fromQuery(endpoint, query, signed)
572
573 - def _checkAuth(self, query, server_url):
574 request = self._createCheckAuthRequest(query) 575 if request is None: 576 return False 577 response = self._makeKVPost(request, server_url) 578 if response is None: 579 return False 580 return self._processCheckAuthResponse(response, server_url)
581
582 - def _createCheckAuthRequest(self, query):
583 signed = query.get('openid.signed') 584 if signed is None: 585 oidutil.log('No signature present; checkAuth aborted') 586 return None 587 588 # Arguments that are always passed to the server and not 589 # included in the signature. 590 whitelist = ['assoc_handle', 'sig', 'signed', 'invalidate_handle'] 591 signed = signed.split(',') + whitelist 592 593 check_args = dict([(k, v) for k, v in query.iteritems() 594 if k.startswith('openid.') and k[7:] in signed]) 595 596 check_args['openid.mode'] = 'check_authentication' 597 return check_args
598
599 - def _processCheckAuthResponse(self, response, server_url):
600 is_valid = response.get('is_valid', 'false') 601 602 invalidate_handle = response.get('invalidate_handle') 603 if invalidate_handle is not None: 604 self.store.removeAssociation(server_url, invalidate_handle) 605 606 if is_valid == 'true': 607 return True 608 else: 609 oidutil.log('Server responds that checkAuth call is not valid') 610 return False
611
612 - def _getAssociation(self, server_url):
613 if self.store.isDumb(): 614 return None 615 616 assoc = self.store.getAssociation(server_url) 617 618 if assoc is None or assoc.expiresIn <= 0: 619 assoc_session, args = self._createAssociateRequest(server_url) 620 try: 621 response = self._makeKVPost(args, server_url) 622 except fetchers.HTTPFetchingError, why: 623 oidutil.log('openid.associate request failed: %s' % 624 (str(why),)) 625 assoc = None 626 else: 627 assoc = self._parseAssociation( 628 response, assoc_session, server_url) 629 630 return assoc
631
632 - def _createAssociateRequest(self, server_url):
633 proto = urlparse(server_url)[0] 634 if proto == 'https': 635 session_type = PlainTextConsumerSession 636 else: 637 session_type = DiffieHellmanConsumerSession 638 639 assoc_session = session_type() 640 641 args = { 642 'openid.mode': 'associate', 643 'openid.assoc_type':'HMAC-SHA1', 644 } 645 646 if assoc_session.session_type is not None: 647 args['openid.session_type'] = assoc_session.session_type 648 649 args.update(assoc_session.getRequest()) 650 return assoc_session, args
651
652 - def _parseAssociation(self, results, assoc_session, server_url):
653 try: 654 assoc_type = results['assoc_type'] 655 assoc_handle = results['assoc_handle'] 656 expires_in_str = results['expires_in'] 657 except KeyError, e: 658 fmt = 'Getting association: missing key in response from %s: %s' 659 oidutil.log(fmt % (server_url, e[0])) 660 return None 661 662 if assoc_type != 'HMAC-SHA1': 663 fmt = 'Unsupported assoc_type returned from server %s: %s' 664 oidutil.log(fmt % (server_url, assoc_type)) 665 return None 666 667 try: 668 expires_in = int(expires_in_str) 669 except ValueError, e: 670 fmt = 'Getting Association: invalid expires_in field: %s' 671 oidutil.log(fmt % (e[0],)) 672 return None 673 674 session_type = results.get('session_type') 675 if session_type != assoc_session.session_type: 676 if session_type is None: 677 oidutil.log('Falling back to plain text association ' 678 'session from %s' % assoc_session.session_type) 679 assoc_session = PlainTextConsumerSession() 680 else: 681 oidutil.log('Session type mismatch. Expected %r, got %r' % 682 (assoc_session.session_type, session_type)) 683 return None 684 685 try: 686 secret = assoc_session.extractSecret(results) 687 except ValueError, why: 688 oidutil.log('Malformed response for %s session: %s' % ( 689 assoc_session.session_type, why[0])) 690 return None 691 except KeyError, why: 692 fmt = 'Getting association: missing key in response from %s: %s' 693 oidutil.log(fmt % (server_url, why[0])) 694 return None 695 696 assoc = Association.fromExpiresIn( 697 expires_in, assoc_handle, secret, assoc_type) 698 self.store.storeAssociation(server_url, assoc) 699 700 return assoc
701
702 -class AuthRequest(object):
703 - def __init__(self, endpoint, assoc):
704 """ 705 Creates a new AuthRequest object. This just stores each 706 argument in an appropriately named field. 707 708 Users of this library should not create instances of this 709 class. Instances of this class are created by the library 710 when needed. 711 """ 712 self.assoc = assoc 713 self.endpoint = endpoint 714 self.extra_args = {} 715 self.return_to_args = {}
716
717 - def addExtensionArg(self, namespace, key, value):
718 """Add an extension argument to this OpenID authentication 719 request. 720 721 Use caution when adding arguments, because they will be 722 URL-escaped and appended to the redirect URL, which can easily 723 get quite long. 724 725 @param namespace: The namespace for the extension. For 726 example, the simple registration extension uses the 727 namespace C{sreg}. 728 729 @type namespace: str 730 731 @param key: The key within the extension namespace. For 732 example, the nickname field in the simple registration 733 extension's key is C{nickname}. 734 735 @type key: str 736 737 @param value: The value to provide to the server for this 738 argument. 739 740 @type value: str 741 """ 742 arg_name = '.'.join(['openid', namespace, key]) 743 self.extra_args[arg_name] = value
744
745 - def redirectURL(self, trust_root, return_to, immediate=False):
746 if immediate: 747 mode = 'checkid_immediate' 748 else: 749 mode = 'checkid_setup' 750 751 return_to = oidutil.appendArgs(return_to, self.return_to_args) 752 753 redir_args = { 754 'openid.mode': mode, 755 'openid.identity': self.endpoint.getServerID(), 756 'openid.return_to': return_to, 757 'openid.trust_root': trust_root, 758 } 759 760 if self.assoc: 761 redir_args['openid.assoc_handle'] = self.assoc.handle 762 763 redir_args.update(self.extra_args) 764 return oidutil.appendArgs(self.endpoint.server_url, redir_args)
765 766 FAILURE = 'failure' 767 SUCCESS = 'success' 768 CANCEL = 'cancel' 769 SETUP_NEEDED = 'setup_needed' 770
771 -class Response(object):
772 status = None
773
774 -class SuccessResponse(Response):
775 """A response with a status of SUCCESS. Indicates that this request is a 776 successful acknowledgement from the OpenID server that the 777 supplied URL is, indeed controlled by the requesting agent. 778 779 @ivar identity_url: The identity URL that has been authenticated 780 781 @ivar endpoint: The endpoint that authenticated the identifier. You 782 may access other discovered information related to this endpoint, 783 such as the CanonicalID of an XRI, through this object. 784 @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>} 785 786 @ivar signed_args: The arguments in the server's response that 787 were signed and verified. 788 789 @cvar status: SUCCESS 790 """ 791 792 status = SUCCESS 793
794 - def __init__(self, endpoint, signed_args):
795 self.endpoint = endpoint 796 self.identity_url = endpoint.identity_url 797 self.signed_args = signed_args
798
799 - def fromQuery(cls, endpoint, query, signed):
800 signed_args = {} 801 for field_name in signed.split(','): 802 field_name = 'openid.' + field_name 803 signed_args[field_name] = query.get(field_name, '') 804 return cls(endpoint, signed_args)
805 806 fromQuery = classmethod(fromQuery) 807
808 - def extensionResponse(self, prefix):
809 """extract signed extension data from the server's response. 810 811 @param prefix: The extension namespace from which to extract 812 the extension data. 813 """ 814 response = {} 815 prefix = 'openid.%s.' % (prefix,) 816 prefix_len = len(prefix) 817 for k, v in self.signed_args.iteritems(): 818 if k.startswith(prefix): 819 response_key = k[prefix_len:] 820 response[response_key] = v 821 822 return response
823
824 - def getReturnTo(self):
825 """Get the openid.return_to argument from this response. 826 827 This is useful for verifying that this request was initiated 828 by this consumer. 829 830 @returns: The return_to URL supplied to the server on the 831 initial request, or C{None} if the response did not contain 832 an C{openid.return_to} argument. 833 834 @returntype: str 835 """ 836 return self.signed_args.get('openid.return_to', None)
837 838 839
840 -class FailureResponse(Response):
841 """A response with a status of FAILURE. Indicates that the OpenID 842 protocol has failed. This could be locally or remotely triggered. 843 844 @ivar identity_url: The identity URL for which authenitcation was 845 attempted, if it can be determined. Otherwise, None. 846 847 @ivar message: A message indicating why the request failed, if one 848 is supplied. otherwise, None. 849 850 @cvar status: FAILURE 851 """ 852 853 status = FAILURE 854
855 - def __init__(self, endpoint, message=None):
856 self.endpoint = endpoint 857 if endpoint is not None: 858 self.identity_url = endpoint.identity_url 859 else: 860 self.identity_url = None 861 self.message = message
862 863
864 - def __repr__(self):
865 return "<%s.%s id=%r message=%r>" % ( 866 self.__class__.__module__, self.__class__.__name__, 867 self.identity_url, self.message)
868 869
870 -class CancelResponse(Response):
871 """A response with a status of CANCEL. Indicates that the user 872 cancelled the OpenID authentication request. 873 874 @ivar identity_url: The identity URL for which authenitcation was 875 attempted, if it can be determined. Otherwise, None. 876 877 @cvar status: CANCEL 878 """ 879 880 status = CANCEL 881
882 - def __init__(self, endpoint):
883 self.endpoint = endpoint 884 self.identity_url = endpoint.identity_url
885
886 -class SetupNeededResponse(Response):
887 """A response with a status of SETUP_NEEDED. Indicates that the 888 request was in immediate mode, and the server is unable to 889 authenticate the user without further interaction. 890 891 @ivar identity_url: The identity URL for which authenitcation was 892 attempted. 893 894 @ivar setup_url: A URL that can be used to send the user to the 895 server to set up for authentication. The user should be 896 redirected in to the setup_url, either in the current window 897 or in a new browser window. 898 899 @cvar status: SETUP_NEEDED 900 """ 901 902 status = SETUP_NEEDED 903
904 - def __init__(self, endpoint, setup_url=None):
905 self.endpoint = endpoint 906 self.identity_url = endpoint.identity_url 907 self.setup_url = setup_url
908