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

Source Code for Module openid.server.server

   1  # -*- test-case-name: openid.test.server -*- 
   2  """OpenID server protocol and logic. 
   3   
   4  Overview 
   5  ======== 
   6   
   7      An OpenID server must perform three tasks: 
   8   
   9          1. Examine the incoming request to determine its nature and validity. 
  10   
  11          2. Make a decision about how to respond to this request. 
  12   
  13          3. Format the response according to the protocol. 
  14   
  15      The first and last of these tasks may performed by 
  16      the L{decodeRequest<Server.decodeRequest>} and 
  17      L{encodeResponse<Server.encodeResponse>} methods of the 
  18      L{Server} object.  Who gets to do the intermediate task -- deciding 
  19      how to respond to the request -- will depend on what type of request it 
  20      is. 
  21   
  22      If it's a request to authenticate a user (a X{C{checkid_setup}} or 
  23      X{C{checkid_immediate}} request), you need to decide if you will assert 
  24      that this user may claim the identity in question.  Exactly how you do 
  25      that is a matter of application policy, but it generally involves making 
  26      sure the user has an account with your system and is logged in, checking 
  27      to see if that identity is hers to claim, and verifying with the user that 
  28      she does consent to releasing that information to the party making the 
  29      request. 
  30   
  31      Examine the properties of the L{CheckIDRequest} object, and if 
  32      and when you've come to a decision, form a response by calling 
  33      L{CheckIDRequest.answer}. 
  34   
  35      Other types of requests relate to establishing associations between client 
  36      and server and verifying the authenticity of previous communications. 
  37      L{Server} contains all the logic and data necessary to respond to 
  38      such requests; just pass it to L{Server.handleRequest}. 
  39   
  40   
  41  OpenID Extensions 
  42  ================= 
  43   
  44      Do you want to provide other information for your users 
  45      in addition to authentication?  Version 1.2 of the OpenID 
  46      protocol allows consumers to add extensions to their requests. 
  47      For example, with sites using the U{Simple Registration 
  48      Extension<http://www.openidenabled.com/openid/simple-registration-extension/>}, 
  49      a user can agree to have their nickname and e-mail address sent to a 
  50      site when they sign up. 
  51   
  52      Since extensions do not change the way OpenID authentication works, 
  53      code to handle extension requests may be completely separate from the 
  54      L{OpenIDRequest} class here.  But you'll likely want data sent back by 
  55      your extension to be signed.  L{OpenIDResponse} provides methods with 
  56      which you can add data to it which can be signed with the other data in 
  57      the OpenID signature. 
  58   
  59      For example:: 
  60   
  61          # when request is a checkid_* request 
  62          response = request.answer(True) 
  63          # this will a signed 'openid.sreg.timezone' parameter to the response 
  64          response.addField('sreg', 'timezone', 'America/Los_Angeles') 
  65   
  66   
  67  Stores 
  68  ====== 
  69   
  70      The OpenID server needs to maintain state between requests in order 
  71      to function.  Its mechanism for doing this is called a store.  The 
  72      store interface is defined in C{L{openid.store.interface.OpenIDStore}}. 
  73      Additionally, several concrete store implementations are provided, so that 
  74      most sites won't need to implement a custom store.  For a store backed 
  75      by flat files on disk, see C{L{openid.store.filestore.FileOpenIDStore}}. 
  76      For stores based on MySQL or SQLite, see the C{L{openid.store.sqlstore}} 
  77      module. 
  78   
  79   
  80  Upgrading 
  81  ========= 
  82   
  83      The keys by which a server looks up associations in its store have changed 
  84      in version 1.2 of this library.  If your store has entries created from 
  85      version 1.0 code, you should empty it. 
  86   
  87   
  88  @group Requests: OpenIDRequest, AssociateRequest, CheckIDRequest, 
  89      CheckAuthRequest 
  90   
  91  @group Responses: OpenIDResponse 
  92   
  93  @group HTTP Codes: HTTP_OK, HTTP_REDIRECT, HTTP_ERROR 
  94   
  95  @group Response Encodings: ENCODE_KVFORM, ENCODE_URL 
  96  """ 
  97   
  98  import time 
  99  from copy import deepcopy 
 100   
 101  from openid import cryptutil 
 102  from openid import kvform 
 103  from openid import oidutil 
 104  from openid.dh import DiffieHellman 
 105  from openid.server.trustroot import TrustRoot 
 106  from openid.association import Association 
 107   
 108  HTTP_OK = 200 
 109  HTTP_REDIRECT = 302 
 110  HTTP_ERROR = 400 
 111   
 112  BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate'] 
 113  OPENID_PREFIX = 'openid.' 
 114   
 115  ENCODE_KVFORM = ('kvform',) 
 116  ENCODE_URL = ('URL/redirect',) 
 117   
