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