95
97
@group Response Encodings: ENCODE_KVFORM, ENCODE_URL
100
import time, warnings
99
101
from copy import deepcopy
101
103
from openid import cryptutil
102
from openid import kvform
103
104
from openid import oidutil
104
105
from openid.dh import DiffieHellman
106
from openid.store.nonce import mkNonce
105
107
from openid.server.trustroot import TrustRoot
106
from openid.association import Association
108
from openid.association import Association, default_negotiator, getSecretSize
109
from openid.message import Message, OPENID_NS, OPENID1_NS, \
110
OPENID2_NS, IDENTIFIER_SELECT
109
113
HTTP_REDIRECT = 302
112
116
BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
113
OPENID_PREFIX = 'openid.'
115
118
ENCODE_KVFORM = ('kvform',)
116
119
ENCODE_URL = ('URL/redirect',)
118
123
class OpenIDRequest(object):
119
124
"""I represent an incoming OpenID request.
147
150
mode = "check_authentication"
152
required_fields = ["identity", "return_to", "response_nonce"]
150
def __init__(self, assoc_handle, sig, signed, invalidate_handle=None):
154
def __init__(self, assoc_handle, signed, invalidate_handle=None):
153
157
These parameters are assigned directly as class attributes, see
154
158
my L{class documentation<CheckAuthRequest>} for their descriptions.
156
160
@type assoc_handle: str
158
@type signed: list of pairs
161
@type signed: L{Message}
159
162
@type invalidate_handle: str
161
164
self.assoc_handle = assoc_handle
163
165
self.signed = signed
164
166
self.invalidate_handle = invalidate_handle
167
def fromQuery(klass, query):
168
"""Construct me from a web query.
170
@param query: The query parameters as a dictionary with each
171
key mapping to one value.
167
self.namespace = OPENID2_NS
170
def fromMessage(klass, message, op_endpoint=UNUSED):
171
"""Construct me from an OpenID Message.
173
@param message: An OpenID check_authentication Message
174
@type message: L{openid.message.Message}
174
176
@returntype: L{CheckAuthRequest}
176
178
self = klass.__new__(klass)
178
self.assoc_handle = query[OPENID_PREFIX + 'assoc_handle']
179
self.sig = query[OPENID_PREFIX + 'sig']
180
signed_list = query[OPENID_PREFIX + 'signed']
182
raise ProtocolError(query,
183
text="%s request missing required parameter %s"
185
(self.mode, e.args[0], query))
187
self.invalidate_handle = query.get(OPENID_PREFIX + 'invalidate_handle')
189
signed_list = signed_list.split(',')
191
for field in signed_list:
194
# XXX KLUDGE HAX WEB PROTOCoL BR0KENNN
195
# openid.mode is currently check_authentication because
196
# that's the mode of this request. But the signature
197
# was made on something with a different openid.mode.
198
# http://article.gmane.org/gmane.comp.web.openid.general/537
201
value = query[OPENID_PREFIX + field]
205
text="Couldn't find signed field %r in query %s"
208
signed_pairs.append((field, value))
210
self.signed = signed_pairs
179
self.message = message
180
self.namespace = message.getOpenIDNamespace()
181
self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
182
self.sig = message.getArg(OPENID_NS, 'sig')
184
if (self.assoc_handle is None or
186
fmt = "%s request missing required parameter from message %s"
188
message, text=fmt % (self.mode, message))
190
self.invalidate_handle = message.getArg(OPENID_NS, 'invalidate_handle')
192
self.signed = message.copy()
193
# openid.mode is currently check_authentication because
194
# that's the mode of this request. But the signature
195
# was made on something with a different openid.mode.
196
# http://article.gmane.org/gmane.comp.web.openid.general/537
197
if self.signed.hasKey(OPENID_NS, "mode"):
198
self.signed.setArg(OPENID_NS, "mode", "id_res")
213
fromQuery = classmethod(fromQuery)
202
fromMessage = classmethod(fromMessage)
216
205
def answer(self, signatory):
258
249
@cvar session_type: The session_type for this association
259
250
session. There is no type defined for plain-text in the OpenID
260
specification, so we use 'plaintext'.
251
specification, so we use 'no-encryption'.
261
252
@type session_type: str
263
254
@see: U{OpenID Specs, Mode: associate
264
255
<http://openid.net/specs.bml#mode-associate>}
265
256
@see: AssociateRequest
267
session_type = 'plaintext'
258
session_type = 'no-encryption'
259
allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
269
def fromQuery(cls, unused_request):
261
def fromMessage(cls, unused_request):
272
fromQuery = classmethod(fromQuery)
264
fromMessage = classmethod(fromMessage)
274
266
def answer(self, secret):
275
267
return {'mac_key': oidutil.toBase64(secret)}
278
class DiffieHellmanServerSession(object):
270
class DiffieHellmanSHA1ServerSession(object):
279
271
"""An object that knows how to handle association requests with the
280
272
Diffie-Hellman session type.
295
287
@see: AssociateRequest
297
289
session_type = 'DH-SHA1'
290
hash_func = staticmethod(cryptutil.sha1)
291
allowed_assoc_types = ['HMAC-SHA1']
299
293
def __init__(self, dh, consumer_pubkey):
301
295
self.consumer_pubkey = consumer_pubkey
303
def fromQuery(cls, query):
297
def fromMessage(cls, message):
305
@param query: The associate request's query parameters
306
@type query: {str:str}
299
@param message: The associate request message
300
@type message: openid.message.Message
308
@returntype: L{DiffieHellmanServerSession}
302
@returntype: L{DiffieHellmanSHA1ServerSession}
310
304
@raises ProtocolError: When parameters required to establish the
311
305
session are missing.
313
dh_modulus = query.get('openid.dh_modulus')
314
dh_gen = query.get('openid.dh_gen')
307
dh_modulus = message.getArg(OPENID_NS, 'dh_modulus')
308
dh_gen = message.getArg(OPENID_NS, 'dh_gen')
315
309
if (dh_modulus is None and dh_gen is not None or
316
310
dh_gen is None and dh_modulus is not None):
332
327
dh = DiffieHellman.fromDefaults()
334
consumer_pubkey = query.get('openid.dh_consumer_public')
329
consumer_pubkey = message.getArg(OPENID_NS, 'dh_consumer_public')
335
330
if consumer_pubkey is None:
336
raise ProtocolError("Public key for DH-SHA1 session "
337
"not found in query %s" % (query,))
331
raise ProtocolError(message, "Public key for DH-SHA1 session "
332
"not found in message %s" % (message,))
339
334
consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey)
341
336
return cls(dh, consumer_pubkey)
343
fromQuery = classmethod(fromQuery)
338
fromMessage = classmethod(fromMessage)
345
340
def answer(self, secret):
346
mac_key = self.dh.xorSecret(self.consumer_pubkey, secret)
341
mac_key = self.dh.xorSecret(self.consumer_pubkey,
348
345
'dh_server_public': cryptutil.longToBase64(self.dh.public),
349
346
'enc_mac_key': oidutil.toBase64(mac_key),
349
class DiffieHellmanSHA256ServerSession(DiffieHellmanSHA1ServerSession):
350
session_type = 'DH-SHA256'
351
hash_func = staticmethod(cryptutil.sha256)
352
allowed_assoc_types = ['HMAC-SHA256']
353
354
class AssociateRequest(OpenIDRequest):
354
355
"""A request to establish an X{association}.
384
385
super(AssociateRequest, self).__init__()
385
386
self.session = session
388
def fromQuery(klass, query):
389
"""Construct me from a web query.
391
@param query: The query parameters as a dictionary with each
392
key mapping to one value.
387
self.assoc_type = assoc_type
388
self.namespace = OPENID2_NS
391
def fromMessage(klass, message, op_endpoint=UNUSED):
392
"""Construct me from an OpenID Message.
394
@param message: The OpenID associate request
395
@type message: openid.message.Message
395
397
@returntype: L{AssociateRequest}
397
session_type = query.get(OPENID_PREFIX + 'session_type')
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'
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")
399
413
session_class = klass.session_classes[session_type]
401
raise ProtocolError(query,
415
raise ProtocolError(message,
402
416
"Unknown session type %r" % (session_type,))
405
session = session_class.fromQuery(query)
419
session = session_class.fromMessage(message)
406
420
except ValueError, why:
407
raise ProtocolError(query, 'Error parsing %s session: %s' %
421
raise ProtocolError(message, 'Error parsing %s session: %s' %
408
422
(session_class.session_type, why[0]))
410
return klass(session)
412
fromQuery = classmethod(fromQuery)
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))
429
self = klass(session, assoc_type)
430
self.message = message
431
self.namespace = message.getOpenIDNamespace()
434
fromMessage = classmethod(fromMessage)
414
436
def answer(self, assoc):
415
437
"""Respond to this request with an X{association}.
422
444
@returntype: L{OpenIDResponse}
424
446
response = OpenIDResponse(self)
425
response.fields.update({
447
response.fields.updateArgs(OPENID_NS, {
426
448
'expires_in': '%d' % (assoc.getExpiresIn(),),
427
'assoc_type': 'HMAC-SHA1',
449
'assoc_type': self.assoc_type,
428
450
'assoc_handle': assoc.handle,
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
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)
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)
467
response = OpenIDResponse(self)
468
response.fields.setArg(OPENID_NS, 'error_code', 'unsupported-type')
469
response.fields.setArg(OPENID_NS, 'error', message)
471
if preferred_association_type:
472
response.fields.setArg(
473
OPENID_NS, 'assoc_type', preferred_association_type)
475
if preferred_session_type:
476
response.fields.setArg(
477
OPENID_NS, 'session_type', preferred_session_type)
437
481
class CheckIDRequest(OpenIDRequest):
438
482
"""A request to confirm the identity of a user.
484
537
self.immediate = False
485
538
self.mode = "checkid_setup"
487
if not TrustRoot.parse(self.return_to):
540
if self.return_to is not None and \
541
not TrustRoot.parse(self.return_to):
488
542
raise MalformedReturnURL(None, self.return_to)
489
543
if not self.trustRootValid():
490
544
raise UntrustedReturnURL(None, self.return_to, self.trust_root)
493
def fromQuery(klass, query):
494
"""Construct me from a web query.
547
def fromMessage(klass, message, op_endpoint):
548
"""Construct me from an OpenID message.
496
550
@raises ProtocolError: When not all required parameters are present
499
553
@raises MalformedReturnURL: When the C{return_to} URL is not a URL.
501
555
@raises UntrustedReturnURL: When the C{return_to} URL is outside
502
556
the C{trust_root}.
504
@param query: The query parameters as a dictionary with each
505
key mapping to one value.
558
@param message: An OpenID checkid_* request Message
559
@type message: openid.message.Message
561
@param op_endpoint: The endpoint URL of the server that this
563
@type op_endpoint: str
508
565
@returntype: L{CheckIDRequest}
510
567
self = klass.__new__(klass)
511
mode = query[OPENID_PREFIX + 'mode']
568
self.message = message
569
self.namespace = message.getOpenIDNamespace()
570
self.op_endpoint = op_endpoint
571
mode = message.getArg(OPENID_NS, 'mode')
512
572
if mode == "checkid_immediate":
513
573
self.immediate = True
514
574
self.mode = "checkid_immediate"
516
576
self.immediate = False
517
577
self.mode = "checkid_setup"
524
for field in required:
525
value = query.get(OPENID_PREFIX + field)
529
text="Missing required field %s from %r"
531
setattr(self, field, value)
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,))
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 "
590
raise ProtocolError(message, text=s)
593
self.claimed_id = None
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)
533
599
# There's a case for making self.trust_root be a TrustRoot
534
600
# here. But if TrustRoot isn't currently part of the "public" API,
535
601
# I'm not sure it's worth doing.
536
self.trust_root = query.get(OPENID_PREFIX + 'trust_root', self.return_to)
537
self.assoc_handle = query.get(OPENID_PREFIX + 'assoc_handle')
602
if self.namespace == OPENID1_NS:
603
self.trust_root = message.getArg(
604
OPENID_NS, 'trust_root', self.return_to)
606
self.trust_root = message.getArg(
607
OPENID_NS, 'realm', self.return_to)
609
if self.return_to is self.trust_root is None:
610
raise ProtocolError(message, "openid.realm required when " +
611
"openid.return_to absent")
613
self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
539
615
# Using TrustRoot.parse here is a bit misleading, as we're not
540
616
# parsing return_to as a trust root at all. However, valid URLs
568
652
tr = TrustRoot.parse(self.trust_root)
570
654
raise MalformedTrustRoot(None, self.trust_root)
571
return tr.validateURL(self.return_to)
574
def answer(self, allow, server_url=None):
656
if self.return_to is not None:
657
return tr.validateURL(self.return_to)
661
def answer(self, allow, server_url=None, identity=None, claimed_id=None):
575
662
"""Respond to this request.
577
664
@param allow: Allow this user to claim this identity, and allow the
578
665
consumer to have this information?
579
666
@type allow: bool
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.
668
@param server_url: DEPRECATED. Passing C{op_endpoint} to the
669
L{Server} constructor makes this optional.
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.
588
678
Optional for requests where C{CheckIDRequest.immediate} is C{False}
589
679
or C{allow} is C{True}.
591
681
@type server_url: str
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
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
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.
697
This parameter is new in OpenID 2.0.
698
@type claimed_id: str or None
593
700
@returntype: L{OpenIDResponse}
702
@change: Version 2.0 deprecates C{server_url} and adds C{claimed_id}.
595
if allow or self.immediate:
704
# FIXME: undocumented exceptions
705
if not self.return_to:
706
raise NoReturnToError
709
if self.namespace != OPENID1_NS and not self.op_endpoint:
710
# In other words, that warning I raised in Server.__init__?
711
# You should pay attention to it now.
712
raise RuntimeError("%s should be constructed with op_endpoint "
713
"to respond to OpenID 2.0 messages." %
715
server_url = self.op_endpoint
719
elif self.namespace == OPENID1_NS:
726
mode = 'setup_needed'
600
730
response = OpenIDResponse(self)
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,))
736
if identity and not claimed_id:
737
claimed_id = identity
603
response.addFields(None, {
740
if self.identity == IDENTIFIER_SELECT:
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
749
if identity and (self.identity != identity):
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
759
"This request specified no identity and you "
760
"supplied %r" % (identity,))
761
response_identity = None
763
if self.namespace == OPENID1_NS and response_identity is None:
765
"Request was an OpenID 1 request, so response must "
766
"include an identifier."
769
response.fields.updateArgs(OPENID_NS, {
605
'identity': self.identity,
771
'op_endpoint': server_url,
606
772
'return_to': self.return_to,
773
'response_nonce': mkNonce(),
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)
609
response.addField(None, 'mode', mode, False)
783
response.fields.setArg(OPENID_NS, 'mode', mode)
610
784
if self.immediate:
785
if self.namespace == OPENID1_NS and not server_url:
612
786
raise ValueError("setup_url is required for allow=False "
613
"in immediate mode.")
787
"in OpenID 1.x immediate mode.")
614
788
# Make a new request just like me, but with immediate=False.
615
789
setup_request = self.__class__(
616
790
self.identity, self.return_to, self.trust_root,
617
immediate=False, assoc_handle=self.assoc_handle)
791
immediate=False, assoc_handle=self.assoc_handle,
792
op_endpoint=self.op_endpoint)
618
793
setup_url = setup_request.encodeToURL(server_url)
619
response.addField(None, 'user_setup_url', setup_url, False)
794
response.fields.setArg(OPENID_NS, 'user_setup_url', setup_url)
807
if not self.return_to:
808
raise NoReturnToError
632
810
# Imported from the alternate reality where these classes are used
633
811
# in both the client and server code, so Requests are Encodable too.
634
812
# That's right, code imported from alternate realities all for the
635
813
# love of you, id_res/user_setup_url.
636
814
q = {'mode': self.mode,
637
815
'identity': self.identity,
816
'claimed_id': self.claimed_id,
638
817
'return_to': self.return_to}
639
818
if self.trust_root:
640
q['trust_root'] = self.trust_root
819
if self.namespace == OPENID1_NS:
820
q['trust_root'] = self.trust_root
822
q['realm'] = self.trust_root
641
823
if self.assoc_handle:
642
824
q['assoc_handle'] = self.assoc_handle
644
q = dict([(OPENID_PREFIX + k, v) for k, v in q.iteritems()])
646
return oidutil.appendArgs(server_url, q)
826
response = Message(self.namespace)
827
response.updateArgs(self.namespace, q)
828
return response.toURL(server_url)
649
831
def getCancelURL(self):
716
def addField(self, namespace, key, value, signed=True):
717
"""Add a field to this response.
719
@param namespace: The extension namespace the field is in, with no
720
leading "C{openid.}" e.g. "C{sreg}".
723
@param key: The field's name, e.g. "C{fullname}".
726
@param value: The field's value.
729
@param signed: Whether this field should be signed.
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)
739
def addFields(self, namespace, fields, signed=True):
740
"""Add a number of fields to this response.
742
@param namespace: The extension namespace the field is in, with no
743
leading "C{openid.}" e.g. "C{sreg}".
746
@param fields: A dictionary with the fields to add.
747
e.g. C{{"fullname": "Frank the Goat"}}
749
@param signed: Whether these fields should be signed.
752
for key, value in fields.iteritems():
753
self.addField(namespace, key, value, signed)
756
def update(self, namespace, other):
757
"""Update my fields with those from another L{OpenIDResponse}.
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.
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.
769
@param namespace: The extension namespace the field is in, with no
770
leading "C{openid.}" e.g. "C{sreg}".
773
@param other: A response object to update from.
774
@type other: L{OpenIDResponse}
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
782
namespaced_fields = other.fields
783
namespaced_signed = other.signed
784
self.fields.update(namespaced_fields)
785
self.signed.extend(namespaced_signed)
788
901
def needsSigning(self):
789
902
"""Does this response require signing?
791
904
@returntype: bool
794
(self.request.mode in ['checkid_setup', 'checkid_immediate'])
906
return self.fields.getArg(OPENID_NS, 'mode') == 'id_res'
799
909
# implements IEncodable
908
1023
self.store = store
911
def verify(self, assoc_handle, sig, signed_pairs):
1026
def verify(self, assoc_handle, message):
912
1027
"""Verify that the signature for some data is valid.
914
1029
@param assoc_handle: The handle of the association used to sign the
916
1031
@type assoc_handle: str
918
@param sig: The base-64 encoded signature to check.
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
1033
@param message: The signed message to verify
1034
@type message: openid.message.Message
926
1036
@returns: C{True} if the signature is valid, C{False} if not.
927
1037
@returntype: bool
929
1039
assoc = self.getAssociation(assoc_handle, dumb=True)
931
oidutil.log("failed to get assoc with handle %r to verify sig %r"
932
% (assoc_handle, sig))
935
# Not using Association.checkSignature here is intentional;
936
# Association should not know things like "the list of signed pairs is
937
# in the request's 'signed' parameter and it is comma-separated."
938
expected_sig = oidutil.toBase64(assoc.sign(signed_pairs))
940
return sig == expected_sig
1041
oidutil.log("failed to get assoc with handle %r to verify "
1043
% (assoc_handle, message))
1047
valid = assoc.checkMessageSignature(message)
1048
except ValueError, ex:
1049
oidutil.log("Error in verifying %s with %s: %s" % (message,
943
1056
def sign(self, response):
957
1070
assoc_handle = response.request.assoc_handle
958
1071
if assoc_handle:
960
assoc = self.getAssociation(assoc_handle, dumb=False)
1073
# disabling expiration check because even if the association
1074
# is expired, we still need to know some properties of the
1075
# association so that we may preserve those properties when
1076
# creating the fallback association.
1077
assoc = self.getAssociation(assoc_handle, dumb=False,
1078
checkExpiration=False)
1080
if not assoc or assoc.expiresIn <= 0:
962
1081
# fall back to dumb mode
963
signed_response.fields['invalidate_handle'] = assoc_handle
964
assoc = self.createAssociation(dumb=True)
1082
signed_response.fields.setArg(
1083
OPENID_NS, 'invalidate_handle', assoc_handle)
1084
assoc_type = assoc and assoc.assoc_type or 'HMAC-SHA1'
1085
if assoc and assoc.expiresIn <= 0:
1086
# now do the clean-up that the disabled checkExpiration
1087
# code didn't get to do.
1088
self.invalidate(assoc_handle, dumb=False)
1089
assoc = self.createAssociation(dumb=True, assoc_type=assoc_type)
967
1092
assoc = self.createAssociation(dumb=True)
969
signed_response.fields['assoc_handle'] = assoc.handle
970
assoc.addSignature(signed_response.signed, signed_response.fields,
1094
signed_response.fields = assoc.signMessage(signed_response.fields)
972
1095
return signed_response
1155
myquery = dict(filter(lambda (k, v): k.startswith(OPENID_PREFIX),
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,))
1288
message = Message.fromPostArgs(query)
1290
mode = message.getArg(OPENID_NS, 'mode')
1166
raise ProtocolError(
1168
text="No %smode value in query %r" % (
1169
OPENID_PREFIX, query))
1292
fmt = "No mode value in message %s"
1293
raise ProtocolError(message, text=fmt % (message,))
1170
1295
handler = self._handlers.get(mode, self.defaultDecoder)
1171
return handler(query)
1174
def defaultDecoder(self, query):
1296
return handler(message, self.server.op_endpoint)
1299
def defaultDecoder(self, message, server):
1175
1300
"""Called to decode queries when no handler for that mode is found.
1177
1302
@raises ProtocolError: This implementation always raises
1178
1303
L{ProtocolError}.
1180
mode = query[OPENID_PREFIX + 'mode']
1181
raise ProtocolError(
1183
text="No decoder for mode %r" % (mode,))
1305
mode = message.getArg(OPENID_NS, 'mode')
1306
fmt = "No decoder for mode %r"
1307
raise ProtocolError(message, text=fmt % (mode,))
1225
1349
@ivar encoder: I'm using this to encode things.
1226
1350
@type encoder: L{Encoder}
1352
@ivar op_endpoint: My URL.
1353
@type op_endpoint: str
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}
1229
1360
signatoryClass = Signatory
1230
1361
encoderClass = SigningEncoder
1231
1362
decoderClass = Decoder
1233
def __init__(self, store):
1364
def __init__(self, store, op_endpoint=None):
1234
1365
"""A new L{Server}.
1236
1367
@param store: The back-end where my associations are stored.
1237
1368
@type store: L{openid.store.interface.OpenIDStore}
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
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.
1239
1379
self.store = store
1240
1380
self.signatory = self.signatoryClass(self.store)
1241
1381
self.encoder = self.encoderClass(self.signatory)
1242
self.decoder = self.decoderClass()
1382
self.decoder = self.decoderClass(self)
1383
self.negotiator = default_negotiator.copy()
1386
warnings.warn("%s.%s constructor requires op_endpoint parameter "
1387
"for OpenID 2.0 servers" %
1388
(self.__class__.__module__, self.__class__.__name__),
1390
self.op_endpoint = op_endpoint
1245
1393
def handleRequest(self, request):
1273
1423
def openid_associate(self, request):
1274
"""Handle and respond to {associate} requests.
1424
"""Handle and respond to C{associate} requests.
1276
1426
@returntype: L{OpenIDResponse}
1278
assoc = self.signatory.createAssociation(dumb=False)
1279
return request.answer(assoc)
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)
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(
1442
preferred_assoc_type,
1443
preferred_session_type)
1282
1446
def decodeRequest(self, query):
1318
1482
class ProtocolError(Exception):
1319
1483
"""A message did not conform to the OpenID protocol.
1321
@ivar query: The query that is failing to be a valid OpenID request.
1485
@ivar message: The query that is failing to be a valid OpenID request.
1486
@type message: openid.message.Message
1325
def __init__(self, query, text=None):
1489
def __init__(self, message, text=None, reference=None, contact=None):
1326
1490
"""When an error occurs.
1328
@param query: The query that is failing to be a valid OpenID request.
1492
@param message: The message that is failing to be a valid
1494
@type message: openid.message.Message
1331
1496
@param text: A message about the encountered error. Set as C{args[0]}.
1332
1497
@type text: str
1499
self.openid_message = message
1500
self.reference = reference
1501
self.contact = contact
1502
assert type(message) not in [str, unicode]
1335
1503
Exception.__init__(self, text)
1506
def getReturnTo(self):
1507
"""Get the return_to argument from the request, if any.
1511
if self.openid_message is None:
1514
return self.openid_message.getArg(OPENID_NS, 'return_to')
1338
1516
def hasReturnTo(self):
1339
1517
"""Did this request have a return_to parameter?
1341
1519
@returntype: bool
1343
if self.query is None:
1346
return (OPENID_PREFIX + 'return_to') in self.query
1521
return self.getReturnTo() is not None
1523
def toMessage(self):
1524
"""Generate a Message object for sending to the relying party,
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))
1532
if self.contact is not None:
1533
reply.setArg(OPENID_NS, 'contact', str(self.contact))
1535
if self.reference is not None:
1536
reply.setArg(OPENID_NS, 'reference', str(self.reference))
1349
1540
# implements IEncodable
1351
1542
def encodeToURL(self):
1352
"""Encode a response as a URL for the user agent to GET.
1354
You will generally use this URL with a HTTP redirect.
1356
@returns: A URL to direct the user agent back to.
1359
return_to = self.query.get(OPENID_PREFIX + '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),
1543
return self.toMessage().toURL(self.getReturnTo())
1368
1545
def encodeToKVForm(self):
1369
"""Encode a response in key-value colon/newline format.
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.
1375
U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>}
1379
return kvform.dictToKV({
1546
return self.toMessage().toKVForm()
1385
1548
def whichEncoding(self):
1386
1549
"""How should I be encoded?