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