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

Source Code for Module openid.server.server

   1  # -*- test-case-name: openid.test.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, optionally 
  32      check L{CheckIDRequest.returnToVerified}, and and when you've come 
  33      to a decision, form a response by calling 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 the request 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 2.0 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://openid.net/specs/openid-simple-registration-extension-1_0.html>}, 
  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          # as well as a namespace declaration for the openid.sreg namespace 
  65          response.fields.setArg('http://openid.net/sreg/1.0', 'timezone', 'America/Los_Angeles') 
  66   
  67      There are helper modules for a number of extensions, including 
  68      L{Attribute Exchange<openid.extensions.ax>}, 
  69      L{PAPE<openid.extensions.pape>}, and 
  70      L{Simple Registration<openid.extensions.sreg>} in the L{openid.extensions} 
  71      package. 
  72   
  73  Stores 
  74  ====== 
  75   
  76      The OpenID server needs to maintain state between requests in order 
  77      to function.  Its mechanism for doing this is called a store.  The 
  78      store interface is defined in C{L{openid.store.interface.OpenIDStore}}. 
  79      Additionally, several concrete store implementations are provided, so that 
  80      most sites won't need to implement a custom store.  For a store backed 
  81      by flat files on disk, see C{L{openid.store.filestore.FileOpenIDStore}}. 
  82      For stores based on MySQL or SQLite, see the C{L{openid.store.sqlstore}} 
  83      module. 
  84   
  85   
  86  Upgrading 
  87  ========= 
  88   
  89  From 1.0 to 1.1 
  90  --------------- 
  91   
  92      The keys by which a server looks up associations in its store have changed 
  93      in version 1.2 of this library.  If your store has entries created from 
  94      version 1.0 code, you should empty it. 
  95   
  96  From 1.1 to 2.0 
  97  --------------- 
  98   
  99      One of the additions to the OpenID protocol was a specified nonce 
 100      format for one-way nonces.  As a result, the nonce table in the store 
 101      has changed.  You'll need to run contrib/upgrade-store-1.1-to-2.0 to 
 102      upgrade your store, or you'll encounter errors about the wrong number 
 103      of columns in the oid_nonces table. 
 104   
 105      If you've written your own custom store or code that interacts 
 106      directly with it, you'll need to review the change notes in 
 107      L{openid.store.interface}. 
 108   
 109  @group Requests: OpenIDRequest, AssociateRequest, CheckIDRequest, 
 110      CheckAuthRequest 
 111   
 112  @group Responses: OpenIDResponse 
 113   
 114  @group HTTP Codes: HTTP_OK, HTTP_REDIRECT, HTTP_ERROR 
 115   
 116  @group Response Encodings: ENCODE_KVFORM, ENCODE_HTML_FORM, ENCODE_URL 
 117  """ 
 118   
 119  import time, warnings 
 120  from copy import deepcopy 
 121   
 122  from openid import cryptutil 
 123  from openid import oidutil 
 124  from openid import kvform 
 125  from openid.dh import DiffieHellman 
 126  from openid.store.nonce import mkNonce 
 127  from openid.server.trustroot import TrustRoot, verifyReturnTo 
 128  from openid.association import Association, default_negotiator, getSecretSize 
 129  from openid.message import Message, InvalidOpenIDNamespace, \ 
 130       OPENID_NS, OPENID2_NS, IDENTIFIER_SELECT, OPENID1_URL_LIMIT 
 131  from openid.urinorm import urinorm 
 132   
 133  HTTP_OK = 200 
 134  HTTP_REDIRECT = 302 
 135  HTTP_ERROR = 400 
 136   
 137  BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate'] 
 138   
 139  ENCODE_KVFORM = ('kvform',) 
 140  ENCODE_URL = ('URL/redirect',) 
 141  ENCODE_HTML_FORM = ('HTML form',) 
 142   
 143  UNUSED = None 
 144   
145 -class OpenIDRequest(object):
146 """I represent an incoming OpenID request. 147 148 @cvar mode: the C{X{openid.mode}} of this request. 149 @type mode: str 150 """ 151 mode = None
152 153
154 -class CheckAuthRequest(OpenIDRequest):
155 """A request to verify the validity of a previous response. 156 157 @cvar mode: "X{C{check_authentication}}" 158 @type mode: str 159 160 @ivar assoc_handle: The X{association handle} the response was signed with. 161 @type assoc_handle: str 162 @ivar signed: The message with the signature which wants checking. 163 @type signed: L{Message} 164 165 @ivar invalidate_handle: An X{association handle} the client is asking 166 about the validity of. Optional, may be C{None}. 167 @type invalidate_handle: str 168 169 @see: U{OpenID Specs, Mode: check_authentication 170 <http://openid.net/specs.bml#mode-check_authentication>} 171 """ 172 mode = "check_authentication" 173 174 required_fields = ["identity", "return_to", "response_nonce"] 175
176 - def __init__(self, assoc_handle, signed, invalidate_handle=None):
177 """Construct me. 178 179 These parameters are assigned directly as class attributes, see 180 my L{class documentation<CheckAuthRequest>} for their descriptions. 181 182 @type assoc_handle: str 183 @type signed: L{Message} 184 @type invalidate_handle: str 185 """ 186 self.assoc_handle = assoc_handle 187 self.signed = signed 188 self.invalidate_handle = invalidate_handle 189 self.namespace = OPENID2_NS
190 191
192 - def fromMessage(klass, message, op_endpoint=UNUSED):
193 """Construct me from an OpenID Message. 194 195 @param message: An OpenID check_authentication Message 196 @type message: L{openid.message.Message} 197 198 @returntype: L{CheckAuthRequest} 199 """ 200 self = klass.__new__(klass) 201 self.message = message 202 self.namespace = message.getOpenIDNamespace() 203 self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') 204 self.sig = message.getArg(OPENID_NS, 'sig') 205 206 if (self.assoc_handle is None or 207 self.sig is None): 208 fmt = "%s request missing required parameter from message %s" 209 raise ProtocolError( 210 message, text=fmt % (self.mode, message)) 211 212 self.invalidate_handle = message.getArg(OPENID_NS, 'invalidate_handle') 213 214 self.signed = message.copy() 215 # openid.mode is currently check_authentication because 216 # that's the mode of this request. But the signature 217 # was made on something with a different openid.mode. 218 # http://article.gmane.org/gmane.comp.web.openid.general/537 219 if self.signed.hasKey(OPENID_NS, "mode"): 220 self.signed.setArg(OPENID_NS, "mode", "id_res") 221 222 return self
223 224 fromMessage = classmethod(fromMessage) 225
226 - def answer(self, signatory):
227 """Respond to this request. 228 229 Given a L{Signatory}, I can check the validity of the signature and 230 the X{C{invalidate_handle}}. 231 232 @param signatory: The L{Signatory} to use to check the signature. 233 @type signatory: L{Signatory} 234 235 @returns: A response with an X{C{is_valid}} (and, if 236 appropriate X{C{invalidate_handle}}) field. 237 @returntype: L{OpenIDResponse} 238 """ 239 is_valid = signatory.verify(self.assoc_handle, self.signed) 240 # Now invalidate that assoc_handle so it this checkAuth message cannot 241 # be replayed. 242 signatory.invalidate(self.assoc_handle, dumb=True) 243 response = OpenIDResponse(self) 244 valid_str = (is_valid and "true") or "false" 245 response.fields.setArg(OPENID_NS, 'is_valid', valid_str) 246 247 if self.invalidate_handle: 248 assoc = signatory.getAssociation(self.invalidate_handle, dumb=False) 249 if not assoc: 250 response.fields.setArg( 251 OPENID_NS, 'invalidate_handle', self.invalidate_handle) 252 return response
253 254
255 - def __str__(self):
256 if self.invalidate_handle: 257 ih = " invalidate? %r" % (self.invalidate_handle,) 258 else: 259 ih = "" 260 s = "<%s handle: %r sig: %r: signed: %r%s>" % ( 261 self.__class__.__name__, self.assoc_handle, 262 self.sig, self.signed, ih) 263 return s
264 265
266 -class PlainTextServerSession(object):
267 """An object that knows how to handle association requests with no 268 session type. 269 270 @cvar session_type: The session_type for this association 271 session. There is no type defined for plain-text in the OpenID 272 specification, so we use 'no-encryption'. 273 @type session_type: str 274 275 @see: U{OpenID Specs, Mode: associate 276 <http://openid.net/specs.bml#mode-associate>} 277 @see: AssociateRequest 278 """ 279 session_type = 'no-encryption' 280 allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] 281
282 - def fromMessage(cls, unused_request):
283 return cls()
284 285 fromMessage = classmethod(fromMessage) 286
287 - def answer(self, secret):
288 return {'mac_key': oidutil.toBase64(secret)}
289 290
291 -class DiffieHellmanSHA1ServerSession(object):
292 """An object that knows how to handle association requests with the 293 Diffie-Hellman session type. 294 295 @cvar session_type: The session_type for this association 296 session. 297 @type session_type: str 298 299 @ivar dh: The Diffie-Hellman algorithm values for this request 300 @type dh: DiffieHellman 301 302 @ivar consumer_pubkey: The public key sent by the consumer in the 303 associate request 304 @type consumer_pubkey: long 305 306 @see: U{OpenID Specs, Mode: associate 307 <http://openid.net/specs.bml#mode-associate>} 308 @see: AssociateRequest 309 """ 310 session_type = 'DH-SHA1' 311 hash_func = staticmethod(cryptutil.sha1) 312 allowed_assoc_types = ['HMAC-SHA1'] 313
314 - def __init__(self, dh, consumer_pubkey):
315 self.dh = dh 316 self.consumer_pubkey = consumer_pubkey
317
318 - def fromMessage(cls, message):
319 """ 320 @param message: The associate request message 321 @type message: openid.message.Message 322 323 @returntype: L{DiffieHellmanSHA1ServerSession} 324 325 @raises ProtocolError: When parameters required to establish the 326 session are missing. 327 """ 328 dh_modulus = message.getArg(OPENID_NS, 'dh_modulus') 329 dh_gen = message.getArg(OPENID_NS, 'dh_gen') 330 if (dh_modulus is None and dh_gen is not None or 331 dh_gen is None and dh_modulus is not None): 332 333 if dh_modulus is None: 334 missing = 'modulus' 335 else: 336 missing = 'generator' 337 338 raise ProtocolError(message, 339 'If non-default modulus or generator is ' 340 'supplied, both must be supplied. Missing %s' 341 % (missing,)) 342 343 if dh_modulus or dh_gen: 344 dh_modulus = cryptutil.base64ToLong(dh_modulus) 345 dh_gen = cryptutil.base64ToLong(dh_gen) 346 dh = DiffieHellman(dh_modulus, dh_gen) 347 else: 348 dh = DiffieHellman.fromDefaults() 349 350 consumer_pubkey = message.getArg(OPENID_NS, 'dh_consumer_public') 351 if consumer_pubkey is None: 352 raise ProtocolError(message, "Public key for DH-SHA1 session " 353 "not found in message %s" % (message,)) 354 355 consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey) 356 357 return cls(dh, consumer_pubkey)
358 359 fromMessage = classmethod(fromMessage) 360
361 - def answer(self, secret):
362 mac_key = self.dh.xorSecret(self.consumer_pubkey, 363 secret, 364 self.hash_func) 365 return { 366 'dh_server_public': cryptutil.longToBase64(self.dh.public), 367 'enc_mac_key': oidutil.toBase64(mac_key), 368 }
369
370 -class DiffieHellmanSHA256ServerSession(DiffieHellmanSHA1ServerSession):
371 session_type = 'DH-SHA256' 372 hash_func = staticmethod(cryptutil.sha256) 373 allowed_assoc_types = ['HMAC-SHA256']
374
375 -class AssociateRequest(OpenIDRequest):
376 """A request to establish an X{association}. 377 378 @cvar mode: "X{C{check_authentication}}" 379 @type mode: str 380 381 @ivar assoc_type: The type of association. The protocol currently only 382 defines one value for this, "X{C{HMAC-SHA1}}". 383 @type assoc_type: str 384 385 @ivar session: An object that knows how to handle association 386 requests of a certain type. 387 388 @see: U{OpenID Specs, Mode: associate 389 <http://openid.net/specs.bml#mode-associate>} 390 """ 391 392 mode = "associate" 393 394 session_classes = { 395 'no-encryption': PlainTextServerSession, 396 'DH-SHA1': DiffieHellmanSHA1ServerSession, 397 'DH-SHA256': DiffieHellmanSHA256ServerSession, 398 } 399
400 - def __init__(self, session, assoc_type):
401 """Construct me. 402 403 The session is assigned directly as a class attribute. See my 404 L{class documentation<AssociateRequest>} for its description. 405 """ 406 super(AssociateRequest, self).__init__() 407 self.session = session 408 self.assoc_type = assoc_type 409 self.namespace = OPENID2_NS
410 411
412 - def fromMessage(klass, message, op_endpoint=UNUSED):
413 """Construct me from an OpenID Message. 414 415 @param message: The OpenID associate request 416 @type message: openid.message.Message 417 418 @returntype: L{AssociateRequest} 419 """ 420 if message.isOpenID1(): 421 session_type = message.getArg(OPENID_NS, 'session_type') 422 if session_type == 'no-encryption': 423 oidutil.log('Received OpenID 1 request with a no-encryption ' 424 'assocaition session type. Continuing anyway.') 425 elif not session_type: 426 session_type = 'no-encryption' 427 else: 428 session_type = message.getArg(OPENID2_NS, 'session_type') 429 if session_type is None: 430 raise ProtocolError(message, 431 text="session_type missing from request") 432 433 try: 434 session_class = klass.session_classes[session_type] 435 except KeyError: 436 raise ProtocolError(message, 437 "Unknown session type %r" % (session_type,)) 438 439 try: 440 session = session_class.fromMessage(message) 441 except ValueError, why: 442 raise ProtocolError(message, 'Error parsing %s session: %s' % 443 (session_class.session_type, why[0])) 444 445 assoc_type = message.getArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1') 446 if assoc_type not in session.allowed_assoc_types: 447 fmt = 'Session type %s does not support association type %s' 448 raise ProtocolError(message, fmt % (session_type, assoc_type)) 449 450 self = klass(session, assoc_type) 451 self.message = message 452 self.namespace = message.getOpenIDNamespace() 453 return self
454 455 fromMessage = classmethod(fromMessage) 456
457 - def answer(self, assoc):
458 """Respond to this request with an X{association}. 459 460 @param assoc: The association to send back. 461 @type assoc: L{openid.association.Association} 462 463 @returns: A response with the association information, encrypted 464 to the consumer's X{public key} if appropriate. 465 @returntype: L{OpenIDResponse} 466 """ 467 response = OpenIDResponse(self) 468 response.fields.updateArgs(OPENID_NS, { 469 'expires_in': '%d' % (assoc.getExpiresIn(),), 470 'assoc_type': self.assoc_type, 471 'assoc_handle': assoc.handle, 472 }) 473 response.fields.updateArgs(OPENID_NS, 474 self.session.answer(assoc.secret)) 475 476 if not (self.session.session_type == 'no-encryption' and 477 self.message.isOpenID1()): 478 # The session type "no-encryption" did not have a name 479 # in OpenID v1, it was just omitted. 480 response.fields.setArg( 481 OPENID_NS, 'session_type', self.session.session_type) 482 483 return response
484
485 - def answerUnsupported(self, message, preferred_association_type=None, 486 preferred_session_type=None):
487 """Respond to this request indicating that the association 488 type or association session type is not supported.""" 489 if self.message.isOpenID1(): 490 raise ProtocolError(self.message) 491 492 response = OpenIDResponse(self) 493 response.fields.setArg(OPENID_NS, 'error_code', 'unsupported-type') 494 response.fields.setArg(OPENID_NS, 'error', message) 495 496 if preferred_association_type: 497 response.fields.setArg( 498 OPENID_NS, 'assoc_type', preferred_association_type) 499 500 if preferred_session_type: 501 response.fields.setArg( 502 OPENID_NS, 'session_type', preferred_session_type) 503 504 return response
505
506 -class CheckIDRequest(OpenIDRequest):
507 """A request to confirm the identity of a user. 508 509 This class handles requests for openid modes X{C{checkid_immediate}} 510 and X{C{checkid_setup}}. 511 512 @cvar mode: "X{C{checkid_immediate}}" or "X{C{checkid_setup}}" 513 @type mode: str 514 515 @ivar immediate: Is this an immediate-mode request? 516 @type immediate: bool 517 518 @ivar identity: The OP-local identifier being checked. 519 @type identity: str 520 521 @ivar claimed_id: The claimed identifier. Not present in OpenID 1.x 522 messages. 523 @type claimed_id: str 524 525 @ivar trust_root: "Are you Frank?" asks the checkid request. "Who wants 526 to know?" C{trust_root}, that's who. This URL identifies the party 527 making the request, and the user will use that to make her decision 528 about what answer she trusts them to have. Referred to as "realm" in 529 OpenID 2.0. 530 @type trust_root: str 531 532 @ivar return_to: The URL to send the user agent back to to reply to this 533 request. 534 @type return_to: str 535 536 @ivar assoc_handle: Provided in smart mode requests, a handle for a 537 previously established association. C{None} for dumb mode requests. 538 @type assoc_handle: str 539 """ 540
541 - def __init__(self, identity, return_to, trust_root=None, immediate=False, 542 assoc_handle=None, op_endpoint=None, claimed_id=None):
543 """Construct me. 544 545 These parameters are assigned directly as class attributes, see 546 my L{class documentation<CheckIDRequest>} for their descriptions. 547 548 @raises MalformedReturnURL: When the C{return_to} URL is not a URL. 549 """ 550 self.assoc_handle = assoc_handle 551 self.identity = identity 552 self.claimed_id = claimed_id or identity 553 self.return_to = return_to 554 self.trust_root = trust_root or return_to 555 self.op_endpoint = op_endpoint 556 assert self.op_endpoint is not None 557 if immediate: 558 self.immediate = True 559 self.mode = "checkid_immediate" 560 else: 561 self.immediate = False 562 self.mode = "checkid_setup" 563 564 if self.return_to is not None and \ 565 not TrustRoot.parse(self.return_to): 566 raise MalformedReturnURL(None, self.return_to) 567 if not self.trustRootValid(): 568 raise UntrustedReturnURL(None, self.return_to, self.trust_root) 569 self.message = None
570
571 - def _getNamespace(self):
572 warnings.warn('The "namespace" attribute of CheckIDRequest objects ' 573 'is deprecated. Use "message.getOpenIDNamespace()" ' 574 'instead', DeprecationWarning, stacklevel=2) 575 return self.message.getOpenIDNamespace()
576 577 namespace = property(_getNamespace) 578
579 - def fromMessage(klass, message, op_endpoint):
580 """Construct me from an OpenID message. 581 582 @raises ProtocolError: When not all required parameters are present 583 in the message. 584 585 @raises MalformedReturnURL: When the C{return_to} URL is not a URL. 586 587 @raises UntrustedReturnURL: When the C{return_to} URL is outside 588 the C{trust_root}. 589 590 @param message: An OpenID checkid_* request Message 591 @type message: openid.message.Message 592 593 @param op_endpoint: The endpoint URL of the server that this 594 message was sent to. 595 @type op_endpoint: str 596 597 @returntype: L{CheckIDRequest} 598 """ 599 self = klass.__new__(klass) 600 self.message = message 601 self.op_endpoint = op_endpoint 602 mode = message.getArg(OPENID_NS, 'mode') 603 if mode == "checkid_immediate": 604 self.immediate = True 605 self.mode = "checkid_immediate" 606 else: 607 self.immediate = False 608 self.mode = "checkid_setup" 609 610 self.return_to = message.getArg(OPENID_NS, 'return_to') 611 if message.isOpenID1() and not self.return_to: 612 fmt = "Missing required field 'return_to' from %r" 613 raise ProtocolError(message, text=fmt % (message,)) 614 615 self.identity = message.getArg(OPENID_NS, 'identity') 616 self.claimed_id = message.getArg(OPENID_NS, 'claimed_id') 617 if message.isOpenID1(): 618 if self.identity is None: 619 s = "OpenID 1 message did not contain openid.identity" 620 raise ProtocolError(message, text=s) 621 else: 622 if self.identity and not self.claimed_id: 623 s = ("OpenID 2.0 message contained openid.identity but not " 624 "claimed_id") 625 raise ProtocolError(message, text=s) 626 elif self.claimed_id and not self.identity: 627 s = ("OpenID 2.0 message contained openid.claimed_id but not " 628 "identity") 629 raise ProtocolError(message, text=s) 630 631 # There's a case for making self.trust_root be a TrustRoot 632 # here. But if TrustRoot isn't currently part of the "public" API, 633 # I'm not sure it's worth doing. 634 635 if message.isOpenID1(): 636 trust_root_param = 'trust_root' 637 else: 638 trust_root_param = 'realm' 639 640 # Using 'or' here is slightly different than sending a default 641 # argument to getArg, as it will treat no value and an empty 642 # string as equivalent. 643 self.trust_root = (message.getArg(OPENID_NS, trust_root_param) 644 or self.return_to) 645 646 if not message.isOpenID1(): 647 if self.return_to is self.trust_root is None: 648 raise ProtocolError(message, "openid.realm required when " + 649 "openid.return_to absent") 650 651 self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') 652 653 # Using TrustRoot.parse here is a bit misleading, as we're not 654 # parsing return_to as a trust root at all. However, valid URLs 655 # are valid trust roots, so we can use this to get an idea if it 656 # is a valid URL. Not all trust roots are valid return_to URLs, 657 # however (particularly ones with wildcards), so this is still a 658 # little sketchy. 659 if self.return_to is not None and \ 660 not TrustRoot.parse(self.return_to): 661 raise MalformedReturnURL(message, self.return_to) 662 663 # I first thought that checking to see if the return_to is within 664 # the trust_root is premature here, a logic-not-decoding thing. But 665 # it was argued that this is really part of data validation. A 666 # request with an invalid trust_root/return_to is broken regardless of 667 # application, right? 668 if not self.trustRootValid(): 669 raise UntrustedReturnURL(message, self.return_to, self.trust_root) 670 671 return self
672 673 fromMessage = classmethod(fromMessage) 674
675 - def idSelect(self):
676 """Is the identifier to be selected by the IDP? 677 678 @returntype: bool 679 """ 680 # So IDPs don't have to import the constant 681 return self.identity == IDENTIFIER_SELECT
682
683 - def trustRootValid(self):
684 """Is my return_to under my trust_root? 685 686 @returntype: bool 687 """ 688 if not self.trust_root: 689 return True 690 tr = TrustRoot.parse(self.trust_root) 691 if tr is None: 692 raise MalformedTrustRoot(self.message, self.trust_root) 693 694 if self.return_to is not None: 695 return tr.validateURL(self.return_to) 696 else: 697 return True
698
699 - def returnToVerified(self):
700 """Does the relying party publish the return_to URL for this 701 response under the realm? It is up to the provider to set a 702 policy for what kinds of realms should be allowed. This 703 return_to URL verification reduces vulnerability to data-theft 704 attacks based on open proxies, cross-site-scripting, or open 705 redirectors. 706 707 This check should only be performed after making sure that the 708 return_to URL matches the realm. 709 710 @see: L{trustRootValid} 711 712 @raises openid.yadis.discover.DiscoveryFailure: if the realm 713 URL does not support Yadis discovery (and so does not 714 support the verification process). 715 716 @raises openid.fetchers.HTTPFetchingError: if the realm URL 717 is not reachable. When this is the case, the RP may be hosted 718 on the user's intranet. 719 720 @returntype: bool 721 722 @returns: True if the realm publishes a document with the 723 return_to URL listed 724 725 @since: 2.1.0 726 """ 727 return verifyReturnTo(self.trust_root, self.return_to)
728
729 - def answer(self, allow, server_url=None, identity=None, claimed_id=None):
730 """Respond to this request. 731 732 @param allow: Allow this user to claim this identity, and allow the 733 consumer to have this information? 734 @type allow: bool 735 736 @param server_url: DEPRECATED. Passing C{op_endpoint} to the 737 L{Server} constructor makes this optional. 738 739 When an OpenID 1.x immediate mode request does not succeed, 740 it gets back a URL where the request may be carried out 741 in a not-so-immediate fashion. Pass my URL in here (the 742 fully qualified address of this server's endpoint, i.e. 743 C{http://example.com/server}), and I will use it as a base for the 744 URL for a new request. 745 746 Optional for requests where C{CheckIDRequest.immediate} is C{False} 747 or C{allow} is C{True}. 748 749 @type server_url: str 750 751 @param identity: The OP-local identifier to answer with. Only for use 752 when the relying party requested identifier selection. 753 @type identity: str or None 754 755 @param claimed_id: The claimed identifier to answer with, for use 756 with identifier selection in the case where the claimed identifier 757 and the OP-local identifier differ, i.e. when the claimed_id uses 758 delegation. 759 760 If C{identity} is provided but this is not, C{claimed_id} will 761 default to the value of C{identity}. When answering requests 762 that did not ask for identifier selection, the response 763 C{claimed_id} will default to that of the request. 764 765 This parameter is new in OpenID 2.0. 766 @type claimed_id: str or None 767 768 @returntype: L{OpenIDResponse} 769 770 @change: Version 2.0 deprecates C{server_url} and adds C{claimed_id}. 771 772 @raises NoReturnError: when I do not have a return_to. 773 """ 774 assert self.message is not None 775 776 if not self.return_to: 777 raise NoReturnToError 778 779 if not server_url: 780 if not self.message.isOpenID1() and not self.op_endpoint: 781 # In other words, that warning I raised in Server.__init__? 782 # You should pay attention to it now. 783 raise RuntimeError("%s should be constructed with op_endpoint " 784 "to respond to OpenID 2.0 messages." % 785 (self,)) 786 server_url = self.op_endpoint 787 788 if allow: 789 mode = 'id_res' 790 elif self.message.isOpenID1(): 791 if self.immediate: 792 mode = 'id_res' 793 else: 794 mode = 'cancel' 795 else: 796 if self.immediate: 797 mode = 'setup_needed' 798 else: 799 mode = 'cancel' 800 801 response = OpenIDResponse(self) 802 803 if claimed_id and self.message.isOpenID1(): 804 namespace = self.message.getOpenIDNamespace() 805 raise VersionError("claimed_id is new in OpenID 2.0 and not " 806 "available for %s" % (namespace,)) 807 808 if allow: 809 if self.identity == IDENTIFIER_SELECT: 810 if not identity: 811 raise ValueError( 812 "This request uses IdP-driven identifier selection." 813 "You must supply an identifier in the response.") 814 response_identity = identity 815 response_claimed_id = claimed_id or identity 816 817 elif self.identity: 818 if identity and (self.identity != identity): 819 normalized_request_identity = urinorm(self.identity) 820 normalized_answer_identity = urinorm(identity) 821 822 if (normalized_request_identity != 823 normalized_answer_identity): 824 raise ValueError( 825 "Request was for identity %r, cannot reply " 826 "with identity %r" % (self.identity, identity)) 827 828 # The "identity" value in the response shall always be 829 # the same as that in the request, otherwise the RP is 830 # likely to not validate the response. 831 response_identity = self.identity 832 response_claimed_id = self.claimed_id 833 else: 834 if identity: 835 raise ValueError( 836 "This request specified no identity and you " 837 "supplied %r" % (identity,)) 838 response_identity = None 839 840 if self.message.isOpenID1() and response_identity is None: 841 raise ValueError( 842 "Request was an OpenID 1 request, so response must " 843 "include an identifier." 844 ) 845 846 response.fields.updateArgs(OPENID_NS, { 847 'mode': mode, 848 'return_to': self.return_to, 849 'response_nonce': mkNonce(), 850 }) 851 852 if server_url: 853 response.fields.setArg(OPENID_NS, 'op_endpoint', server_url) 854 855 if response_identity is not None: 856 response.fields.setArg( 857 OPENID_NS, 'identity', response_identity) 858 if self.message.isOpenID2(): 859 response.fields.setArg( 860 OPENID_NS, 'claimed_id', response_claimed_id) 861 else: 862 response.fields.setArg(OPENID_NS, 'mode', mode) 863 if self.immediate: 864 if self.message.isOpenID1() and not server_url: 865 raise ValueError("setup_url is required for allow=False " 866 "in OpenID 1.x immediate mode.") 867 # Make a new request just like me, but with immediate=False. 868 setup_request = self.__class__( 869 self.identity, self.return_to, self.trust_root, 870 immediate=False, assoc_handle=self.assoc_handle, 871 op_endpoint=self.op_endpoint, claimed_id=self.claimed_id) 872 873 # XXX: This API is weird. 874 setup_request.message = self.message 875 876 setup_url = setup_request.encodeToURL(server_url) 877 response.fields.setArg(OPENID_NS, 'user_setup_url', setup_url) 878 879 return response
880 881
882 - def encodeToURL(self, server_url):
883 """Encode this request as a URL to GET. 884 885 @param server_url: The URL of the OpenID server to make this request of. 886 @type server_url: str 887 888 @returntype: str 889 890 @raises NoReturnError: when I do not have a return_to. 891 """ 892 if not self.return_to: 893 raise NoReturnToError 894 895 # Imported from the alternate reality where these classes are used 896 # in both the client and server code, so Requests are Encodable too. 897 # That's right, code imported from alternate realities all for the 898 # love of you, id_res/user_setup_url. 899 q = {'mode': self.mode, 900 'identity': self.identity, 901 'claimed_id': self.claimed_id, 902 'return_to': self.return_to} 903 if self.trust_root: 904 if self.message.isOpenID1(): 905 q['trust_root'] = self.trust_root 906 else: 907 q['realm'] = self.trust_root 908 if self.assoc_handle: 909 q['assoc_handle'] = self.assoc_handle 910 911 response = Message(self.message.getOpenIDNamespace()) 912 response.updateArgs(OPENID_NS, q) 913 return response.toURL(server_url)
914 915
916 - def getCancelURL(self):
917 """Get the URL to cancel this request. 918 919 Useful for creating a "Cancel" button on a web form so that operation 920 can be carried out directly without another trip through the server. 921 922 (Except you probably want to make another trip through the server so 923 that it knows that the user did make a decision. Or you could simulate 924 this method by doing C{.answer(False).encodeToURL()}) 925 926 @returntype: str 927 @returns: The return_to URL with openid.mode = cancel. 928 929 @raises NoReturnError: when I do not have a return_to. 930 """ 931 if not self.return_to: 932 raise NoReturnToError 933 934 if self.immediate: 935 raise ValueError("Cancel is not an appropriate response to " 936 "immediate mode requests.") 937 938 response = Message(self.message.getOpenIDNamespace()) 939 response.setArg(OPENID_NS, 'mode', 'cancel') 940 return response.toURL(self.return_to)
941 942
943 - def __repr__(self):
944 return '<%s id:%r im:%s tr:%r ah:%r>' % (self.__class__.__name__, 945 self.identity, 946 self.immediate, 947 self.trust_root, 948 self.assoc_handle)
949 950 951
952 -class OpenIDResponse(object):
953 """I am a response to an OpenID request. 954 955 @ivar request: The request I respond to. 956 @type request: L{OpenIDRequest} 957 958 @ivar fields: My parameters as a dictionary with each key mapping to 959 one value. Keys are parameter names with no leading "C{openid.}". 960 e.g. "C{identity}" and "C{mac_key}", never "C{openid.identity}". 961 @type fields: L{openid.message.Message} 962 963 @ivar signed: The names of the fields which should be signed. 964 @type signed: list of str 965 """ 966 967 # Implementer's note: In a more symmetric client/server 968 # implementation, there would be more types of OpenIDResponse 969 # object and they would have validated attributes according to the 970 # type of response. But as it is, Response objects in a server are 971 # basically write-only, their only job is to go out over the wire, 972 # so this is just a loose wrapper around OpenIDResponse.fields. 973
974 - def __init__(self, request):
975 """Make a response to an L{OpenIDRequest}. 976 977 @type request: L{OpenIDRequest} 978 """ 979 self.request = request 980 self.fields = Message(request.namespace)
981
982 - def __str__(self):
983 return "%s for %s: %s" % ( 984 self.__class__.__name__, 985 self.request.__class__.__name__, 986 self.fields)
987 988
989 - def toFormMarkup(self, form_tag_attrs=None):
990 """Returns the form markup for this response. 991 992 @param form_tag_attrs: Dictionary of attributes to be added to 993 the form tag. 'accept-charset' and 'enctype' have defaults 994 that can be overridden. If a value is supplied for 995 'action' or 'method', it will be replaced. 996 997 @returntype: str 998 999 @since: 2.1.0 1000 """ 1001 return self.fields.toFormMarkup(self.request.return_to, 1002 form_tag_attrs=form_tag_attrs)
1003
1004 - def toHTML(self, form_tag_attrs=None):
1005 """Returns an HTML document that auto-submits the form markup 1006 for this response. 1007 1008 @returntype: str 1009 1010 @see: toFormMarkup 1011 1012 @since: 2.1.? 1013 """ 1014 return oidutil.autoSubmitHTML(self.toFormMarkup(form_tag_attrs))
1015
1016 - def renderAsForm(self):
1017 """Returns True if this response's encoding is 1018 ENCODE_HTML_FORM. Convenience method for server authors. 1019 1020 @returntype: bool 1021 1022 @since: 2.1.0 1023 """ 1024 return self.whichEncoding() == ENCODE_HTML_FORM
1025 1026
1027 - def needsSigning(self):
1028 """Does this response require signing? 1029 1030 @returntype: bool 1031 """ 1032 return self.fields.getArg(OPENID_NS, 'mode') == 'id_res'
1033 1034 1035 # implements IEncodable 1036
1037 - def whichEncoding(self):
1038 """How should I be encoded? 1039 1040 @returns: one of ENCODE_URL, ENCODE_HTML_FORM, or ENCODE_KVFORM. 1041 1042 @change: 2.1.0 added the ENCODE_HTML_FORM response. 1043 """ 1044 if self.request.mode in BROWSER_REQUEST_MODES: 1045 if self.fields.getOpenIDNamespace() == OPENID2_NS and \ 1046 len(self.encodeToURL()) > OPENID1_URL_LIMIT: 1047 return ENCODE_HTML_FORM 1048 else: 1049 return ENCODE_URL 1050 else: 1051 return ENCODE_KVFORM
1052 1053
1054 - def encodeToURL(self):
1055 """Encode a response as a URL for the user agent to GET. 1056 1057 You will generally use this URL with a HTTP redirect. 1058 1059 @returns: A URL to direct the user agent back to. 1060 @returntype: str 1061 """ 1062 return self.fields.toURL(self.request.return_to)
1063 1064
1065 - def addExtension(self, extension_response):
1066 """ 1067 Add an extension response to this response message. 1068 1069 @param extension_response: An object that implements the 1070 extension interface for adding arguments to an OpenID 1071 message. 1072 @type extension_response: L{openid.extension} 1073 1074 @returntype: None 1075 """ 1076 extension_response.toMessage(self.fields)
1077 1078
1079 - def encodeToKVForm(self):
1080 """Encode a response in key-value colon/newline format. 1081 1082 This is a machine-readable format used to respond to messages which 1083 came directly from the consumer and not through the user agent. 1084 1085 @see: OpenID Specs, 1086 U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>} 1087 1088 @returntype: str 1089 """ 1090 return self.fields.toKVForm()
1091 1092 1093
1094 -class WebResponse(object):
1095 """I am a response to an OpenID request in terms a web server understands. 1096 1097 I generally come from an L{Encoder}, either directly or from 1098 L{Server.encodeResponse}. 1099 1100 @ivar code: The HTTP code of this response. 1101 @type code: int 1102 1103 @ivar headers: Headers to include in this response. 1104 @type headers: dict 1105 1106 @ivar body: The body of this response. 1107 @type body: str 1108 """ 1109
1110 - def __init__(self, code=HTTP_OK, headers=None, body=""):
1111 """Construct me. 1112 1113 These parameters are assigned directly as class attributes, see 1114 my L{class documentation<WebResponse>} for their descriptions. 1115 """ 1116 self.code = code 1117 if headers is not None: 1118 self.headers = headers 1119 else: 1120 self.headers = {} 1121 self.body = body
1122 1123 1124
1125 -class Signatory(object):
1126 """I sign things. 1127 1128 I also check signatures. 1129 1130 All my state is encapsulated in an 1131 L{OpenIDStore<openid.store.interface.OpenIDStore>}, which means 1132 I'm not generally pickleable but I am easy to reconstruct. 1133 1134 @cvar SECRET_LIFETIME: The number of seconds a secret remains valid. 1135 @type SECRET_LIFETIME: int 1136 """ 1137 1138 SECRET_LIFETIME = 14 * 24 * 60 * 60 # 14 days, in seconds 1139 1140 # keys have a bogus server URL in them because the filestore 1141 # really does expect that key to be a URL. This seems a little 1142 # silly for the server store, since I expect there to be only one 1143 # server URL. 1144 _normal_key = 'http://localhost/|normal' 1145 _dumb_key = 'http://localhost/|dumb' 1146 1147
1148 - def __init__(self, store):
1149 """Create a new Signatory. 1150 1151 @param store: The back-end where my associations are stored. 1152 @type store: L{openid.store.interface.OpenIDStore} 1153 """ 1154 assert store is not None 1155 self.store = store
1156 1157
1158 - def verify(self, assoc_handle, message):
1159 """Verify that the signature for some data is valid. 1160 1161 @param assoc_handle: The handle of the association used to sign the 1162 data. 1163 @type assoc_handle: str 1164 1165 @param message: The signed message to verify 1166 @type message: openid.message.Message 1167 1168 @returns: C{True} if the signature is valid, C{False} if not. 1169 @returntype: bool 1170 """ 1171 assoc = self.getAssociation(assoc_handle, dumb=True) 1172 if not assoc: 1173 oidutil.log("failed to get assoc with handle %r to verify " 1174 "message %r" 1175 % (assoc_handle, message)) 1176 return False 1177 1178 try: 1179 valid = assoc.checkMessageSignature(message) 1180 except ValueError, ex: 1181 oidutil.log("Error in verifying %s with %s: %s" % (message, 1182 assoc, 1183 ex)) 1184 return False 1185 return valid
1186 1187
1188 - def sign(self, response):
1189 """Sign a response. 1190 1191 I take a L{OpenIDResponse}, create a signature for everything 1192 in its L{signed<OpenIDResponse.signed>} list, and return a new 1193 copy of the response object with that signature included. 1194 1195 @param response: A response to sign. 1196 @type response: L{OpenIDResponse} 1197 1198 @returns: A signed copy of the response. 1199 @returntype: L{OpenIDResponse} 1200 """ 1201 signed_response = deepcopy(response) 1202 assoc_handle = response.request.assoc_handle 1203 if assoc_handle: 1204 # normal mode 1205 # disabling expiration check because even if the association 1206 # is expired, we still need to know some properties of the 1207 # association so that we may preserve those properties when 1208 # creating the fallback association. 1209 assoc = self.getAssociation(assoc_handle, dumb=False, 1210 checkExpiration=False) 1211 1212 if not assoc or assoc.expiresIn <= 0: 1213 # fall back to dumb mode 1214 signed_response.fields.setArg( 1215 OPENID_NS, 'invalidate_handle', assoc_handle) 1216 assoc_type = assoc and assoc.assoc_type or 'HMAC-SHA1' 1217 if assoc and assoc.expiresIn <= 0: 1218 # now do the clean-up that the disabled checkExpiration 1219 # code didn't get to do. 1220 self.invalidate(assoc_handle, dumb=False) 1221 assoc = self.createAssociation(dumb=True, assoc_type=assoc_type) 1222 else: 1223 # dumb mode. 1224 assoc = self.createAssociation(dumb=True) 1225 1226 try: 1227 signed_response.fields = assoc.signMessage(signed_response.fields) 1228 except kvform.KVFormError, err: 1229 raise EncodingError(response, explanation=str(err)) 1230 return signed_response
1231 1232
1233 - def createAssociation(self, dumb=True, assoc_type='HMAC-SHA1'):
1234 """Make a new association. 1235 1236 @param dumb: Is this association for a dumb-mode transaction? 1237 @type dumb: bool 1238 1239 @param assoc_type: The type of association to create. Currently 1240 there is only one type defined, C{HMAC-SHA1}. 1241 @type assoc_type: str 1242 1243 @returns: the new association. 1244 @returntype: L{openid.association.Association} 1245 """ 1246 secret = cryptutil.getBytes(getSecretSize(assoc_type)) 1247 uniq = oidutil.toBase64(cryptutil.getBytes(4)) 1248 handle = '{%s}{%x}{%s}' % (assoc_type, int(time.time()), uniq) 1249 1250 assoc = Association.fromExpiresIn( 1251 self.SECRET_LIFETIME, handle, secret, assoc_type) 1252 1253 if dumb: 1254 key = self._dumb_key 1255 else: 1256 key = self._normal_key 1257 self.store.storeAssociation(key, assoc) 1258 return assoc
1259 1260
1261 - def getAssociation(self, assoc_handle, dumb, checkExpiration=True):
1262 """Get the association with the specified handle. 1263 1264 @type assoc_handle: str 1265 1266 @param dumb: Is this association used with dumb mode? 1267 @type dumb: bool 1268 1269 @returns: the association, or None if no valid association with that 1270 handle was found. 1271 @returntype: L{openid.association.Association} 1272 """ 1273 # Hmm. We've created an interface that deals almost entirely with 1274 # assoc_handles. The only place outside the Signatory that uses this 1275 # (and thus the only place that ever sees Association objects) is 1276 # when creating a response to an association request, as it must have 1277 # the association's secret. 1278 1279 if assoc_handle is None: 1280 raise ValueError("assoc_handle must not be None") 1281 1282 if dumb: 1283 key = self._dumb_key 1284 else: 1285 key = self._normal_key 1286 assoc = self.store.getAssociation(key, assoc_handle) 1287 if assoc is not None and assoc.expiresIn <= 0: 1288 oidutil.log("requested %sdumb key %r is expired (by %s seconds)" % 1289 ((not dumb) and 'not-' or '', 1290 assoc_handle, assoc.expiresIn)) 1291 if checkExpiration: 1292 self.store.removeAssociation(key, assoc_handle) 1293 assoc = None 1294 return assoc
1295 1296
1297 - def invalidate(self, assoc_handle, dumb):
1298 """Invalidates the association with the given handle. 1299 1300 @type assoc_handle: str 1301 1302 @param dumb: Is this association used with dumb mode? 1303 @type dumb: bool 1304 """ 1305 if dumb: 1306 key = self._dumb_key 1307 else: 1308 key = self._normal_key 1309 self.store.removeAssociation(key, assoc_handle)
1310 1311 1312
1313 -class Encoder(object):
1314 """I encode responses in to L{WebResponses<WebResponse>}. 1315 1316 If you don't like L{WebResponses<WebResponse>}, you can do 1317 your own handling of L{OpenIDResponses<OpenIDResponse>} with 1318 L{OpenIDResponse.whichEncoding}, L{OpenIDResponse.encodeToURL}, and 1319 L{OpenIDResponse.encodeToKVForm}. 1320 """ 1321 1322 responseFactory = WebResponse 1323 1324
1325 - def encode(self, response):
1326 """Encode a response to a L{WebResponse}. 1327 1328 @raises EncodingError: When I can't figure out how to encode this 1329 message. 1330 """ 1331 encode_as = response.whichEncoding() 1332 if encode_as == ENCODE_KVFORM: 1333 wr = self.responseFactory(body=response.encodeToKVForm()) 1334 if isinstance(response, Exception): 1335 wr.code = HTTP_ERROR 1336 elif encode_as == ENCODE_URL: 1337 location = response.encodeToURL() 1338 wr = self.responseFactory(code=HTTP_REDIRECT, 1339 headers={'location': location}) 1340 elif encode_as == ENCODE_HTML_FORM: 1341 wr = self.responseFactory(code=HTTP_OK, 1342 body=response.toFormMarkup()) 1343 else: 1344 # Can't encode this to a protocol message. You should probably 1345 # render it to HTML and show it to the user. 1346 raise EncodingError(response) 1347 return wr
1348 1349 1350
1351 -class SigningEncoder(Encoder):
1352 """I encode responses in to L{WebResponses<WebResponse>}, signing them when required. 1353 """ 1354
1355 - def __init__(self, signatory):
1356 """Create a L{SigningEncoder}. 1357 1358 @param signatory: The L{Signatory} I will make signatures with. 1359 @type signatory: L{Signatory} 1360 """ 1361 self.signatory = signatory
1362 1363
1364 - def encode(self, response):
1365 """Encode a response to a L{WebResponse}, signing it first if appropriate. 1366 1367 @raises EncodingError: When I can't figure out how to encode this 1368 message. 1369 1370 @raises AlreadySigned: When this response is already signed. 1371 1372 @returntype: L{WebResponse} 1373 """ 1374 # the isinstance is a bit of a kludge... it means there isn't really 1375 # an adapter to make the interfaces quite match. 1376 if (not isinstance(response, Exception)) and response.needsSigning(): 1377 if not self.signatory: 1378 raise ValueError( 1379 "Must have a store to sign this request: %s" % 1380 (response,), response) 1381 if response.fields.hasKey(OPENID_NS, 'sig'): 1382 raise AlreadySigned(response) 1383 response = self.signatory.sign(response) 1384 return super(SigningEncoder, self).encode(response)
1385 1386 1387
1388 -class Decoder(object):
1389 """I decode an incoming web request in to a L{OpenIDRequest}. 1390 """ 1391 1392 _handlers = { 1393 'checkid_setup': CheckIDRequest.fromMessage, 1394 'checkid_immediate': CheckIDRequest.fromMessage, 1395 'check_authentication': CheckAuthRequest.fromMessage, 1396 'associate': AssociateRequest.fromMessage, 1397 } 1398
1399 - def __init__(self, server):
1400 """Construct a Decoder. 1401 1402 @param server: The server which I am decoding requests for. 1403 (Necessary because some replies reference their server.) 1404 @type server: L{Server} 1405 """ 1406 self.server = server
1407
1408 - def decode(self, query):
1409 """I transform query parameters into an L{OpenIDRequest}. 1410 1411 If the query does not seem to be an OpenID request at all, I return 1412 C{None}. 1413 1414 @param query: The query parameters as a dictionary with each 1415 key mapping to one value. 1416 @type query: dict 1417 1418 @raises ProtocolError: When the query does not seem to be a valid 1419 OpenID request. 1420 1421 @returntype: L{OpenIDRequest} 1422 """ 1423 if not query: 1424 return None 1425 1426 try: 1427 message = Message.fromPostArgs(query) 1428 except InvalidOpenIDNamespace, err: 1429 # It's useful to have a Message attached to a ProtocolError, so we 1430 # override the bad ns value to build a Message out of it. Kinda 1431 # kludgy, since it's made of lies, but the parts that aren't lies 1432 # are more useful than a 'None'. 1433 query = query.copy() 1434 query['openid.ns'] = OPENID2_NS 1435 message = Message.fromPostArgs(query) 1436 raise ProtocolError(message, str(err)) 1437 1438 mode = message.getArg(OPENID_NS, 'mode') 1439 if not mode: 1440 fmt = "No mode value in message %s" 1441 raise ProtocolError(message, text=fmt % (message,)) 1442 1443 handler = self._handlers.get(mode, self.defaultDecoder) 1444 return handler(message, self.server.op_endpoint)
1445 1446
1447 - def defaultDecoder(self, message, server):
1448 """Called to decode queries when no handler for that mode is found. 1449 1450 @raises ProtocolError: This implementation always raises 1451 L{ProtocolError}. 1452 """ 1453 mode = message.getArg(OPENID_NS, 'mode') 1454 fmt = "Unrecognized OpenID mode %r" 1455 raise ProtocolError(message, text=fmt % (mode,))
1456 1457 1458
1459 -class Server(object):
1460 """I handle requests for an OpenID server. 1461 1462 Some types of requests (those which are not C{checkid} requests) may be 1463 handed to my L{handleRequest} method, and I will take care of it and 1464 return a response. 1465 1466 For your convenience, I also provide an interface to L{Decoder.decode} 1467 and L{SigningEncoder.encode} through my methods L{decodeRequest} and 1468 L{encodeResponse}. 1469 1470 All my state is encapsulated in an 1471 L{OpenIDStore<openid.store.interface.OpenIDStore>}, which means 1472 I'm not generally pickleable but I am easy to reconstruct. 1473 1474 Example:: 1475 1476 oserver = Server(FileOpenIDStore(data_path), "http://example.com/op") 1477 request = oserver.decodeRequest(query) 1478 if request.mode in ['checkid_immediate', 'checkid_setup']: 1479 if self.isAuthorized(request.identity, request.trust_root): 1480 response = request.answer(True) 1481 elif request.immediate: 1482 response = request.answer(False) 1483 else: 1484 self.showDecidePage(request) 1485 return 1486 else: 1487 response = oserver.handleRequest(request) 1488 1489 webresponse = oserver.encode(response) 1490 1491 @ivar signatory: I'm using this for associate requests and to sign things. 1492 @type signatory: L{Signatory} 1493 1494 @ivar decoder: I'm using this to decode things. 1495 @type decoder: L{Decoder} 1496 1497 @ivar encoder: I'm using this to encode things. 1498 @type encoder: L{Encoder} 1499 1500 @ivar op_endpoint: My URL. 1501 @type op_endpoint: str 1502 1503 @ivar negotiator: I use this to determine which kinds of 1504 associations I can make and how. 1505 @type negotiator: L{openid.association.SessionNegotiator} 1506 """ 1507 1508 signatoryClass = Signatory 1509 encoderClass = SigningEncoder 1510 decoderClass = Decoder 1511
1512 - def __init__(self, store, op_endpoint=None):
1513 """A new L{Server}. 1514 1515 @param store: The back-end where my associations are stored. 1516 @type store: L{openid.store.interface.OpenIDStore} 1517 1518 @param op_endpoint: My URL, the fully qualified address of this 1519 server's endpoint, i.e. C{http://example.com/server} 1520 @type op_endpoint: str 1521 1522 @change: C{op_endpoint} is new in library version 2.0. It 1523 currently defaults to C{None} for compatibility with 1524 earlier versions of the library, but you must provide it 1525 if you want to respond to any version 2 OpenID requests. 1526 """ 1527 self.store = store 1528 self.signatory = self.signatoryClass(self.store) 1529 self.encoder = self.encoderClass(self.signatory) 1530 self.decoder = self.decoderClass(self) 1531 self.negotiator = default_negotiator.copy() 1532 1533 if not op_endpoint: 1534 warnings.warn("%s.%s constructor requires op_endpoint parameter " 1535 "for OpenID 2.0 servers" % 1536 (self.__class__.__module__, self.__class__.__name__), 1537 stacklevel=2) 1538 self.op_endpoint = op_endpoint
1539 1540
1541 - def handleRequest(self, request):
1542 """Handle a request. 1543 1544 Give me a request, I will give you a response. Unless it's a type 1545 of request I cannot handle myself, in which case I will raise 1546 C{NotImplementedError}. In that case, you can handle it yourself, 1547 or add a method to me for handling that request type. 1548 1549 @raises NotImplementedError: When I do not have a handler defined 1550 for that type of request. 1551 1552 @returntype: L{OpenIDResponse} 1553 """ 1554 handler = getattr(self, 'openid_' + request.mode, None) 1555 if handler is not None: 1556 return handler(request) 1557 else: 1558 raise NotImplementedError( 1559 "%s has no handler for a request of mode %r." % 1560 (self, request.mode))
1561 1562
1563 - def openid_check_authentication(self, request):
1564 """Handle and respond to C{check_authentication} requests. 1565 1566 @returntype: L{OpenIDResponse} 1567 """ 1568 return request.answer(self.signatory)
1569 1570
1571 - def openid_associate(self, request):
1572 """Handle and respond to C{associate} requests. 1573 1574 @returntype: L{OpenIDResponse} 1575 """ 1576 # XXX: TESTME 1577 assoc_type = request.assoc_type 1578 session_type = request.session.session_type 1579 if self.negotiator.isAllowed(assoc_type, session_type): 1580 assoc = self.signatory.createAssociation(dumb=False, 1581 assoc_type=assoc_type) 1582 return request.answer(assoc) 1583 else: 1584 message = ('Association type %r is not supported with ' 1585 'session type %r' % (assoc_type, session_type)) 1586 (preferred_assoc_type, preferred_session_type) = \ 1587 self.negotiator.getAllowedType() 1588 return request.answerUnsupported( 1589 message, 1590 preferred_assoc_type, 1591 preferred_session_type)
1592 1593
1594 - def decodeRequest(self, query):
1595 """Transform query parameters into an L{OpenIDRequest}. 1596 1597 If the query does not seem to be an OpenID request at all, I return 1598 C{None}. 1599 1600 @param query: The query parameters as a dictionary with each 1601 key mapping to one value. 1602 @type query: dict 1603 1604 @raises ProtocolError: When the query does not seem to be a valid 1605 OpenID request. 1606 1607 @returntype: L{OpenIDRequest} 1608 1609 @see: L{Decoder.decode} 1610 """ 1611 return self.decoder.decode(query)
1612 1613
1614 - def encodeResponse(self, response):
1615 """Encode a response to a L{WebResponse}, signing it first if appropriate. 1616 1617 @raises EncodingError: When I can't figure out how to encode this 1618 message. 1619 1620 @raises AlreadySigned: When this response is already signed. 1621 1622 @returntype: L{WebResponse} 1623 1624 @see: L{SigningEncoder.encode} 1625 """ 1626 return self.encoder.encode(response)
1627 1628 1629
1630 -class ProtocolError(Exception):
1631 """A message did not conform to the OpenID protocol. 1632 1633 @ivar message: The query that is failing to be a valid OpenID request. 1634 @type message: openid.message.Message 1635 """ 1636
1637 - def __init__(self, message, text=None, reference=None, contact=None):
1638 """When an error occurs. 1639 1640 @param message: The message that is failing to be a valid 1641 OpenID request. 1642 @type message: openid.message.Message 1643 1644 @param text: A message about the encountered error. Set as C{args[0]}. 1645 @type text: str 1646 """ 1647 self.openid_message = message 1648 self.reference = reference 1649 self.contact = contact 1650 assert type(message) not in [str, unicode] 1651 Exception.__init__(self, text)
1652 1653
1654 - def getReturnTo(self):
1655 """Get the return_to argument from the request, if any. 1656 1657 @returntype: str 1658 """ 1659 if self.openid_message is None: 1660 return None 1661 else: 1662 return self.openid_message.getArg(OPENID_NS, 'return_to')
1663
1664 - def hasReturnTo(self):
1665 """Did this request have a return_to parameter? 1666 1667 @returntype: bool 1668 """ 1669 return self.getReturnTo() is not None
1670
1671 - def toMessage(self):
1672 """Generate a Message object for sending to the relying party, 1673 after encoding. 1674 """ 1675 namespace = self.openid_message.getOpenIDNamespace() 1676 reply = Message(namespace) 1677 reply.setArg(OPENID_NS, 'mode', 'error') 1678 reply.setArg(OPENID_NS, 'error', str(self)) 1679 1680 if self.contact is not None: 1681 reply.setArg(OPENID_NS, 'contact', str(self.contact)) 1682 1683 if self.reference is not None: 1684 reply.setArg(OPENID_NS, 'reference', str(self.reference)) 1685 1686 return reply
1687 1688 # implements IEncodable 1689
1690 - def encodeToURL(self):
1691 return self.toMessage().toURL(self.getReturnTo())
1692
1693 - def encodeToKVForm(self):
1694 return self.toMessage().toKVForm()
1695
1696 - def toFormMarkup(self):
1697 """Encode to HTML form markup for POST. 1698 1699 @since: 2.1.0 1700 """ 1701 return self.toMessage().toFormMarkup(self.getReturnTo())
1702
1703 - def toHTML(self):
1704 """Encode to a full HTML page, wrapping the form markup in a page 1705 that will autosubmit the form. 1706 1707 @since: 2.1.? 1708 """ 1709 return oidutil.autoSubmitHTML(self.toFormMarkup())
1710
1711 - def whichEncoding(self):
1712 """How should I be encoded? 1713 1714 @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None, 1715 I cannot be encoded as a protocol message and should be 1716 displayed to the user. 1717 """ 1718 if self.hasReturnTo(): 1719 if self.openid_message.getOpenIDNamespace() == OPENID2_NS and \ 1720 len(self.encodeToURL()) > OPENID1_URL_LIMIT: 1721 return ENCODE_HTML_FORM 1722 else: 1723 return ENCODE_URL 1724 1725 if self.openid_message is None: 1726 return None 1727 1728 mode = self.openid_message.getArg(OPENID_NS, 'mode') 1729 if mode: 1730 if mode not in BROWSER_REQUEST_MODES: 1731 return ENCODE_KVFORM 1732 1733 # According to the OpenID spec as of this writing, we are probably 1734 # supposed to switch on request type here (GET versus POST) to figure 1735 # out if we're supposed to print machine-readable or human-readable 1736 # content at this point. GET/POST seems like a pretty lousy way of 1737 # making the distinction though, as it's just as possible that the 1738 # user agent could have mistakenly been directed to post to the 1739 # server URL. 1740 1741 # Basically, if your request was so broken that you didn't manage to 1742 # include an openid.mode, I'm not going to worry too much about 1743 # returning you something you can't parse. 1744 return None
1745 1746 1747
1748 -class VersionError(Exception):
1749 """Raised when an operation was attempted that is not compatible with 1750 the protocol version being used."""
1751 1752 1753
1754 -class NoReturnToError(Exception):
1755 """Raised when a response to a request cannot be generated because 1756 the request contains no return_to URL. 1757 """ 1758 pass
1759 1760 1761
1762 -class EncodingError(Exception):
1763 """Could not encode this as a protocol message. 1764 1765 You should probably render it and show it to the user. 1766 1767 @ivar response: The response that failed to encode. 1768 @type response: L{OpenIDResponse} 1769 """ 1770
1771 - def __init__(self, response, explanation=None):
1772 Exception.__init__(self, response) 1773 self.response = response 1774 self.explanation = explanation
1775
1776 - def __str__(self):
1777 if self.explanation: 1778 s = '%s: %s' % (self.__class__.__name__, 1779 self.explanation) 1780 else: 1781 s = '%s for Response %s' % ( 1782 self.__class__.__name__, self.response) 1783 return s
1784 1785
1786 -class AlreadySigned(EncodingError):
1787 """This response is already signed."""
1788 1789 1790
1791 -class UntrustedReturnURL(ProtocolError):
1792 """A return_to is outside the trust_root.""" 1793
1794 - def __init__(self, message, return_to, trust_root):
1795 ProtocolError.__init__(self, message) 1796 self.return_to = return_to 1797 self.trust_root = trust_root
1798
1799 - def __str__(self):
1800 return "return_to %r not under trust_root %r" % (self.return_to, 1801 self.trust_root)
1802 1803
1804 -class MalformedReturnURL(ProtocolError):
1805 """The return_to URL doesn't look like a valid URL."""
1806 - def __init__(self, openid_message, return_to):
1807 self.return_to = return_to 1808 ProtocolError.__init__(self, openid_message)
1809 1810 1811
1812 -class MalformedTrustRoot(ProtocolError):
1813 """The trust root is not well-formed. 1814 1815 @see: OpenID Specs, U{openid.trust_root<http://openid.net/specs.bml#mode-checkid_immediate>} 1816 """ 1817 pass
1818 1819 1820 #class IEncodable: # Interface 1821 # def encodeToURL(return_to): 1822 # """Encode a response as a URL for redirection. 1823 # 1824 # @returns: A URL to direct the user agent back to. 1825 # @returntype: str 1826 # """ 1827 # pass 1828 # 1829 # def encodeToKvform(): 1830 # """Encode a response in key-value colon/newline format. 1831 # 1832 # This is a machine-readable format used to respond to messages which 1833 # came directly from the consumer and not through the user agent. 1834 # 1835 # @see: OpenID Specs, 1836 # U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>} 1837 # 1838 # @returntype: str 1839 # """ 1840 # pass 1841 # 1842 # def whichEncoding(): 1843 # """How should I be encoded? 1844 # 1845 # @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None, 1846 # I cannot be encoded as a protocol message and should be 1847 # displayed to the user. 1848 # """ 1849 # pass 1850