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, optionally
32 check L{CheckIDRequest.returnToVerified}, and and when you've come
33 to a decision, form a response by calling L{CheckIDRequest.answer}.
34
35 Other types of requests relate to establishing associations between client
36 and server and verifying the authenticity of previous communications.
37 L{Server} contains all the logic and data necessary to respond to
38 such requests; just pass the request to L{Server.handleRequest}.
39
40
41 OpenID Extensions
42 =================
43
44 Do you want to provide other information for your users
45 in addition to authentication? Version 2.0 of the OpenID
46 protocol allows consumers to add extensions to their requests.
47 For example, with sites using the U{Simple Registration
48 Extension<http://openid.net/specs/openid-simple-registration-extension-1_0.html>},
49 a user can agree to have their nickname and e-mail address sent to a
50 site when they sign up.
51
52 Since extensions do not change the way OpenID authentication works,
53 code to handle extension requests may be completely separate from the
54 L{OpenIDRequest} class here. But you'll likely want data sent back by
55 your extension to be signed. L{OpenIDResponse} provides methods with
56 which you can add data to it which can be signed with the other data in
57 the OpenID signature.
58
59 For example::
60
61 # when request is a checkid_* request
62 response = request.answer(True)
63 # this will a signed 'openid.sreg.timezone' parameter to the response
64 # as well as a namespace declaration for the openid.sreg namespace
65 response.fields.setArg('http://openid.net/sreg/1.0', 'timezone', 'America/Los_Angeles')
66
67 There are helper modules for a number of extensions, including
68 L{Attribute Exchange<openid.extensions.ax>},
69 L{PAPE<openid.extensions.pape>}, and
70 L{Simple Registration<openid.extensions.sreg>} in the L{openid.extensions}
71 package.
72
73 Stores
74 ======
75
76 The OpenID server needs to maintain state between requests in order
77 to function. Its mechanism for doing this is called a store. The
78 store interface is defined in C{L{openid.store.interface.OpenIDStore}}.
79 Additionally, several concrete store implementations are provided, so that
80 most sites won't need to implement a custom store. For a store backed
81 by flat files on disk, see C{L{openid.store.filestore.FileOpenIDStore}}.
82 For stores based on MySQL or SQLite, see the C{L{openid.store.sqlstore}}
83 module.
84
85
86 Upgrading
87 =========
88
89 From 1.0 to 1.1
90 ---------------
91
92 The keys by which a server looks up associations in its store have changed
93 in version 1.2 of this library. If your store has entries created from
94 version 1.0 code, you should empty it.
95
96 From 1.1 to 2.0
97 ---------------
98
99 One of the additions to the OpenID protocol was a specified nonce
100 format for one-way nonces. As a result, the nonce table in the store
101 has changed. You'll need to run contrib/upgrade-store-1.1-to-2.0 to
102 upgrade your store, or you'll encounter errors about the wrong number
103 of columns in the oid_nonces table.
104
105 If you've written your own custom store or code that interacts
106 directly with it, you'll need to review the change notes in
107 L{openid.store.interface}.
108
109 @group Requests: OpenIDRequest, AssociateRequest, CheckIDRequest,
110 CheckAuthRequest
111
112 @group Responses: OpenIDResponse
113
114 @group HTTP Codes: HTTP_OK, HTTP_REDIRECT, HTTP_ERROR
115
116 @group Response Encodings: ENCODE_KVFORM, ENCODE_HTML_FORM, ENCODE_URL
117 """
118
119 import time, warnings
120 from copy import deepcopy
121
122 from openid import cryptutil
123 from openid import oidutil
124 from openid import kvform
125 from openid.dh import DiffieHellman
126 from openid.store.nonce import mkNonce
127 from openid.server.trustroot import TrustRoot, verifyReturnTo
128 from openid.association import Association, default_negotiator, getSecretSize
129 from openid.message import Message, InvalidOpenIDNamespace, \
130 OPENID_NS, OPENID2_NS, IDENTIFIER_SELECT, OPENID1_URL_LIMIT
131 from openid.urinorm import urinorm
132
133 HTTP_OK = 200
134 HTTP_REDIRECT = 302
135 HTTP_ERROR = 400
136
137 BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
138
139 ENCODE_KVFORM = ('kvform',)
140 ENCODE_URL = ('URL/redirect',)
141 ENCODE_HTML_FORM = ('HTML form',)
142
143 UNUSED = None
144
146 """I represent an incoming OpenID request.
147
148 @cvar mode: the C{X{openid.mode}} of this request.
149 @type mode: str
150 """
151 mode = None
152
153
155 """A request to verify the validity of a previous response.
156
157 @cvar mode: "X{C{check_authentication}}"
158 @type mode: str
159
160 @ivar assoc_handle: The X{association handle} the response was signed with.
161 @type assoc_handle: str
162 @ivar signed: The message with the signature which wants checking.
163 @type signed: L{Message}
164
165 @ivar invalidate_handle: An X{association handle} the client is asking
166 about the validity of. Optional, may be C{None}.
167 @type invalidate_handle: str
168
169 @see: U{OpenID Specs, Mode: check_authentication
170 <http://openid.net/specs.bml#mode-check_authentication>}
171 """
172 mode = "check_authentication"
173
174 required_fields = ["identity", "return_to", "response_nonce"]
175
176 - def __init__(self, assoc_handle, signed, invalidate_handle=None):
177 """Construct me.
178
179 These parameters are assigned directly as class attributes, see
180 my L{class documentation<CheckAuthRequest>} for their descriptions.
181
182 @type assoc_handle: str
183 @type signed: L{Message}
184 @type invalidate_handle: str
185 """
186 self.assoc_handle = assoc_handle
187 self.signed = signed
188 self.invalidate_handle = invalidate_handle
189 self.namespace = OPENID2_NS
190
191
223
224 fromMessage = classmethod(fromMessage)
225
227 """Respond to this request.
228
229 Given a L{Signatory}, I can check the validity of the signature and
230 the X{C{invalidate_handle}}.
231
232 @param signatory: The L{Signatory} to use to check the signature.
233 @type signatory: L{Signatory}
234
235 @returns: A response with an X{C{is_valid}} (and, if
236 appropriate X{C{invalidate_handle}}) field.
237 @returntype: L{OpenIDResponse}
238 """
239 is_valid = signatory.verify(self.assoc_handle, self.signed)
240
241
242 signatory.invalidate(self.assoc_handle, dumb=True)
243 response = OpenIDResponse(self)
244 valid_str = (is_valid and "true") or "false"
245 response.fields.setArg(OPENID_NS, 'is_valid', valid_str)
246
247 if self.invalidate_handle:
248 assoc = signatory.getAssociation(self.invalidate_handle, dumb=False)
249 if not assoc:
250 response.fields.setArg(
251 OPENID_NS, 'invalidate_handle', self.invalidate_handle)
252 return response
253
254
256 if self.invalidate_handle:
257 ih = " invalidate? %r" % (self.invalidate_handle,)
258 else:
259 ih = ""
260 s = "<%s handle: %r sig: %r: signed: %r%s>" % (
261 self.__class__.__name__, self.assoc_handle,
262 self.sig, self.signed, ih)
263 return s
264
265
267 """An object that knows how to handle association requests with no
268 session type.
269
270 @cvar session_type: The session_type for this association
271 session. There is no type defined for plain-text in the OpenID
272 specification, so we use 'no-encryption'.
273 @type session_type: str
274
275 @see: U{OpenID Specs, Mode: associate
276 <http://openid.net/specs.bml#mode-associate>}
277 @see: AssociateRequest
278 """
279 session_type = 'no-encryption'
280 allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
281
282 - def fromMessage(cls, unused_request):
284
285 fromMessage = classmethod(fromMessage)
286
287 - def answer(self, secret):
288 return {'mac_key': oidutil.toBase64(secret)}
289
290
292 """An object that knows how to handle association requests with the
293 Diffie-Hellman session type.
294
295 @cvar session_type: The session_type for this association
296 session.
297 @type session_type: str
298
299 @ivar dh: The Diffie-Hellman algorithm values for this request
300 @type dh: DiffieHellman
301
302 @ivar consumer_pubkey: The public key sent by the consumer in the
303 associate request
304 @type consumer_pubkey: long
305
306 @see: U{OpenID Specs, Mode: associate
307 <http://openid.net/specs.bml#mode-associate>}
308 @see: AssociateRequest
309 """
310 session_type = 'DH-SHA1'
311 hash_func = staticmethod(cryptutil.sha1)
312 allowed_assoc_types = ['HMAC-SHA1']
313
314 - def __init__(self, dh, consumer_pubkey):
315 self.dh = dh
316 self.consumer_pubkey = consumer_pubkey
317
319 """
320 @param message: The associate request message
321 @type message: openid.message.Message
322
323 @returntype: L{DiffieHellmanSHA1ServerSession}
324
325 @raises ProtocolError: When parameters required to establish the
326 session are missing.
327 """
328 dh_modulus = message.getArg(OPENID_NS, 'dh_modulus')
329 dh_gen = message.getArg(OPENID_NS, 'dh_gen')
330 if (dh_modulus is None and dh_gen is not None or
331 dh_gen is None and dh_modulus is not None):
332
333 if dh_modulus is None:
334 missing = 'modulus'
335 else:
336 missing = 'generator'
337
338 raise ProtocolError(message,
339 'If non-default modulus or generator is '
340 'supplied, both must be supplied. Missing %s'
341 % (missing,))
342
343 if dh_modulus or dh_gen:
344 dh_modulus = cryptutil.base64ToLong(dh_modulus)
345 dh_gen = cryptutil.base64ToLong(dh_gen)
346 dh = DiffieHellman(dh_modulus, dh_gen)
347 else:
348 dh = DiffieHellman.fromDefaults()
349
350 consumer_pubkey = message.getArg(OPENID_NS, 'dh_consumer_public')
351 if consumer_pubkey is None:
352 raise ProtocolError(message, "Public key for DH-SHA1 session "
353 "not found in message %s" % (message,))
354
355 consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey)
356
357 return cls(dh, consumer_pubkey)
358
359 fromMessage = classmethod(fromMessage)
360
369
374
376 """A request to establish an X{association}.
377
378 @cvar mode: "X{C{check_authentication}}"
379 @type mode: str
380
381 @ivar assoc_type: The type of association. The protocol currently only
382 defines one value for this, "X{C{HMAC-SHA1}}".
383 @type assoc_type: str
384
385 @ivar session: An object that knows how to handle association
386 requests of a certain type.
387
388 @see: U{OpenID Specs, Mode: associate
389 <http://openid.net/specs.bml#mode-associate>}
390 """
391
392 mode = "associate"
393
394 session_classes = {
395 'no-encryption': PlainTextServerSession,
396 'DH-SHA1': DiffieHellmanSHA1ServerSession,
397 'DH-SHA256': DiffieHellmanSHA256ServerSession,
398 }
399
400 - def __init__(self, session, assoc_type):
401 """Construct me.
402
403 The session is assigned directly as a class attribute. See my
404 L{class documentation<AssociateRequest>} for its description.
405 """
406 super(AssociateRequest, self).__init__()
407 self.session = session
408 self.assoc_type = assoc_type
409 self.namespace = OPENID2_NS
410
411
413 """Construct me from an OpenID Message.
414
415 @param message: The OpenID associate request
416 @type message: openid.message.Message
417
418 @returntype: L{AssociateRequest}
419 """
420 if message.isOpenID1():
421 session_type = message.getArg(OPENID_NS, 'session_type')
422 if session_type == 'no-encryption':
423 oidutil.log('Received OpenID 1 request with a no-encryption '
424 'assocaition session type. Continuing anyway.')
425 elif not session_type:
426 session_type = 'no-encryption'
427 else:
428 session_type = message.getArg(OPENID2_NS, 'session_type')
429 if session_type is None:
430 raise ProtocolError(message,
431 text="session_type missing from request")
432
433 try:
434 session_class = klass.session_classes[session_type]
435 except KeyError:
436 raise ProtocolError(message,
437 "Unknown session type %r" % (session_type,))
438
439 try:
440 session = session_class.fromMessage(message)
441 except ValueError, why:
442 raise ProtocolError(message, 'Error parsing %s session: %s' %
443 (session_class.session_type, why[0]))
444
445 assoc_type = message.getArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
446 if assoc_type not in session.allowed_assoc_types:
447 fmt = 'Session type %s does not support association type %s'
448 raise ProtocolError(message, fmt % (session_type, assoc_type))
449
450 self = klass(session, assoc_type)
451 self.message = message
452 self.namespace = message.getOpenIDNamespace()
453 return self
454
455 fromMessage = classmethod(fromMessage)
456
484
485 - def answerUnsupported(self, message, preferred_association_type=None,
486 preferred_session_type=None):
505
507 """A request to confirm the identity of a user.
508
509 This class handles requests for openid modes X{C{checkid_immediate}}
510 and X{C{checkid_setup}}.
511
512 @cvar mode: "X{C{checkid_immediate}}" or "X{C{checkid_setup}}"
513 @type mode: str
514
515 @ivar immediate: Is this an immediate-mode request?
516 @type immediate: bool
517
518 @ivar identity: The OP-local identifier being checked.
519 @type identity: str
520
521 @ivar claimed_id: The claimed identifier. Not present in OpenID 1.x
522 messages.
523 @type claimed_id: str
524
525 @ivar trust_root: "Are you Frank?" asks the checkid request. "Who wants
526 to know?" C{trust_root}, that's who. This URL identifies the party
527 making the request, and the user will use that to make her decision
528 about what answer she trusts them to have. Referred to as "realm" in
529 OpenID 2.0.
530 @type trust_root: str
531
532 @ivar return_to: The URL to send the user agent back to to reply to this
533 request.
534 @type return_to: str
535
536 @ivar assoc_handle: Provided in smart mode requests, a handle for a
537 previously established association. C{None} for dumb mode requests.
538 @type assoc_handle: str
539 """
540
541 - def __init__(self, identity, return_to, trust_root=None, immediate=False,
542 assoc_handle=None, op_endpoint=None, claimed_id=None):
543 """Construct me.
544
545 These parameters are assigned directly as class attributes, see
546 my L{class documentation<CheckIDRequest>} for their descriptions.
547
548 @raises MalformedReturnURL: When the C{return_to} URL is not a URL.
549 """
550 self.assoc_handle = assoc_handle
551 self.identity = identity
552 self.claimed_id = claimed_id or identity
553 self.return_to = return_to
554 self.trust_root = trust_root or return_to
555 self.op_endpoint = op_endpoint
556 assert self.op_endpoint is not None
557 if immediate:
558 self.immediate = True
559 self.mode = "checkid_immediate"
560 else:
561 self.immediate = False
562 self.mode = "checkid_setup"
563
564 if self.return_to is not None and \
565 not TrustRoot.parse(self.return_to):
566 raise MalformedReturnURL(None, self.return_to)
567 if not self.trustRootValid():
568 raise UntrustedReturnURL(None, self.return_to, self.trust_root)
569 self.message = None
570
572 warnings.warn('The "namespace" attribute of CheckIDRequest objects '
573 'is deprecated. Use "message.getOpenIDNamespace()" '
574 'instead', DeprecationWarning, stacklevel=2)
575 return self.message.getOpenIDNamespace()
576
577 namespace = property(_getNamespace)
578
580 """Construct me from an OpenID message.
581
582 @raises ProtocolError: When not all required parameters are present
583 in the message.
584
585 @raises MalformedReturnURL: When the C{return_to} URL is not a URL.
586
587 @raises UntrustedReturnURL: When the C{return_to} URL is outside
588 the C{trust_root}.
589
590 @param message: An OpenID checkid_* request Message
591 @type message: openid.message.Message
592
593 @param op_endpoint: The endpoint URL of the server that this
594 message was sent to.
595 @type op_endpoint: str
596
597 @returntype: L{CheckIDRequest}
598 """
599 self = klass.__new__(klass)
600 self.message = message
601 self.op_endpoint = op_endpoint
602 mode = message.getArg(OPENID_NS, 'mode')
603 if mode == "checkid_immediate":
604 self.immediate = True
605 self.mode = "checkid_immediate"
606 else:
607 self.immediate = False
608 self.mode = "checkid_setup"
609
610 self.return_to = message.getArg(OPENID_NS, 'return_to')
611 if message.isOpenID1() and not self.return_to:
612 fmt = "Missing required field 'return_to' from %r"
613 raise ProtocolError(message, text=fmt % (message,))
614
615 self.identity = message.getArg(OPENID_NS, 'identity')
616 self.claimed_id = message.getArg(OPENID_NS, 'claimed_id')
617 if message.isOpenID1():
618 if self.identity is None:
619 s = "OpenID 1 message did not contain openid.identity"
620 raise ProtocolError(message, text=s)
621 else:
622 if self.identity and not self.claimed_id:
623 s = ("OpenID 2.0 message contained openid.identity but not "
624 "claimed_id")
625 raise ProtocolError(message, text=s)
626 elif self.claimed_id and not self.identity:
627 s = ("OpenID 2.0 message contained openid.claimed_id but not "
628 "identity")
629 raise ProtocolError(message, text=s)
630
631
632
633
634
635 if message.isOpenID1():
636 trust_root_param = 'trust_root'
637 else:
638 trust_root_param = 'realm'
639
640
641
642
643 self.trust_root = (message.getArg(OPENID_NS, trust_root_param)
644 or self.return_to)
645
646 if not message.isOpenID1():
647 if self.return_to is self.trust_root is None:
648 raise ProtocolError(message, "openid.realm required when " +
649 "openid.return_to absent")
650
651 self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
652
653
654
655
656
657
658
659 if self.return_to is not None and \
660 not TrustRoot.parse(self.return_to):
661 raise MalformedReturnURL(message, self.return_to)
662
663
664
665
666
667
668 if not self.trustRootValid():
669 raise UntrustedReturnURL(message, self.return_to, self.trust_root)
670
671 return self
672
673 fromMessage = classmethod(fromMessage)
674
676 """Is the identifier to be selected by the IDP?
677
678 @returntype: bool
679 """
680
681 return self.identity == IDENTIFIER_SELECT
682
684 """Is my return_to under my trust_root?
685
686 @returntype: bool
687 """
688 if not self.trust_root:
689 return True
690 tr = TrustRoot.parse(self.trust_root)
691 if tr is None:
692 raise MalformedTrustRoot(self.message, self.trust_root)
693
694 if self.return_to is not None:
695 return tr.validateURL(self.return_to)
696 else:
697 return True
698
700 """Does the relying party publish the return_to URL for this
701 response under the realm? It is up to the provider to set a
702 policy for what kinds of realms should be allowed. This
703 return_to URL verification reduces vulnerability to data-theft
704 attacks based on open proxies, cross-site-scripting, or open
705 redirectors.
706
707 This check should only be performed after making sure that the
708 return_to URL matches the realm.
709
710 @see: L{trustRootValid}
711
712 @raises openid.yadis.discover.DiscoveryFailure: if the realm
713 URL does not support Yadis discovery (and so does not
714 support the verification process).
715
716 @raises openid.fetchers.HTTPFetchingError: if the realm URL
717 is not reachable. When this is the case, the RP may be hosted
718 on the user's intranet.
719
720 @returntype: bool
721
722 @returns: True if the realm publishes a document with the
723 return_to URL listed
724
725 @since: 2.1.0
726 """
727 return verifyReturnTo(self.trust_root, self.return_to)
728
729 - def answer(self, allow, server_url=None, identity=None, claimed_id=None):
730 """Respond to this request.
731
732 @param allow: Allow this user to claim this identity, and allow the
733 consumer to have this information?
734 @type allow: bool
735
736 @param server_url: DEPRECATED. Passing C{op_endpoint} to the
737 L{Server} constructor makes this optional.
738
739 When an OpenID 1.x immediate mode request does not succeed,
740 it gets back a URL where the request may be carried out
741 in a not-so-immediate fashion. Pass my URL in here (the
742 fully qualified address of this server's endpoint, i.e.
743 C{http://example.com/server}), and I will use it as a base for the
744 URL for a new request.
745
746 Optional for requests where C{CheckIDRequest.immediate} is C{False}
747 or C{allow} is C{True}.
748
749 @type server_url: str
750
751 @param identity: The OP-local identifier to answer with. Only for use
752 when the relying party requested identifier selection.
753 @type identity: str or None
754
755 @param claimed_id: The claimed identifier to answer with, for use
756 with identifier selection in the case where the claimed identifier
757 and the OP-local identifier differ, i.e. when the claimed_id uses
758 delegation.
759
760 If C{identity} is provided but this is not, C{claimed_id} will
761 default to the value of C{identity}. When answering requests
762 that did not ask for identifier selection, the response
763 C{claimed_id} will default to that of the request.
764
765 This parameter is new in OpenID 2.0.
766 @type claimed_id: str or None
767
768 @returntype: L{OpenIDResponse}
769
770 @change: Version 2.0 deprecates C{server_url} and adds C{claimed_id}.
771
772 @raises NoReturnError: when I do not have a return_to.
773 """
774 assert self.message is not None
775
776 if not self.return_to:
777 raise NoReturnToError
778
779 if not server_url:
780 if not self.message.isOpenID1() and not self.op_endpoint:
781
782
783 raise RuntimeError("%s should be constructed with op_endpoint "
784 "to respond to OpenID 2.0 messages." %
785 (self,))
786 server_url = self.op_endpoint
787
788 if allow:
789 mode = 'id_res'
790 elif self.message.isOpenID1():
791 if self.immediate:
792 mode = 'id_res'
793 else:
794 mode = 'cancel'
795 else:
796 if self.immediate:
797 mode = 'setup_needed'
798 else:
799 mode = 'cancel'
800
801 response = OpenIDResponse(self)
802
803 if claimed_id and self.message.isOpenID1():
804 namespace = self.message.getOpenIDNamespace()
805 raise VersionError("claimed_id is new in OpenID 2.0 and not "
806 "available for %s" % (namespace,))
807
808 if allow:
809 if self.identity == IDENTIFIER_SELECT:
810 if not identity:
811 raise ValueError(
812 "This request uses IdP-driven identifier selection."
813 "You must supply an identifier in the response.")
814 response_identity = identity
815 response_claimed_id = claimed_id or identity
816
817 elif self.identity:
818 if identity and (self.identity != identity):
819 normalized_request_identity = urinorm(self.identity)
820 normalized_answer_identity = urinorm(identity)
821
822 if (normalized_request_identity !=
823 normalized_answer_identity):
824 raise ValueError(
825 "Request was for identity %r, cannot reply "
826 "with identity %r" % (self.identity, identity))
827
828
829
830
831 response_identity = self.identity
832 response_claimed_id = self.claimed_id
833 else:
834 if identity:
835 raise ValueError(
836 "This request specified no identity and you "
837 "supplied %r" % (identity,))
838 response_identity = None
839
840 if self.message.isOpenID1() and response_identity is None:
841 raise ValueError(
842 "Request was an OpenID 1 request, so response must "
843 "include an identifier."
844 )
845
846 response.fields.updateArgs(OPENID_NS, {
847 'mode': mode,
848 'return_to': self.return_to,
849 'response_nonce': mkNonce(),
850 })
851
852 if server_url:
853 response.fields.setArg(OPENID_NS, 'op_endpoint', server_url)
854
855 if response_identity is not None:
856 response.fields.setArg(
857 OPENID_NS, 'identity', response_identity)
858 if self.message.isOpenID2():
859 response.fields.setArg(
860 OPENID_NS, 'claimed_id', response_claimed_id)
861 else:
862 response.fields.setArg(OPENID_NS, 'mode', mode)
863 if self.immediate:
864 if self.message.isOpenID1() and not server_url:
865 raise ValueError("setup_url is required for allow=False "
866 "in OpenID 1.x immediate mode.")
867
868 setup_request = self.__class__(
869 self.identity, self.return_to, self.trust_root,
870 immediate=False, assoc_handle=self.assoc_handle,
871 op_endpoint=self.op_endpoint, claimed_id=self.claimed_id)
872
873
874 setup_request.message = self.message
875
876 setup_url = setup_request.encodeToURL(server_url)
877 response.fields.setArg(OPENID_NS, 'user_setup_url', setup_url)
878
879 return response
880
881
883 """Encode this request as a URL to GET.
884
885 @param server_url: The URL of the OpenID server to make this request of.
886 @type server_url: str
887
888 @returntype: str
889
890 @raises NoReturnError: when I do not have a return_to.
891 """
892 if not self.return_to:
893 raise NoReturnToError
894
895
896
897
898
899 q = {'mode': self.mode,
900 'identity': self.identity,
901 'claimed_id': self.claimed_id,
902 'return_to': self.return_to}
903 if self.trust_root:
904 if self.message.isOpenID1():
905 q['trust_root'] = self.trust_root
906 else:
907 q['realm'] = self.trust_root
908 if self.assoc_handle:
909 q['assoc_handle'] = self.assoc_handle
910
911 response = Message(self.message.getOpenIDNamespace())
912 response.updateArgs(OPENID_NS, q)
913 return response.toURL(server_url)
914
915
917 """Get the URL to cancel this request.
918
919 Useful for creating a "Cancel" button on a web form so that operation
920 can be carried out directly without another trip through the server.
921
922 (Except you probably want to make another trip through the server so
923 that it knows that the user did make a decision. Or you could simulate
924 this method by doing C{.answer(False).encodeToURL()})
925
926 @returntype: str
927 @returns: The return_to URL with openid.mode = cancel.
928
929 @raises NoReturnError: when I do not have a return_to.
930 """
931 if not self.return_to:
932 raise NoReturnToError
933
934 if self.immediate:
935 raise ValueError("Cancel is not an appropriate response to "
936 "immediate mode requests.")
937
938 response = Message(self.message.getOpenIDNamespace())
939 response.setArg(OPENID_NS, 'mode', 'cancel')
940 return response.toURL(self.return_to)
941
942
944 return '<%s id:%r im:%s tr:%r ah:%r>' % (self.__class__.__name__,
945 self.identity,
946 self.immediate,
947 self.trust_root,
948 self.assoc_handle)
949
950
951
953 """I am a response to an OpenID request.
954
955 @ivar request: The request I respond to.
956 @type request: L{OpenIDRequest}
957
958 @ivar fields: My parameters as a dictionary with each key mapping to
959 one value. Keys are parameter names with no leading "C{openid.}".
960 e.g. "C{identity}" and "C{mac_key}", never "C{openid.identity}".
961 @type fields: L{openid.message.Message}
962
963 @ivar signed: The names of the fields which should be signed.
964 @type signed: list of str
965 """
966
967
968
969
970
971
972
973
975 """Make a response to an L{OpenIDRequest}.
976
977 @type request: L{OpenIDRequest}
978 """
979 self.request = request
980 self.fields = Message(request.namespace)
981
983 return "%s for %s: %s" % (
984 self.__class__.__name__,
985 self.request.__class__.__name__,
986 self.fields)
987
988
1003
1004 - def toHTML(self, form_tag_attrs=None):
1005 """Returns an HTML document that auto-submits the form markup
1006 for this response.
1007
1008 @returntype: str
1009
1010 @see: toFormMarkup
1011
1012 @since: 2.1.?
1013 """
1014 return oidutil.autoSubmitHTML(self.toFormMarkup(form_tag_attrs))
1015
1025
1026
1028 """Does this response require signing?
1029
1030 @returntype: bool
1031 """
1032 return self.fields.getArg(OPENID_NS, 'mode') == 'id_res'
1033
1034
1035
1036
1052
1053
1055 """Encode a response as a URL for the user agent to GET.
1056
1057 You will generally use this URL with a HTTP redirect.
1058
1059 @returns: A URL to direct the user agent back to.
1060 @returntype: str
1061 """
1062 return self.fields.toURL(self.request.return_to)
1063
1064
1066 """
1067 Add an extension response to this response message.
1068
1069 @param extension_response: An object that implements the
1070 extension interface for adding arguments to an OpenID
1071 message.
1072 @type extension_response: L{openid.extension}
1073
1074 @returntype: None
1075 """
1076 extension_response.toMessage(self.fields)
1077
1078
1091
1092
1093
1095 """I am a response to an OpenID request in terms a web server understands.
1096
1097 I generally come from an L{Encoder}, either directly or from
1098 L{Server.encodeResponse}.
1099
1100 @ivar code: The HTTP code of this response.
1101 @type code: int
1102
1103 @ivar headers: Headers to include in this response.
1104 @type headers: dict
1105
1106 @ivar body: The body of this response.
1107 @type body: str
1108 """
1109
1111 """Construct me.
1112
1113 These parameters are assigned directly as class attributes, see
1114 my L{class documentation<WebResponse>} for their descriptions.
1115 """
1116 self.code = code
1117 if headers is not None:
1118 self.headers = headers
1119 else:
1120 self.headers = {}
1121 self.body = body
1122
1123
1124
1126 """I sign things.
1127
1128 I also check signatures.
1129
1130 All my state is encapsulated in an
1131 L{OpenIDStore<openid.store.interface.OpenIDStore>}, which means
1132 I'm not generally pickleable but I am easy to reconstruct.
1133
1134 @cvar SECRET_LIFETIME: The number of seconds a secret remains valid.
1135 @type SECRET_LIFETIME: int
1136 """
1137
1138 SECRET_LIFETIME = 14 * 24 * 60 * 60
1139
1140
1141
1142
1143
1144 _normal_key = 'http://localhost/|normal'
1145 _dumb_key = 'http://localhost/|dumb'
1146
1147
1149 """Create a new Signatory.
1150
1151 @param store: The back-end where my associations are stored.
1152 @type store: L{openid.store.interface.OpenIDStore}
1153 """
1154 assert store is not None
1155 self.store = store
1156
1157
1158 - def verify(self, assoc_handle, message):
1159 """Verify that the signature for some data is valid.
1160
1161 @param assoc_handle: The handle of the association used to sign the
1162 data.
1163 @type assoc_handle: str
1164
1165 @param message: The signed message to verify
1166 @type message: openid.message.Message
1167
1168 @returns: C{True} if the signature is valid, C{False} if not.
1169 @returntype: bool
1170 """
1171 assoc = self.getAssociation(assoc_handle, dumb=True)
1172 if not assoc:
1173 oidutil.log("failed to get assoc with handle %r to verify "
1174 "message %r"
1175 % (assoc_handle, message))
1176 return False
1177
1178 try:
1179 valid = assoc.checkMessageSignature(message)
1180 except ValueError, ex:
1181 oidutil.log("Error in verifying %s with %s: %s" % (message,
1182 assoc,
1183 ex))
1184 return False
1185 return valid
1186
1187
1188 - def sign(self, response):
1189 """Sign a response.
1190
1191 I take a L{OpenIDResponse}, create a signature for everything
1192 in its L{signed<OpenIDResponse.signed>} list, and return a new
1193 copy of the response object with that signature included.
1194
1195 @param response: A response to sign.
1196 @type response: L{OpenIDResponse}
1197
1198 @returns: A signed copy of the response.
1199 @returntype: L{OpenIDResponse}
1200 """
1201 signed_response = deepcopy(response)
1202 assoc_handle = response.request.assoc_handle
1203 if assoc_handle:
1204
1205
1206
1207
1208
1209 assoc = self.getAssociation(assoc_handle, dumb=False,
1210 checkExpiration=False)
1211
1212 if not assoc or assoc.expiresIn <= 0:
1213
1214 signed_response.fields.setArg(
1215 OPENID_NS, 'invalidate_handle', assoc_handle)
1216 assoc_type = assoc and assoc.assoc_type or 'HMAC-SHA1'
1217 if assoc and assoc.expiresIn <= 0:
1218
1219
1220 self.invalidate(assoc_handle, dumb=False)
1221 assoc = self.createAssociation(dumb=True, assoc_type=assoc_type)
1222 else:
1223
1224 assoc = self.createAssociation(dumb=True)
1225
1226 try:
1227 signed_response.fields = assoc.signMessage(signed_response.fields)
1228 except kvform.KVFormError, err:
1229 raise EncodingError(response, explanation=str(err))
1230 return signed_response
1231
1232
1234 """Make a new association.
1235
1236 @param dumb: Is this association for a dumb-mode transaction?
1237 @type dumb: bool
1238
1239 @param assoc_type: The type of association to create. Currently
1240 there is only one type defined, C{HMAC-SHA1}.
1241 @type assoc_type: str
1242
1243 @returns: the new association.
1244 @returntype: L{openid.association.Association}
1245 """
1246 secret = cryptutil.getBytes(getSecretSize(assoc_type))
1247 uniq = oidutil.toBase64(cryptutil.getBytes(4))
1248 handle = '{%s}{%x}{%s}' % (assoc_type, int(time.time()), uniq)
1249
1250 assoc = Association.fromExpiresIn(
1251 self.SECRET_LIFETIME, handle, secret, assoc_type)
1252
1253 if dumb:
1254 key = self._dumb_key
1255 else:
1256 key = self._normal_key
1257 self.store.storeAssociation(key, assoc)
1258 return assoc
1259
1260
1262 """Get the association with the specified handle.
1263
1264 @type assoc_handle: str
1265
1266 @param dumb: Is this association used with dumb mode?
1267 @type dumb: bool
1268
1269 @returns: the association, or None if no valid association with that
1270 handle was found.
1271 @returntype: L{openid.association.Association}
1272 """
1273
1274
1275
1276
1277
1278
1279 if assoc_handle is None:
1280 raise ValueError("assoc_handle must not be None")
1281
1282 if dumb:
1283 key = self._dumb_key
1284 else:
1285 key = self._normal_key
1286 assoc = self.store.getAssociation(key, assoc_handle)
1287 if assoc is not None and assoc.expiresIn <= 0:
1288 oidutil.log("requested %sdumb key %r is expired (by %s seconds)" %
1289 ((not dumb) and 'not-' or '',
1290 assoc_handle, assoc.expiresIn))
1291 if checkExpiration:
1292 self.store.removeAssociation(key, assoc_handle)
1293 assoc = None
1294 return assoc
1295
1296
1298 """Invalidates the association with the given handle.
1299
1300 @type assoc_handle: str
1301
1302 @param dumb: Is this association used with dumb mode?
1303 @type dumb: bool
1304 """
1305 if dumb:
1306 key = self._dumb_key
1307 else:
1308 key = self._normal_key
1309 self.store.removeAssociation(key, assoc_handle)
1310
1311
1312
1314 """I encode responses in to L{WebResponses<WebResponse>}.
1315
1316 If you don't like L{WebResponses<WebResponse>}, you can do
1317 your own handling of L{OpenIDResponses<OpenIDResponse>} with
1318 L{OpenIDResponse.whichEncoding}, L{OpenIDResponse.encodeToURL}, and
1319 L{OpenIDResponse.encodeToKVForm}.
1320 """
1321
1322 responseFactory = WebResponse
1323
1324
1348
1349
1350
1352 """I encode responses in to L{WebResponses<WebResponse>}, signing them when required.
1353 """
1354
1356 """Create a L{SigningEncoder}.
1357
1358 @param signatory: The L{Signatory} I will make signatures with.
1359 @type signatory: L{Signatory}
1360 """
1361 self.signatory = signatory
1362
1363
1365 """Encode a response to a L{WebResponse}, signing it first if appropriate.
1366
1367 @raises EncodingError: When I can't figure out how to encode this
1368 message.
1369
1370 @raises AlreadySigned: When this response is already signed.
1371
1372 @returntype: L{WebResponse}
1373 """
1374
1375
1376 if (not isinstance(response, Exception)) and response.needsSigning():
1377 if not self.signatory:
1378 raise ValueError(
1379 "Must have a store to sign this request: %s" %
1380 (response,), response)
1381 if response.fields.hasKey(OPENID_NS, 'sig'):
1382 raise AlreadySigned(response)
1383 response = self.signatory.sign(response)
1384 return super(SigningEncoder, self).encode(response)
1385
1386
1387
1389 """I decode an incoming web request in to a L{OpenIDRequest}.
1390 """
1391
1392 _handlers = {
1393 'checkid_setup': CheckIDRequest.fromMessage,
1394 'checkid_immediate': CheckIDRequest.fromMessage,
1395 'check_authentication': CheckAuthRequest.fromMessage,
1396 'associate': AssociateRequest.fromMessage,
1397 }
1398
1400 """Construct a Decoder.
1401
1402 @param server: The server which I am decoding requests for.
1403 (Necessary because some replies reference their server.)
1404 @type server: L{Server}
1405 """
1406 self.server = server
1407
1445
1446
1448 """Called to decode queries when no handler for that mode is found.
1449
1450 @raises ProtocolError: This implementation always raises
1451 L{ProtocolError}.
1452 """
1453 mode = message.getArg(OPENID_NS, 'mode')
1454 fmt = "Unrecognized OpenID mode %r"
1455 raise ProtocolError(message, text=fmt % (mode,))
1456
1457
1458
1460 """I handle requests for an OpenID server.
1461
1462 Some types of requests (those which are not C{checkid} requests) may be
1463 handed to my L{handleRequest} method, and I will take care of it and
1464 return a response.
1465
1466 For your convenience, I also provide an interface to L{Decoder.decode}
1467 and L{SigningEncoder.encode} through my methods L{decodeRequest} and
1468 L{encodeResponse}.
1469
1470 All my state is encapsulated in an
1471 L{OpenIDStore<openid.store.interface.OpenIDStore>}, which means
1472 I'm not generally pickleable but I am easy to reconstruct.
1473
1474 Example::
1475
1476 oserver = Server(FileOpenIDStore(data_path), "http://example.com/op")
1477 request = oserver.decodeRequest(query)
1478 if request.mode in ['checkid_immediate', 'checkid_setup']:
1479 if self.isAuthorized(request.identity, request.trust_root):
1480 response = request.answer(True)
1481 elif request.immediate:
1482 response = request.answer(False)
1483 else:
1484 self.showDecidePage(request)
1485 return
1486 else:
1487 response = oserver.handleRequest(request)
1488
1489 webresponse = oserver.encode(response)
1490
1491 @ivar signatory: I'm using this for associate requests and to sign things.
1492 @type signatory: L{Signatory}
1493
1494 @ivar decoder: I'm using this to decode things.
1495 @type decoder: L{Decoder}
1496
1497 @ivar encoder: I'm using this to encode things.
1498 @type encoder: L{Encoder}
1499
1500 @ivar op_endpoint: My URL.
1501 @type op_endpoint: str
1502
1503 @ivar negotiator: I use this to determine which kinds of
1504 associations I can make and how.
1505 @type negotiator: L{openid.association.SessionNegotiator}
1506 """
1507
1508 signatoryClass = Signatory
1509 encoderClass = SigningEncoder
1510 decoderClass = Decoder
1511
1512 - def __init__(self, store, op_endpoint=None):
1513 """A new L{Server}.
1514
1515 @param store: The back-end where my associations are stored.
1516 @type store: L{openid.store.interface.OpenIDStore}
1517
1518 @param op_endpoint: My URL, the fully qualified address of this
1519 server's endpoint, i.e. C{http://example.com/server}
1520 @type op_endpoint: str
1521
1522 @change: C{op_endpoint} is new in library version 2.0. It
1523 currently defaults to C{None} for compatibility with
1524 earlier versions of the library, but you must provide it
1525 if you want to respond to any version 2 OpenID requests.
1526 """
1527 self.store = store
1528 self.signatory = self.signatoryClass(self.store)
1529 self.encoder = self.encoderClass(self.signatory)
1530 self.decoder = self.decoderClass(self)
1531 self.negotiator = default_negotiator.copy()
1532
1533 if not op_endpoint:
1534 warnings.warn("%s.%s constructor requires op_endpoint parameter "
1535 "for OpenID 2.0 servers" %
1536 (self.__class__.__module__, self.__class__.__name__),
1537 stacklevel=2)
1538 self.op_endpoint = op_endpoint
1539
1540
1542 """Handle a request.
1543
1544 Give me a request, I will give you a response. Unless it's a type
1545 of request I cannot handle myself, in which case I will raise
1546 C{NotImplementedError}. In that case, you can handle it yourself,
1547 or add a method to me for handling that request type.
1548
1549 @raises NotImplementedError: When I do not have a handler defined
1550 for that type of request.
1551
1552 @returntype: L{OpenIDResponse}
1553 """
1554 handler = getattr(self, 'openid_' + request.mode, None)
1555 if handler is not None:
1556 return handler(request)
1557 else:
1558 raise NotImplementedError(
1559 "%s has no handler for a request of mode %r." %
1560 (self, request.mode))
1561
1562
1564 """Handle and respond to C{check_authentication} requests.
1565
1566 @returntype: L{OpenIDResponse}
1567 """
1568 return request.answer(self.signatory)
1569
1570
1572 """Handle and respond to C{associate} requests.
1573
1574 @returntype: L{OpenIDResponse}
1575 """
1576
1577 assoc_type = request.assoc_type
1578 session_type = request.session.session_type
1579 if self.negotiator.isAllowed(assoc_type, session_type):
1580 assoc = self.signatory.createAssociation(dumb=False,
1581 assoc_type=assoc_type)
1582 return request.answer(assoc)
1583 else:
1584 message = ('Association type %r is not supported with '
1585 'session type %r' % (assoc_type, session_type))
1586 (preferred_assoc_type, preferred_session_type) = \
1587 self.negotiator.getAllowedType()
1588 return request.answerUnsupported(
1589 message,
1590 preferred_assoc_type,
1591 preferred_session_type)
1592
1593
1595 """Transform query parameters into an L{OpenIDRequest}.
1596
1597 If the query does not seem to be an OpenID request at all, I return
1598 C{None}.
1599
1600 @param query: The query parameters as a dictionary with each
1601 key mapping to one value.
1602 @type query: dict
1603
1604 @raises ProtocolError: When the query does not seem to be a valid
1605 OpenID request.
1606
1607 @returntype: L{OpenIDRequest}
1608
1609 @see: L{Decoder.decode}
1610 """
1611 return self.decoder.decode(query)
1612
1613
1615 """Encode a response to a L{WebResponse}, signing it first if appropriate.
1616
1617 @raises EncodingError: When I can't figure out how to encode this
1618 message.
1619
1620 @raises AlreadySigned: When this response is already signed.
1621
1622 @returntype: L{WebResponse}
1623
1624 @see: L{SigningEncoder.encode}
1625 """
1626 return self.encoder.encode(response)
1627
1628
1629
1631 """A message did not conform to the OpenID protocol.
1632
1633 @ivar message: The query that is failing to be a valid OpenID request.
1634 @type message: openid.message.Message
1635 """
1636
1637 - def __init__(self, message, text=None, reference=None, contact=None):
1638 """When an error occurs.
1639
1640 @param message: The message that is failing to be a valid
1641 OpenID request.
1642 @type message: openid.message.Message
1643
1644 @param text: A message about the encountered error. Set as C{args[0]}.
1645 @type text: str
1646 """
1647 self.openid_message = message
1648 self.reference = reference
1649 self.contact = contact
1650 assert type(message) not in [str, unicode]
1651 Exception.__init__(self, text)
1652
1653
1655 """Get the return_to argument from the request, if any.
1656
1657 @returntype: str
1658 """
1659 if self.openid_message is None:
1660 return None
1661 else:
1662 return self.openid_message.getArg(OPENID_NS, 'return_to')
1663
1665 """Did this request have a return_to parameter?
1666
1667 @returntype: bool
1668 """
1669 return self.getReturnTo() is not None
1670
1687
1688
1689
1692
1695
1702
1704 """Encode to a full HTML page, wrapping the form markup in a page
1705 that will autosubmit the form.
1706
1707 @since: 2.1.?
1708 """
1709 return oidutil.autoSubmitHTML(self.toFormMarkup())
1710
1745
1746
1747
1749 """Raised when an operation was attempted that is not compatible with
1750 the protocol version being used."""
1751
1752
1753
1755 """Raised when a response to a request cannot be generated because
1756 the request contains no return_to URL.
1757 """
1758 pass
1759
1760
1761
1763 """Could not encode this as a protocol message.
1764
1765 You should probably render it and show it to the user.
1766
1767 @ivar response: The response that failed to encode.
1768 @type response: L{OpenIDResponse}
1769 """
1770
1771 - def __init__(self, response, explanation=None):
1775
1777 if self.explanation:
1778 s = '%s: %s' % (self.__class__.__name__,
1779 self.explanation)
1780 else:
1781 s = '%s for Response %s' % (
1782 self.__class__.__name__, self.response)
1783 return s
1784
1785
1787 """This response is already signed."""
1788
1789
1790
1792 """A return_to is outside the trust_root."""
1793
1794 - def __init__(self, message, return_to, trust_root):
1798
1800 return "return_to %r not under trust_root %r" % (self.return_to,
1801 self.trust_root)
1802
1803
1809
1810
1811
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850