118 -class OpenIDRequest(object):
119 """I represent an incoming OpenID request. 120 121 @cvar mode: the C{X{openid.mode}} of this request. 122 @type mode: str 123 """ 124 mode = None
125 126
127 -class CheckAuthRequest(OpenIDRequest):
128 """A request to verify the validity of a previous response. 129 130 @cvar mode: "X{C{check_authentication}}" 131 @type mode: str 132 133 @ivar assoc_handle: The X{association handle} the response was signed with. 134 @type assoc_handle: str 135 @ivar sig: The signature to check. 136 @type sig: str 137 @ivar signed: The ordered list of signed items you want to check. 138 @type signed: list of pairs 139 140 @ivar invalidate_handle: An X{association handle} the client is asking 141 about the validity of. Optional, may be C{None}. 142 @type invalidate_handle: str 143 144 @see: U{OpenID Specs, Mode: check_authentication 145 <http://openid.net/specs.bml#mode-check_authentication>} 146 """ 147 mode = "check_authentication" 148 149
150 - def __init__(self, assoc_handle, sig, signed, invalidate_handle=None):
151 """Construct me. 152 153 These parameters are assigned directly as class attributes, see 154 my L{class documentation<CheckAuthRequest>} for their descriptions. 155 156 @type assoc_handle: str 157 @type sig: str 158 @type signed: list of pairs 159 @type invalidate_handle: str 160 """ 161 self.assoc_handle = assoc_handle 162 self.sig = sig 163 self.signed = signed 164 self.invalidate_handle = invalidate_handle
165 166
167 - def fromQuery(klass, query):
168 """Construct me from a web query. 169 170 @param query: The query parameters as a dictionary with each 171 key mapping to one value. 172 @type query: dict 173 174 @returntype: L{CheckAuthRequest} 175 """ 176 self = klass.__new__(klass) 177 try: 178 self.assoc_handle = query[OPENID_PREFIX + 'assoc_handle'] 179 self.sig = query[OPENID_PREFIX + 'sig'] 180 signed_list = query[OPENID_PREFIX + 'signed'] 181 except KeyError, e: 182 raise ProtocolError(query, 183 text="%s request missing required parameter %s" 184 " from query %s" % 185 (self.mode, e.args[0], query)) 186 187 self.invalidate_handle = query.get(OPENID_PREFIX + 'invalidate_handle') 188 189 signed_list = signed_list.split(',') 190 signed_pairs = [] 191 for field in signed_list: 192 try: 193 if field == 'mode': 194 # XXX KLUDGE HAX WEB PROTOCoL BR0KENNN 195 # openid.mode is currently check_authentication because 196 # that's the mode of this request. But the signature 197 # was made on something with a different openid.mode. 198 # http://article.gmane.org/gmane.comp.web.openid.general/537 199 value = "id_res" 200 else: 201 value = query[OPENID_PREFIX + field] 202 except KeyError, e: 203 raise ProtocolError( 204 query, 205 text="Couldn't find signed field %r in query %s" 206 % (field, query)) 207 else: 208 signed_pairs.append((field, value)) 209 210 self.signed = signed_pairs 211 return self
212 213 fromQuery = classmethod(fromQuery) 214 215
216 - def answer(self, signatory):
217 """Respond to this request. 218 219 Given a L{Signatory}, I can check the validity of the signature and 220 the X{C{invalidate_handle}}. 221 222 @param signatory: The L{Signatory} to use to check the signature. 223 @type signatory: L{Signatory} 224 225 @returns: A response with an X{C{is_valid}} (and, if 226 appropriate X{C{invalidate_handle}}) field. 227 @returntype: L{OpenIDResponse} 228 """ 229 is_valid = signatory.verify(self.assoc_handle, self.sig, self.signed) 230 # Now invalidate that assoc_handle so it this checkAuth message cannot 231 # be replayed. 232 signatory.invalidate(self.assoc_handle, dumb=True) 233 response = OpenIDResponse(self) 234 response.fields['is_valid'] = (is_valid and "true") or "false" 235 236 if self.invalidate_handle: 237 assoc = signatory.getAssociation(self.invalidate_handle, dumb=False) 238 if not assoc: 239 response.fields['invalidate_handle'] = self.invalidate_handle 240 return response
241 242
243 - def __str__(self):
244 if self.invalidate_handle: 245 ih = " invalidate? %r" % (self.invalidate_handle,) 246 else: 247 ih = "" 248 s = "<%s handle: %r sig: %r: signed: %r%s>" % ( 249 self.__class__.__name__, self.assoc_handle, 250 self.sig, self.signed, ih) 251 return s
252 253
254 -class PlainTextServerSession(object):
255 """An object that knows how to handle association requests with no 256 session type. 257 258 @cvar session_type: The session_type for this association 259 session. There is no type defined for plain-text in the OpenID 260 specification, so we use 'plaintext'. 261 @type session_type: str 262 263 @see: U{OpenID Specs, Mode: associate 264 <http://openid.net/specs.bml#mode-associate>} 265 @see: AssociateRequest 266 """ 267 session_type = 'plaintext' 268
269 - def fromQuery(cls, unused_request):
270 return cls()
271 272 fromQuery = classmethod(fromQuery) 273
274 - def answer(self, secret):
275 return {'mac_key': oidutil.toBase64(secret)}
276 277
278 -class DiffieHellmanServerSession(object):
279 """An object that knows how to handle association requests with the 280 Diffie-Hellman session type. 281 282 @cvar session_type: The session_type for this association 283 session. 284 @type session_type: str 285 286 @ivar dh: The Diffie-Hellman algorithm values for this request 287 @type dh: DiffieHellman 288 289 @ivar consumer_pubkey: The public key sent by the consumer in the 290 associate request 291 @type consumer_pubkey: long 292 293 @see: U{OpenID Specs, Mode: associate 294 <http://openid.net/specs.bml#mode-associate>} 295 @see: AssociateRequest 296 """ 297 session_type = 'DH-SHA1' 298
299 - def __init__(self, dh, consumer_pubkey):
300 self.dh = dh 301 self.consumer_pubkey = consumer_pubkey
302
303 - def fromQuery(cls, query):
304 """ 305 @param query: The associate request's query parameters 306 @type query: {str:str} 307 308 @returntype: L{DiffieHellmanServerSession} 309 310 @raises ProtocolError: When parameters required to establish the 311 session are missing. 312 """ 313 dh_modulus = query.get('openid.dh_modulus') 314 dh_gen = query.get('openid.dh_gen') 315 if (dh_modulus is None and dh_gen is not None or 316 dh_gen is None and dh_modulus is not None): 317 318 if dh_modulus is None: 319 missing = 'modulus' 320 else: 321 missing = 'generator' 322 323 raise ProtocolError('If non-default modulus or generator is ' 324 'supplied, both must be supplied. Missing %s' 325 % (missing,)) 326 327 if dh_modulus or dh_gen: 328 dh_modulus = cryptutil.base64ToLong(dh_modulus) 329 dh_gen = cryptutil.base64ToLong(dh_gen) 330 dh = DiffieHellman(dh_modulus, dh_gen) 331 else: 332 dh = DiffieHellman.fromDefaults() 333 334 consumer_pubkey = query.get('openid.dh_consumer_public') 335 if consumer_pubkey is None: 336 raise ProtocolError("Public key for DH-SHA1 session " 337 "not found in query %s" % (query,)) 338 339 consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey) 340 341 return cls(dh, consumer_pubkey)
342 343 fromQuery = classmethod(fromQuery) 344
345 - def answer(self, secret):
346 mac_key = self.dh.xorSecret(self.consumer_pubkey, secret) 347 return { 348 'dh_server_public': cryptutil.longToBase64(self.dh.public), 349 'enc_mac_key': oidutil.toBase64(mac_key), 350 }
351 352
353 -class AssociateRequest(OpenIDRequest):
354 """A request to establish an X{association}. 355 356 @cvar mode: "X{C{check_authentication}}" 357 @type mode: str 358 359 @ivar assoc_type: The type of association. The protocol currently only 360 defines one value for this, "X{C{HMAC-SHA1}}". 361 @type assoc_type: str 362 363 @ivar session: An object that knows how to handle association 364 requests of a certain type. 365 366 @see: U{OpenID Specs, Mode: associate 367 <http://openid.net/specs.bml#mode-associate>} 368 """ 369 370 mode = "associate" 371 assoc_type = 'HMAC-SHA1' 372 373 session_classes = { 374 None: PlainTextServerSession, 375 'DH-SHA1': DiffieHellmanServerSession, 376 } 377
378 - def __init__(self, session):
379 """Construct me. 380 381 The session is assigned directly as a class attribute. See my 382 L{class documentation<AssociateRequest>} for its description. 383 """ 384 super(AssociateRequest, self).__init__() 385 self.session = session
386 387
388 - def fromQuery(klass, query):
389 """Construct me from a web query. 390 391 @param query: The query parameters as a dictionary with each 392 key mapping to one value. 393 @type query: dict 394 395 @returntype: L{AssociateRequest} 396 """ 397 session_type = query.get(OPENID_PREFIX + 'session_type') 398 try: 399 session_class = klass.session_classes[session_type] 400 except KeyError: 401 raise ProtocolError(query, 402 "Unknown session type %r" % (session_type,)) 403 404 try: 405 session = session_class.fromQuery(query) 406 except ValueError, why: 407 raise ProtocolError(query, 'Error parsing %s session: %s' % 408 (session_class.session_type, why[0])) 409 410 return klass(session)
411 412 fromQuery = classmethod(fromQuery) 413
414 - def answer(self, assoc):
415 """Respond to this request with an X{association}. 416 417 @param assoc: The association to send back. 418 @type assoc: L{openid.association.Association} 419 420 @returns: A response with the association information, encrypted 421 to the consumer's X{public key} if appropriate. 422 @returntype: L{OpenIDResponse} 423 """ 424 response = OpenIDResponse(self) 425 response.fields.update({ 426 'expires_in': '%d' % (assoc.getExpiresIn(),), 427 'assoc_type': 'HMAC-SHA1', 428 'assoc_handle': assoc.handle, 429 }) 430 response.fields.update(self.session.answer(assoc.secret)) 431 if self.session.session_type != 'plaintext': 432 response.fields['session_type'] = self.session.session_type 433 434 return response
435 436
437 -class CheckIDRequest(OpenIDRequest):
438 """A request to confirm the identity of a user. 439 440 This class handles requests for openid modes X{C{checkid_immediate}} 441 and X{C{checkid_setup}}. 442 443 @cvar mode: "X{C{checkid_immediate}}" or "X{C{checkid_setup}}" 444 @type mode: str 445 446 @ivar immediate: Is this an immediate-mode request? 447 @type immediate: bool 448 449 @ivar identity: The identity URL being checked. 450 @type identity: str 451 452 @ivar trust_root: "Are you Frank?" asks the checkid request. "Who wants 453 to know?" C{trust_root}, that's who. This URL identifies the party 454 making the request, and the user will use that to make her decision 455 about what answer she trusts them to have. 456 @type trust_root: str 457 458 @ivar return_to: The URL to send the user agent back to to reply to this 459 request. 460 @type return_to: str 461 462 @ivar assoc_handle: Provided in smart mode requests, a handle for a 463 previously established association. C{None} for dumb mode requests. 464 @type assoc_handle: str 465 """ 466
467 - def __init__(self, identity, return_to, trust_root=None, immediate=False, 468 assoc_handle=None):
469 """Construct me. 470 471 These parameters are assigned directly as class attributes, see 472 my L{class documentation<CheckIDRequest>} for their descriptions. 473 474 @raises MalformedReturnURL: When the C{return_to} URL is not a URL. 475 """ 476 self.assoc_handle = assoc_handle 477 self.identity = identity 478 self.return_to = return_to 479 self.trust_root = trust_root or return_to 480 if immediate: 481 self.immediate = True 482 self.mode = "checkid_immediate" 483 else: 484 self.immediate = False 485 self.mode = "checkid_setup" 486 487 if not TrustRoot.parse(self.return_to): 488 raise MalformedReturnURL(None, self.return_to) 489 if not self.trustRootValid(): 490 raise UntrustedReturnURL(None, self.return_to, self.trust_root)
491 492
493 - def fromQuery(klass, query):
494 """Construct me from a web query. 495 496 @raises ProtocolError: When not all required parameters are present 497 in the query. 498 499 @raises MalformedReturnURL: When the C{return_to} URL is not a URL. 500 501 @raises UntrustedReturnURL: When the C{return_to} URL is outside 502 the C{trust_root}. 503 504 @param query: The query parameters as a dictionary with each 505 key mapping to one value. 506 @type query: dict 507 508 @returntype: L{CheckIDRequest} 509 """ 510 self = klass.__new__(klass) 511 mode = query[OPENID_PREFIX + 'mode'] 512 if mode == "checkid_immediate": 513 self.immediate = True 514 self.mode = "checkid_immediate" 515 else: 516 self.immediate = False 517 self.mode = "checkid_setup" 518 519 required = [ 520 'identity', 521 'return_to', 522 ] 523 524 for field in required: 525 value = query.get(OPENID_PREFIX + field) 526 if not value: 527 raise ProtocolError( 528 query, 529 text="Missing required field %s from %r" 530 % (field, query)) 531 setattr(self, field, value) 532 533 # There's a case for making self.trust_root be a TrustRoot 534 # here. But if TrustRoot isn't currently part of the "public" API, 535 # I'm not sure it's worth doing. 536 self.trust_root = query.get(OPENID_PREFIX + 'trust_root', self.return_to) 537 self.assoc_handle = query.get(OPENID_PREFIX + 'assoc_handle') 538 539 # Using TrustRoot.parse here is a bit misleading, as we're not 540 # parsing return_to as a trust root at all. However, valid URLs 541 # are valid trust roots, so we can use this to get an idea if it 542 # is a valid URL. Not all trust roots are valid return_to URLs, 543 # however (particularly ones with wildcards), so this is still a 544 # little sketchy. 545 if not TrustRoot.parse(self.return_to): 546 raise MalformedReturnURL(query, self.return_to) 547 548 # I first thought that checking to see if the return_to is within 549 # the trust_root is premature here, a logic-not-decoding thing. But 550 # it was argued that this is really part of data validation. A 551 # request with an invalid trust_root/return_to is broken regardless of 552 # application, right? 553 if not self.trustRootValid(): 554 raise UntrustedReturnURL(query, self.return_to, self.trust_root) 555 556 return self
557 558 fromQuery = classmethod(fromQuery) 559 560
561 - def trustRootValid(self):
562 """Is my return_to under my trust_root? 563 564 @returntype: bool 565 """ 566 if not self.trust_root: 567 return True 568 tr = TrustRoot.parse(self.trust_root) 569 if tr is None: 570 raise MalformedTrustRoot(None, self.trust_root) 571 return tr.validateURL(self.return_to)
572 573
574 - def answer(self, allow, server_url=None):
575 """Respond to this request. 576 577 @param allow: Allow this user to claim this identity, and allow the 578 consumer to have this information? 579 @type allow: bool 580 581 @param server_url: When an immediate mode request does not 582 succeed, it gets back a URL where the request may be 583 carried out in a not-so-immediate fashion. Pass my URL 584 in here (the fully qualified address of this server's 585 endpoint, i.e. C{http://example.com/server}), and I 586 will use it as a base for the URL for a new request. 587 588 Optional for requests where C{CheckIDRequest.immediate} is C{False} 589 or C{allow} is C{True}. 590 591 @type server_url: str 592 593 @returntype: L{OpenIDResponse} 594 """ 595 if allow or self.immediate: 596 mode = 'id_res' 597 else: 598 mode = 'cancel' 599 600 response = OpenIDResponse(self) 601 602 if allow: 603 response.addFields(None, { 604 'mode': mode, 605 'identity': self.identity, 606 'return_to': self.return_to, 607 }) 608 else: 609 response.addField(None, 'mode', mode, False) 610 if self.immediate: 611 if not server_url: 612 raise ValueError("setup_url is required for allow=False " 613 "in immediate mode.") 614 # Make a new request just like me, but with immediate=False. 615 setup_request = self.__class__( 616 self.identity, self.return_to, self.trust_root, 617 immediate=False, assoc_handle=self.assoc_handle) 618 setup_url = setup_request.encodeToURL(server_url) 619 response.addField(None, 'user_setup_url', setup_url, False) 620 621 return response
622 623
624 - def encodeToURL(self, server_url):
625 """Encode this request as a URL to GET. 626 627 @param server_url: The URL of the OpenID server to make this request of. 628 @type server_url: str 629 630 @returntype: str 631 """ 632 # Imported from the alternate reality where these classes are used 633 # in both the client and server code, so Requests are Encodable too. 634 # That's right, code imported from alternate realities all for the 635 # love of you, id_res/user_setup_url. 636 q = {'mode': self.mode, 637 'identity': self.identity, 638 'return_to': self.return_to} 639 if self.trust_root: 640 q['trust_root'] = self.trust_root 641 if self.assoc_handle: 642 q['assoc_handle'] = self.assoc_handle 643 644 q = dict([(OPENID_PREFIX + k, v) for k, v in q.iteritems()]) 645 646 return oidutil.appendArgs(server_url, q)
647 648
649 - def getCancelURL(self):
650 """Get the URL to cancel this request. 651 652 Useful for creating a "Cancel" button on a web form so that operation 653 can be carried out directly without another trip through the server. 654 655 (Except you probably want to make another trip through the server so 656 that it knows that the user did make a decision. Or you could simulate 657 this method by doing C{.answer(False).encodeToURL()}) 658 659 @returntype: str 660 @returns: The return_to URL with openid.mode = cancel. 661 """ 662 if self.immediate: 663 raise ValueError("Cancel is not an appropriate response to " 664 "immediate mode requests.") 665 return oidutil.appendArgs(self.return_to, {OPENID_PREFIX + 'mode': 666 'cancel'})
667 668
669 - def __str__(self):
670 return '<%s id:%r im:%s tr:%r ah:%r>' % (self.__class__.__name__, 671 self.identity, 672 self.immediate, 673 self.trust_root, 674 self.assoc_handle)
675 676 677
678 -class OpenIDResponse(object):
679 """I am a response to an OpenID request. 680 681 @ivar request: The request I respond to. 682 @type request: L{OpenIDRequest} 683 684 @ivar fields: My parameters as a dictionary with each key mapping to 685 one value. Keys are parameter names with no leading "C{openid.}". 686 e.g. "C{identity}" and "C{mac_key}", never "C{openid.identity}". 687 @type fields: dict 688 689 @ivar signed: The names of the fields which should be signed. 690 @type signed: list of str 691 """ 692 693 # Implementer's note: In a more symmetric client/server 694 # implementation, there would be more types of OpenIDResponse 695 # object and they would have validated attributes according to the 696 # type of response. But as it is, Response objects in a server are 697 # basically write-only, their only job is to go out over the wire, 698 # so this is just a loose wrapper around OpenIDResponse.fields. 699
700 - def __init__(self, request):
701 """Make a response to an L{OpenIDRequest}. 702 703 @type request: L{OpenIDRequest} 704 """ 705 self.request = request 706 self.fields = {} 707 self.signed = []
708
709 - def __str__(self):
710 return "%s for %s: %s" % ( 711 self.__class__.__name__, 712 self.request.__class__.__name__, 713 self.fields)
714 715
716 - def addField(self, namespace, key, value, signed=True):
717 """Add a field to this response. 718 719 @param namespace: The extension namespace the field is in, with no 720 leading "C{openid.}" e.g. "C{sreg}". 721 @type namespace: str 722 723 @param key: The field's name, e.g. "C{fullname}". 724 @type key: str 725 726 @param value: The field's value. 727 @type value: str 728 729 @param signed: Whether this field should be signed. 730 @type signed: bool 731 """ 732 if namespace: 733 key = '%s.%s' % (namespace, key) 734 self.fields[key] = value 735 if signed and key not in self.signed: 736 self.signed.append(key)
737 738
739 - def addFields(self, namespace, fields, signed=True):
740 """Add a number of fields to this response. 741 742 @param namespace: The extension namespace the field is in, with no 743 leading "C{openid.}" e.g. "C{sreg}". 744 @type namespace: str 745 746 @param fields: A dictionary with the fields to add. 747 e.g. C{{"fullname": "Frank the Goat"}} 748 749 @param signed: Whether these fields should be signed. 750 @type signed: bool 751 """ 752 for key, value in fields.iteritems(): 753 self.addField(namespace, key, value, signed)
754 755
756 - def update(self, namespace, other):
757 """Update my fields with those from another L{OpenIDResponse}. 758 759 The idea here is that if you write an OpenID extension, it 760 could produce a Response object with C{fields} and C{signed} 761 attributes, and you could merge it with me using this method 762 before I am signed and sent. 763 764 All entries in C{other.fields} will have their keys prefixed 765 with C{namespace} and added to my fields. All elements of 766 C{other.signed} will be prefixed with C{namespace} and added 767 to my C{signed} list. 768 769 @param namespace: The extension namespace the field is in, with no 770 leading "C{openid.}" e.g. "C{sreg}". 771 @type namespace: str 772 773 @param other: A response object to update from. 774 @type other: L{OpenIDResponse} 775 """ 776 if namespace: 777 namespaced_fields = dict([('%s.%s' % (namespace, k), v) for k, v 778 in other.fields.iteritems()]) 779 namespaced_signed = ['%s.%s' % (namespace, k) for k 780 in other.signed] 781 else: 782 namespaced_fields = other.fields 783 namespaced_signed = other.signed 784 self.fields.update(namespaced_fields) 785 self.signed.extend(namespaced_signed)
786 787
788 - def needsSigning(self):
789 """Does this response require signing? 790 791 @returntype: bool 792 """ 793 return ( 794 (self.request.mode in ['checkid_setup', 'checkid_immediate']) 795 and self.signed 796 )
797 798 799 # implements IEncodable 800
801 - def whichEncoding(self):
802 """How should I be encoded? 803 804 @returns: one of ENCODE_URL or ENCODE_KVFORM. 805 """ 806 if self.request.mode in BROWSER_REQUEST_MODES: 807 return ENCODE_URL 808 else: 809 return ENCODE_KVFORM
810 811
812 - def encodeToURL(self):
813 """Encode a response as a URL for the user agent to GET. 814 815 You will generally use this URL with a HTTP redirect. 816 817 @returns: A URL to direct the user agent back to. 818 @returntype: str 819 """ 820 fields = dict( 821 [(OPENID_PREFIX + k, v.encode('UTF8')) for k, v in self.fields.iteritems()]) 822 return oidutil.appendArgs(self.request.return_to, fields)
823 824
825 - def encodeToKVForm(self):
826 """Encode a response in key-value colon/newline format. 827 828 This is a machine-readable format used to respond to messages which 829 came directly from the consumer and not through the user agent. 830 831 @see: OpenID Specs, 832 U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>} 833 834 @returntype: str 835 """ 836 return kvform.dictToKV(self.fields)
837 838
839 - def __str__(self):
840 return "%s for %s: signed%s %s" % ( 841 self.__class__.__name__, 842 self.request.__class__.__name__, 843 self.signed, self.fields)
844 845 846
847 -class WebResponse(object):
848 """I am a response to an OpenID request in terms a web server understands. 849 850 I generally come from an L{Encoder}, either directly or from 851 L{Server.encodeResponse}. 852 853 @ivar code: The HTTP code of this response. 854 @type code: int 855 856 @ivar headers: Headers to include in this response. 857 @type headers: dict 858 859 @ivar body: The body of this response. 860 @type body: str 861 """ 862
863 - def __init__(self, code=HTTP_OK, headers=None, body=""):
864 """Construct me. 865 866 These parameters are assigned directly as class attributes, see 867 my L{class documentation<WebResponse>} for their descriptions. 868 """ 869 self.code = code 870 if headers is not None: 871 self.headers = headers 872 else: 873 self.headers = {} 874 self.body = body
875 876 877
878 -class Signatory(object):
879 """I sign things. 880 881 I also check signatures. 882 883 All my state is encapsulated in an 884 L{OpenIDStore<openid.store.interface.OpenIDStore>}, which means 885 I'm not generally pickleable but I am easy to reconstruct. 886 887 @cvar SECRET_LIFETIME: The number of seconds a secret remains valid. 888 @type SECRET_LIFETIME: int 889 """ 890 891 SECRET_LIFETIME = 14 * 24 * 60 * 60 # 14 days, in seconds 892 893 # keys have a bogus server URL in them because the filestore 894 # really does expect that key to be a URL. This seems a little 895 # silly for the server store, since I expect there to be only one 896 # server URL. 897 _normal_key = 'http://localhost/|normal' 898 _dumb_key = 'http://localhost/|dumb' 899 900
901 - def __init__(self, store):
902 """Create a new Signatory. 903 904 @param store: The back-end where my associations are stored. 905 @type store: L{openid.store.interface.OpenIDStore} 906 """ 907 assert store is not None 908 self.store = store
909 910
911 - def verify(self, assoc_handle, sig, signed_pairs):
912 """Verify that the signature for some data is valid. 913 914 @param assoc_handle: The handle of the association used to sign the 915 data. 916 @type assoc_handle: str 917 918 @param sig: The base-64 encoded signature to check. 919 @type sig: str 920 921 @param signed_pairs: The data to check, an ordered list of key-value 922 pairs. The keys should be as they are in the request's C{signed} 923 list, without any C{"openid."} prefix. 924 @type signed_pairs: list of pairs 925 926 @returns: C{True} if the signature is valid, C{False} if not. 927 @returntype: bool 928 """ 929 assoc = self.getAssociation(assoc_handle, dumb=True) 930 if not assoc: 931 oidutil.log("failed to get assoc with handle %r to verify sig %r" 932 % (assoc_handle, sig)) 933 return False 934 935 # Not using Association.checkSignature here is intentional; 936 # Association should not know things like "the list of signed pairs is 937 # in the request's 'signed' parameter and it is comma-separated." 938 expected_sig = oidutil.toBase64(assoc.sign(signed_pairs)) 939 940 return sig == expected_sig
941 942
943 - def sign(self, response):
944 """Sign a response. 945 946 I take a L{OpenIDResponse}, create a signature for everything 947 in its L{signed<OpenIDResponse.signed>} list, and return a new 948 copy of the response object with that signature included. 949 950 @param response: A response to sign. 951 @type response: L{OpenIDResponse} 952 953 @returns: A signed copy of the response. 954 @returntype: L{OpenIDResponse} 955 """ 956 signed_response = deepcopy(response) 957 assoc_handle = response.request.assoc_handle 958 if assoc_handle: 959 # normal mode 960 assoc = self.getAssociation(assoc_handle, dumb=False) 961 if not assoc: 962 # fall back to dumb mode 963 signed_response.fields['invalidate_handle'] = assoc_handle 964 assoc = self.createAssociation(dumb=True) 965 else: 966 # dumb mode. 967 assoc = self.createAssociation(dumb=True) 968 969 signed_response.fields['assoc_handle'] = assoc.handle 970 assoc.addSignature(signed_response.signed, signed_response.fields, 971 prefix='') 972 return signed_response
973 974
975 - def createAssociation(self, dumb=True, assoc_type='HMAC-SHA1'):
976 """Make a new association. 977 978 @param dumb: Is this association for a dumb-mode transaction? 979 @type dumb: bool 980 981 @param assoc_type: The type of association to create. Currently 982 there is only one type defined, C{HMAC-SHA1}. 983 @type assoc_type: str 984 985 @returns: the new association. 986 @returntype: L{openid.association.Association} 987 """ 988 secret = cryptutil.getBytes(20) 989 uniq = oidutil.toBase64(cryptutil.getBytes(4)) 990 handle = '{%s}{%x}{%s}' % (assoc_type, int(time.time()), uniq) 991 992 assoc = Association.fromExpiresIn( 993 self.SECRET_LIFETIME, handle, secret, assoc_type) 994 995 if dumb: 996 key = self._dumb_key 997 else: 998 key = self._normal_key 999 self.store.storeAssociation(key, assoc) 1000 return assoc
1001 1002
1003 - def getAssociation(self, assoc_handle, dumb):
1004 """Get the association with the specified handle. 1005 1006 @type assoc_handle: str 1007 1008 @param dumb: Is this association used with dumb mode? 1009 @type dumb: bool 1010 1011 @returns: the association, or None if no valid association with that 1012 handle was found. 1013 @returntype: L{openid.association.Association} 1014 """ 1015 # Hmm. We've created an interface that deals almost entirely with 1016 # assoc_handles. The only place outside the Signatory that uses this 1017 # (and thus the only place that ever sees Association objects) is 1018 # when creating a response to an association request, as it must have 1019 # the association's secret. 1020 1021 if assoc_handle is None: 1022 raise ValueError("assoc_handle must not be None") 1023 1024 if dumb: 1025 key = self._dumb_key 1026 else: 1027 key = self._normal_key 1028 assoc = self.store.getAssociation(key, assoc_handle) 1029 if assoc is not None and assoc.expiresIn <= 0: 1030 oidutil.log("requested %sdumb key %r is expired (by %s seconds)" % 1031 ((not dumb) and 'not-' or '', 1032 assoc_handle, assoc.expiresIn)) 1033 self.store.removeAssociation(key, assoc_handle) 1034 assoc = None 1035 return assoc
1036 1037
1038 - def invalidate(self, assoc_handle, dumb):
1039 """Invalidates the association with the given handle. 1040 1041 @type assoc_handle: str 1042 1043 @param dumb: Is this association used with dumb mode? 1044 @type dumb: bool 1045 """ 1046 if dumb: 1047 key = self._dumb_key 1048 else: 1049 key = self._normal_key 1050 self.store.removeAssociation(key, assoc_handle)
1051 1052 1053
1054 -class Encoder(object):
1055 """I encode responses in to L{WebResponses<WebResponse>}. 1056 1057 If you don't like L{WebResponses<WebResponse>}, you can do 1058 your own handling of L{OpenIDResponses<OpenIDResponse>} with 1059 L{OpenIDResponse.whichEncoding}, L{OpenIDResponse.encodeToURL}, and 1060 L{OpenIDResponse.encodeToKVForm}. 1061 """ 1062 1063 responseFactory = WebResponse 1064 1065
1066 - def encode(self, response):
1067 """Encode a response to a L{WebResponse}. 1068 1069 @raises EncodingError: When I can't figure out how to encode this 1070 message. 1071 """ 1072 encode_as = response.whichEncoding() 1073 if encode_as == ENCODE_KVFORM: 1074 wr = self.responseFactory(body=response.encodeToKVForm()) 1075 if isinstance(response, Exception): 1076 wr.code = HTTP_ERROR 1077 elif encode_as == ENCODE_URL: 1078 location = response.encodeToURL() 1079 wr = self.responseFactory(code=HTTP_REDIRECT, 1080 headers={'location': location}) 1081 else: 1082 # Can't encode this to a protocol message. You should probably 1083 # render it to HTML and show it to the user. 1084 raise EncodingError(response) 1085 return wr
1086 1087 1088
1089 -class SigningEncoder(Encoder):
1090 """I encode responses in to L{WebResponses<WebResponse>}, signing them when required. 1091 """ 1092
1093 - def __init__(self, signatory):
1094 """Create a L{SigningEncoder}. 1095 1096 @param signatory: The L{Signatory} I will make signatures with. 1097 @type signatory: L{Signatory} 1098 """ 1099 self.signatory = signatory
1100 1101
1102 - def encode(self, response):
1103 """Encode a response to a L{WebResponse}, signing it first if appropriate. 1104 1105 @raises EncodingError: When I can't figure out how to encode this 1106 message. 1107 1108 @raises AlreadySigned: When this response is already signed. 1109 1110 @returntype: L{WebResponse} 1111 """ 1112 # the isinstance is a bit of a kludge... it means there isn't really 1113 # an adapter to make the interfaces quite match. 1114 if (not isinstance(response, Exception)) and response.needsSigning(): 1115 if not self.signatory: 1116 raise ValueError( 1117 "Must have a store to sign this request: %s" % 1118 (response,), response) 1119 if 'sig' in response.fields: 1120 raise AlreadySigned(response) 1121 response = self.signatory.sign(response) 1122 return super(SigningEncoder, self).encode(response)
1123 1124 1125
1126 -class Decoder(object):
1127 """I decode an incoming web request in to a L{OpenIDRequest}. 1128 """ 1129 1130 _handlers = { 1131 'checkid_setup': CheckIDRequest.fromQuery, 1132 'checkid_immediate': CheckIDRequest.fromQuery, 1133 'check_authentication': CheckAuthRequest.fromQuery, 1134 'associate': AssociateRequest.fromQuery, 1135 } 1136 1137
1138 - def decode(self, query):
1139 """I transform query parameters into an L{OpenIDRequest}. 1140 1141 If the query does not seem to be an OpenID request at all, I return 1142 C{None}. 1143 1144 @param query: The query parameters as a dictionary with each 1145 key mapping to one value. 1146 @type query: dict 1147 1148 @raises ProtocolError: When the query does not seem to be a valid 1149 OpenID request. 1150 1151 @returntype: L{OpenIDRequest} 1152 """ 1153 if not query: 1154 return None 1155 myquery = dict(filter(lambda (k, v): k.startswith(OPENID_PREFIX), 1156 query.iteritems())) 1157 if not myquery: 1158 return None 1159 1160 mode = myquery.get(OPENID_PREFIX + 'mode') 1161 if isinstance(mode, list): 1162 raise TypeError("query dict must have one value for each key, " 1163 "not lists of values. Query is %r" % (query,)) 1164 1165 if not mode: 1166 raise ProtocolError( 1167 query, 1168 text="No %smode value in query %r" % ( 1169 OPENID_PREFIX, query)) 1170 handler = self._handlers.get(mode, self.defaultDecoder) 1171 return handler(query)
1172 1173
1174 - def defaultDecoder(self, query):
1175 """Called to decode queries when no handler for that mode is found. 1176 1177 @raises ProtocolError: This implementation always raises 1178 L{ProtocolError}. 1179 """ 1180 mode = query[OPENID_PREFIX + 'mode'] 1181 raise ProtocolError( 1182 query, 1183 text="No decoder for mode %r" % (mode,))
1184 1185 1186
1187 -class Server(object):
1188 """I handle requests for an OpenID server. 1189 1190 Some types of requests (those which are not C{checkid} requests) may be 1191 handed to my L{handleRequest} method, and I will take care of it and 1192 return a response. 1193 1194 For your convenience, I also provide an interface to L{Decoder.decode} 1195 and L{SigningEncoder.encode} through my methods L{decodeRequest} and 1196 L{encodeResponse}. 1197 1198 All my state is encapsulated in an 1199 L{OpenIDStore<openid.store.interface.OpenIDStore>}, which means 1200 I'm not generally pickleable but I am easy to reconstruct. 1201 1202 Example:: 1203 1204 oserver = Server(FileOpenIDStore(data_path)) 1205 request = oserver.decodeRequest(query) 1206 if request.mode in ["checkid_immediate", "checkid_setup"]: 1207 if self.isAuthorized(request.identity, request.trust_root): 1208 response = request.answer(True) 1209 elif request.immediate: 1210 response = request.answer(False, self.base_url) 1211 else: 1212 self.showDecidePage(request) 1213 return 1214 else: 1215 response = oserver.handleRequest(request) 1216 1217 webresponse = oserver.encode(response) 1218 1219 @ivar signatory: I'm using this for associate requests and to sign things. 1220 @type signatory: L{Signatory} 1221 1222 @ivar decoder: I'm using this to decode things. 1223 @type decoder: L{Decoder} 1224 1225 @ivar encoder: I'm using this to encode things. 1226 @type encoder: L{Encoder} 1227 """ 1228 1229 signatoryClass = Signatory 1230 encoderClass = SigningEncoder 1231 decoderClass = Decoder 1232
1233 - def __init__(self, store):
1234 """A new L{Server}. 1235 1236 @param store: The back-end where my associations are stored. 1237 @type store: L{openid.store.interface.OpenIDStore} 1238 """ 1239 self.store = store 1240 self.signatory = self.signatoryClass(self.store) 1241 self.encoder = self.encoderClass(self.signatory) 1242 self.decoder = self.decoderClass()
1243 1244
1245 - def handleRequest(self, request):
1246 """Handle a request. 1247 1248 Give me a request, I will give you a response. Unless it's a type 1249 of request I cannot handle myself, in which case I will raise 1250 C{NotImplementedError}. In that case, you can handle it yourself, 1251 or add a method to me for handling that request type. 1252 1253 @raises NotImplementedError: When I do not have a handler defined 1254 for that type of request. 1255 """ 1256 handler = getattr(self, 'openid_' + request.mode, None) 1257 if handler is not None: 1258 return handler(request) 1259 else: 1260 raise NotImplementedError( 1261 "%s has no handler for a request of mode %r." % 1262 (self, request.mode))
1263 1264
1265 - def openid_check_authentication(self, request):
1266 """Handle and respond to {check_authentication} requests. 1267 1268 @returntype: L{OpenIDResponse} 1269 """ 1270 return request.answer(self.signatory)
1271 1272
1273 - def openid_associate(self, request):
1274 """Handle and respond to {associate} requests. 1275 1276 @returntype: L{OpenIDResponse} 1277 """ 1278 assoc = self.signatory.createAssociation(dumb=False) 1279 return request.answer(assoc)
1280 1281
1282 - def decodeRequest(self, query):
1283 """Transform query parameters into an L{OpenIDRequest}. 1284 1285 If the query does not seem to be an OpenID request at all, I return 1286 C{None}. 1287 1288 @param query: The query parameters as a dictionary with each 1289 key mapping to one value. 1290 @type query: dict 1291 1292 @raises ProtocolError: When the query does not seem to be a valid 1293 OpenID request. 1294 1295 @returntype: L{OpenIDRequest} 1296 1297 @see: L{Decoder.decode} 1298 """ 1299 return self.decoder.decode(query)
1300 1301
1302 - def encodeResponse(self, response):
1303 """Encode a response to a L{WebResponse}, signing it first if appropriate. 1304 1305 @raises EncodingError: When I can't figure out how to encode this 1306 message. 1307 1308 @raises AlreadySigned: When this response is already signed. 1309 1310 @returntype: L{WebResponse} 1311 1312 @see: L{Encoder.encode} 1313 """ 1314 return self.encoder.encode(response)
1315 1316 1317
1318 -class ProtocolError(Exception):
1319 """A message did not conform to the OpenID protocol. 1320 1321 @ivar query: The query that is failing to be a valid OpenID request. 1322 @type query: dict 1323 """ 1324
1325 - def __init__(self, query, text=None):
1326 """When an error occurs. 1327 1328 @param query: The query that is failing to be a valid OpenID request. 1329 @type query: dict 1330 1331 @param text: A message about the encountered error. Set as C{args[0]}. 1332 @type text: str 1333 """ 1334 self.query = query 1335 Exception.__init__(self, text)
1336 1337
1338 - def hasReturnTo(self):
1339 """Did this request have a return_to parameter? 1340 1341 @returntype: bool 1342 """ 1343 if self.query is None: 1344 return False 1345 else: 1346 return (OPENID_PREFIX + 'return_to') in self.query
1347 1348 1349 # implements IEncodable 1350
1351 - def encodeToURL(self):
1352 """Encode a response as a URL for the user agent to GET. 1353 1354 You will generally use this URL with a HTTP redirect. 1355 1356 @returns: A URL to direct the user agent back to. 1357 @returntype: str 1358 """ 1359 return_to = self.query.get(OPENID_PREFIX + 'return_to') 1360 if not return_to: 1361 raise ValueError("I have no return_to URL.") 1362 return oidutil.appendArgs(return_to, { 1363 'openid.mode': 'error', 1364 'openid.error': str(self), 1365 })
1366 1367
1368 - def encodeToKVForm(self):
1369 """Encode a response in key-value colon/newline format. 1370 1371 This is a machine-readable format used to respond to messages which 1372 came directly from the consumer and not through the user agent. 1373 1374 @see: OpenID Specs, 1375 U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>} 1376 1377 @returntype: str 1378 """ 1379 return kvform.dictToKV({ 1380 'mode': 'error', 1381 'error': str(self), 1382 })
1383 1384
1385 - def whichEncoding(self):
1386 """How should I be encoded? 1387 1388 @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None, 1389 I cannot be encoded as a protocol message and should be 1390 displayed to the user. 1391 """ 1392 if self.hasReturnTo(): 1393 return ENCODE_URL 1394 1395 if self.query is None: 1396 return None 1397 1398 mode = self.query.get('openid.mode') 1399 if mode: 1400 if mode not in BROWSER_REQUEST_MODES: 1401 return ENCODE_KVFORM 1402 1403 # According to the OpenID spec as of this writing, we are probably 1404 # supposed to switch on request type here (GET versus POST) to figure 1405 # out if we're supposed to print machine-readable or human-readable 1406 # content at this point. GET/POST seems like a pretty lousy way of 1407 # making the distinction though, as it's just as possible that the 1408 # user agent could have mistakenly been directed to post to the 1409 # server URL. 1410 1411 # Basically, if your request was so broken that you didn't manage to 1412 # include an openid.mode, I'm not going to worry too much about 1413 # returning you something you can't parse. 1414 return None
1415 1416 1417
1418 -class EncodingError(Exception):
1419 """Could not encode this as a protocol message. 1420 1421 You should probably render it and show it to the user. 1422 1423 @ivar response: The response that failed to encode. 1424 @type response: L{OpenIDResponse} 1425 """ 1426
1427 - def __init__(self, response):
1428 Exception.__init__(self, response) 1429 self.response = response
1430 1431 1432
1433 -class AlreadySigned(EncodingError):
1434 """This response is already signed."""
1435 1436 1437
1438 -class UntrustedReturnURL(ProtocolError):
1439 """A return_to is outside the trust_root.""" 1440
1441 - def __init__(self, query, return_to, trust_root):
1442 ProtocolError.__init__(self, query) 1443 self.return_to = return_to 1444 self.trust_root = trust_root
1445
1446 - def __str__(self):
1447 return "return_to %r not under trust_root %r" % (self.return_to, 1448 self.trust_root)
1449 1450
1451 -class MalformedReturnURL(ProtocolError):
1452 """The return_to URL doesn't look like a valid URL."""
1453 - def __init__(self, query, return_to):
1454 self.return_to = return_to 1455 ProtocolError.__init__(self, query)
1456 1457 1458
1459 -class MalformedTrustRoot(ProtocolError):
1460 """The trust root is not well-formed. 1461 1462 @see: OpenID Specs, U{openid.trust_root<http://openid.net/specs.bml#mode-checkid_immediate>} 1463 """ 1464 pass
1465 1466 1467 #class IEncodable: # Interface 1468 # def encodeToURL(return_to): 1469 # """Encode a response as a URL for redirection. 1470 # 1471 # @returns: A URL to direct the user agent back to. 1472 # @returntype: str 1473 # """ 1474 # pass 1475 # 1476 # def encodeToKvform(): 1477 # """Encode a response in key-value colon/newline format. 1478 # 1479 # This is a machine-readable format used to respond to messages which 1480 # came directly from the consumer and not through the user agent. 1481 # 1482 # @see: OpenID Specs, 1483 # U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>} 1484 # 1485 # @returntype: str 1486 # """ 1487 # pass 1488 # 1489 # def whichEncoding(): 1490 # """How should I be encoded? 1491 # 1492 # @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None, 1493 # I cannot be encoded as a protocol message and should be 1494 # displayed to the user. 1495 # """ 1496 # pass 1497