1
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
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
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
201
202 fromMessage = classmethod(fromMessage)
203
204
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
220
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
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
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
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
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
348
353
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
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
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
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
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
600
601
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
616
617
618
619
620
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
626
627
628
629
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
638 """Is the identifier to be selected by the IDP?
639
640 @returntype: bool
641 """
642
643 return self.identity == IDENTIFIER_SELECT
644
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
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
711
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
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
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
811
812
813
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
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
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
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
880
881
882
883
884
885
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
895 return "%s for %s: %s" % (
896 self.__class__.__name__,
897 self.request.__class__.__name__,
898 self.fields)
899
900
902 """Does this response require signing?
903
904 @returntype: bool
905 """
906 return self.fields.getArg(OPENID_NS, 'mode') == 'id_res'
907
908
909
910
920
921
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
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
959
960
961
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
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
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
1007
1008
1009
1010
1011
1012 _normal_key = 'http://localhost/|normal'
1013 _dumb_key = 'http://localhost/|dumb'
1014
1015
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
1073
1074
1075
1076
1077 assoc = self.getAssociation(assoc_handle, dumb=False,
1078 checkExpiration=False)
1079
1080 if not assoc or assoc.expiresIn <= 0:
1081
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
1087
1088 self.invalidate(assoc_handle, dumb=False)
1089 assoc = self.createAssociation(dumb=True, assoc_type=assoc_type)
1090 else:
1091
1092 assoc = self.createAssociation(dumb=True)
1093
1094 signed_response.fields = assoc.signMessage(signed_response.fields)
1095 return signed_response
1096
1097
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
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
1139
1140
1141
1142
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
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
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
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
1207
1208 raise EncodingError(response)
1209 return wr
1210
1211
1212
1214 """I encode responses in to L{WebResponses<WebResponse>}, signing them when required.
1215 """
1216
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
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
1237
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
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
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
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
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
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
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
1416 """Handle and respond to C{check_authentication} requests.
1417
1418 @returntype: L{OpenIDResponse}
1419 """
1420 return request.answer(self.signatory)
1421
1422
1424 """Handle and respond to C{associate} requests.
1425
1426 @returntype: L{OpenIDResponse}
1427 """
1428
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
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
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
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
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
1517 """Did this request have a return_to parameter?
1518
1519 @returntype: bool
1520 """
1521 return self.getReturnTo() is not None
1522
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
1541
1544
1547
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
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577 return None
1578
1579
1580
1582 """Raised when an operation was attempted that is not compatible with
1583 the protocol version being used."""
1584
1585
1586
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
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
1605 Exception.__init__(self, response)
1606 self.response = response
1607
1608
1609
1611 """This response is already signed."""
1612
1613
1614
1616 """A return_to is outside the trust_root."""
1617
1618 - def __init__(self, message, return_to, trust_root):
1622
1624 return "return_to %r not under trust_root %r" % (self.return_to,
1625 self.trust_root)
1626
1627
1633
1634
1635
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
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674