382
445
def getRequest(self):
383
446
cpub = cryptutil.longToBase64(self.dh.public)
385
args = {'openid.dh_consumer_public': cpub}
448
args = {'dh_consumer_public': cpub}
387
450
if not self.dh.usingDefaultValues():
389
'openid.dh_modulus': cryptutil.longToBase64(self.dh.modulus),
390
'openid.dh_gen': cryptutil.longToBase64(self.dh.generator),
452
'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
453
'dh_gen': cryptutil.longToBase64(self.dh.generator),
395
458
def extractSecret(self, response):
396
spub = cryptutil.base64ToLong(response['dh_server_public'])
397
enc_mac_key = oidutil.fromBase64(response['enc_mac_key'])
398
return self.dh.xorSecret(spub, enc_mac_key)
459
dh_server_public64 = response.getArg(
460
OPENID_NS, 'dh_server_public', no_default)
461
enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
462
dh_server_public = cryptutil.base64ToLong(dh_server_public64)
463
enc_mac_key = oidutil.fromBase64(enc_mac_key64)
464
return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
466
class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
467
session_type = 'DH-SHA256'
468
hash_func = staticmethod(cryptutil.sha256)
470
allowed_assoc_types = ['HMAC-SHA256']
400
472
class PlainTextConsumerSession(object):
473
session_type = 'no-encryption'
474
allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
403
476
def getRequest(self):
406
479
def extractSecret(self, response):
407
return oidutil.fromBase64(response['mac_key'])
480
mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
481
return oidutil.fromBase64(mac_key64)
483
class SetupNeededError(Exception):
484
"""Internally-used exception that indicates that an immediate-mode
485
request cancelled."""
486
def __init__(self, user_setup_url=None):
487
Exception.__init__(self, user_setup_url)
488
self.user_setup_url = user_setup_url
490
class ProtocolError(ValueError):
491
"""Exception that indicates that a message violated the
492
protocol. It is raised and caught internally to this file."""
494
class TypeURIMismatch(ProtocolError):
495
"""A protocol error arising from type URIs mismatching
498
class ServerError(Exception):
499
"""Exception that is raised when the server returns a 400 response
500
code to a direct request."""
502
def __init__(self, error_text, error_code, message):
503
Exception.__init__(self, error_text)
504
self.error_text = error_text
505
self.error_code = error_code
506
self.message = message
508
def fromMessage(cls, message):
509
"""Generate a ServerError instance, extracting the error text
510
and the error code from the message."""
511
error_text = message.getArg(
512
OPENID_NS, 'error', '<no error message supplied>')
513
error_code = message.getArg(OPENID_NS, 'error_code')
514
return cls(error_text, error_code, message)
516
fromMessage = classmethod(fromMessage)
409
518
class GenericConsumer(object):
410
519
"""This is the implementation of the common logic for OpenID
411
520
consumers. It is unaware of the application in which it is
523
@ivar negotiator: An object that controls the kind of associations
524
that the consumer makes. It defaults to
525
C{L{openid.association.default_negotiator}}. Assign a
526
different negotiator to it if you have specific requirements
527
for how associations are made.
528
@type negotiator: C{L{openid.association.SessionNegotiator}}
416
NONCE_CHRS = string.ascii_letters + string.digits
531
# The name of the query parameter that gets added to the return_to
532
# URL when using OpenID1. You can change this value if you want or
533
# need a different name, but don't make it start with openid,
534
# because it's not a standard protocol thing for OpenID1. For
535
# OpenID2, the library will take care of the nonce using standard
536
# OpenID query parameter names.
537
openid1_nonce_query_arg_name = 'janrain_nonce'
540
'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
541
'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
542
'no-encryption':PlainTextConsumerSession,
545
_discover = staticmethod(discover)
418
547
def __init__(self, store):
419
548
self.store = store
549
self.negotiator = default_negotiator.copy()
421
551
def begin(self, service_endpoint):
422
nonce = self._createNonce()
423
assoc = self._getAssociation(service_endpoint.server_url)
552
"""Create an AuthRequest object for the specified
553
service_endpoint. This method will create an association if
555
if self.store is None:
558
assoc = self._getAssociation(service_endpoint)
424
560
request = AuthRequest(service_endpoint, assoc)
425
request.return_to_args['nonce'] = nonce
561
request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
428
def complete(self, query, endpoint):
429
mode = query.get('openid.mode', '<no mode specified>')
564
def complete(self, message, endpoint, return_to=None):
565
"""Process the OpenID message, using the specified endpoint
566
and return_to URL as context. This method will handle any
567
OpenID message that is sent to the return_to URL.
569
mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
431
if isinstance(mode, list):
432
raise TypeError("query dict must have one value for each key, "
433
"not lists of values. Query is %r" % (query,))
571
if return_to is not None:
572
if not self._checkReturnTo(message, return_to):
573
return FailureResponse(endpoint,
574
"openid.return_to does not match return URL")
435
576
if mode == 'cancel':
436
577
return CancelResponse(endpoint)
437
578
elif mode == 'error':
438
error = query.get('openid.error')
439
return FailureResponse(endpoint, error)
579
error = message.getArg(OPENID_NS, 'error')
580
contact = message.getArg(OPENID_NS, 'contact')
581
reference = message.getArg(OPENID_NS, 'reference')
583
return FailureResponse(endpoint, error, contact=contact,
585
elif message.isOpenID2() and mode == 'setup_needed':
586
return SetupNeededResponse(endpoint)
440
588
elif mode == 'id_res':
441
if endpoint.identity_url is None:
442
return FailureResponse(endpoint, 'No session state found')
444
response = self._doIdRes(query, endpoint)
445
except fetchers.HTTPFetchingError, why:
446
message = 'HTTP request failed: %s' % (str(why),)
447
return FailureResponse(endpoint, message)
590
self._checkSetupNeeded(message)
591
except SetupNeededError, why:
592
return SetupNeededResponse(endpoint, why.user_setup_url)
449
if response.status == 'success':
450
return self._checkNonce(response, query.get('nonce'))
595
return self._doIdRes(message, endpoint)
596
except (ProtocolError, DiscoveryFailure), why:
597
return FailureResponse(endpoint, why[0])
454
599
return FailureResponse(endpoint,
455
600
'Invalid openid.mode: %r' % (mode,))
457
def _checkNonce(self, response, nonce):
458
parsed_url = urlparse(response.getReturnTo())
602
def _checkReturnTo(self, message, return_to):
603
"""Check an OpenID message and its openid.return_to value
604
against a return_to URL from an application. Return True on
605
success, False on failure.
607
# Check the openid.return_to args against args in the original
610
self._verifyReturnToArgs(message.toPostArgs())
611
except ProtocolError, why:
612
oidutil.log("Verifying return_to arguments: %s" % (why[0],))
615
# Check the return_to base URL against the one in the message.
616
msg_return_to = message.getArg(OPENID_NS, 'return_to')
618
# The URL scheme, authority, and path MUST be the same between
620
app_parts = urlparse(return_to)
621
msg_parts = urlparse(msg_return_to)
623
# (addressing scheme, network location, path) must be equal in
625
for part in range(0, 3):
626
if app_parts[part] != msg_parts[part]:
631
_makeKVPost = staticmethod(makeKVPost)
633
def _checkSetupNeeded(self, message):
634
"""Check an id_res message to see if it is a
635
checkid_immediate cancel response.
637
@raises SetupNeededError: if it is a checkid_immediate cancellation
639
# In OpenID 1, we check to see if this is a cancel from
640
# immediate mode by the presence of the user_setup_url
642
if message.isOpenID1():
643
user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
644
if user_setup_url is not None:
645
raise SetupNeededError(user_setup_url)
647
def _doIdRes(self, message, endpoint):
648
"""Handle id_res responses that are not cancellations of
649
immediate mode requests.
651
@param message: the response paramaters.
652
@param endpoint: the discovered endpoint object. May be None.
654
@raises ProtocolError: If the message contents are not
655
well-formed according to the OpenID specification. This
656
includes missing fields or not signing fields that should
659
@raises DiscoveryFailure: If the subject of the id_res message
660
does not match the supplied endpoint, and discovery on the
661
identifier in the message fails (this should only happen
664
@returntype: L{Response}
666
signed_list_str = message.getArg(OPENID_NS, 'signed')
667
if signed_list_str is None:
668
raise ProtocolError("Response missing signed list")
670
signed_list = signed_list_str.split(',')
672
# Checks for presence of appropriate fields (and checks
673
# signed list fields)
674
self._idResCheckForFields(message, signed_list)
676
# Verify discovery information:
677
endpoint = self._verifyDiscoveryResults(message, endpoint)
679
self._idResCheckSignature(message, endpoint.server_url)
681
response_identity = message.getArg(OPENID_NS, 'identity')
683
# Will raise a ProtocolError if the nonce is bad
684
self._idResCheckNonce(message, endpoint)
686
signed_fields = ["openid." + s for s in signed_list]
687
return SuccessResponse(endpoint, message, signed_fields)
689
def _idResGetNonceOpenID1(self, message, endpoint):
690
"""Extract the nonce from an OpenID 1 response
692
See the openid1_nonce_query_arg_name class variable
694
@returns: The nonce as a string or None
696
return_to = message.getArg(OPENID1_NS, 'return_to', None)
697
if return_to is None:
700
parsed_url = urlparse(return_to)
459
701
query = parsed_url[4]
460
702
for k, v in cgi.parse_qsl(query):
463
return FailureResponse(response, 'Nonce mismatch')
467
return FailureResponse(response, 'Nonce missing from return_to: %r'
468
% (response.getReturnTo()))
470
# The nonce matches the signed nonce in the openid.return_to
472
if not self.store.useNonce(nonce):
473
return FailureResponse(response,
474
'Nonce missing from store')
476
# If the nonce check succeeded, return the original success
480
def _createNonce(self):
481
nonce = cryptutil.randomString(self.NONCE_LEN, self.NONCE_CHRS)
482
self.store.storeNonce(nonce)
485
def _makeKVPost(self, args, server_url):
486
mode = args['openid.mode']
487
body = urllib.urlencode(args)
489
resp = fetchers.fetch(server_url, body=body)
491
fmt = 'openid.mode=%s: failed to fetch URL: %s'
492
oidutil.log(fmt % (mode, server_url))
495
response = kvform.kvToDict(resp.body)
496
if resp.status == 400:
497
server_error = response.get('error', '<no message from server>')
498
fmt = 'openid.mode=%s: error returned from server %s: %s'
499
oidutil.log(fmt % (mode, server_url, server_error))
501
elif resp.status != 200:
502
fmt = 'openid.mode=%s: bad status code from server %s: %s'
503
oidutil.log(fmt % (mode, server_url, resp.status))
508
def _doIdRes(self, query, endpoint):
509
"""Handle id_res responses.
511
@param query: the response paramaters.
512
@param consumer_id: The normalized Claimed Identifier.
513
@param server_id: The Delegate Identifier.
514
@param server_url: OpenID server endpoint URL.
516
@returntype: L{Response}
518
user_setup_url = query.get('openid.user_setup_url')
519
if user_setup_url is not None:
520
return SetupNeededResponse(endpoint, user_setup_url)
522
return_to = query.get('openid.return_to')
523
server_id2 = query.get('openid.identity')
524
assoc_handle = query.get('openid.assoc_handle')
526
if return_to is None or server_id2 is None or assoc_handle is None:
527
return FailureResponse(endpoint, 'Missing required field')
529
if endpoint.getServerID() != server_id2:
530
return FailureResponse(endpoint, 'Server ID (delegate) mismatch')
532
signed = query.get('openid.signed')
534
assoc = self.store.getAssociation(endpoint.server_url, assoc_handle)
537
# It's not an association we know about. Dumb mode is our
703
if k == self.openid1_nonce_query_arg_name:
708
def _idResCheckNonce(self, message, endpoint):
709
if message.isOpenID1():
710
# This indicates that the nonce was generated by the consumer
711
nonce = self._idResGetNonceOpenID1(message, endpoint)
714
nonce = message.getArg(OPENID2_NS, 'response_nonce')
715
server_url = endpoint.server_url
718
raise ProtocolError('Nonce missing from response')
721
timestamp, salt = splitNonce(nonce)
722
except ValueError, why:
723
raise ProtocolError('Malformed nonce: %s' % (why[0],))
725
if (self.store is not None and
726
not self.store.useNonce(server_url, timestamp, salt)):
727
raise ProtocolError('Nonce already used or out of range')
729
def _idResCheckSignature(self, message, server_url):
730
assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
731
if self.store is None:
734
assoc = self.store.getAssociation(server_url, assoc_handle)
737
if assoc.getExpiresIn() <= 0:
738
# XXX: It might be a good idea sometimes to re-start the
739
# authentication with a new association. Doing it
740
# automatically opens the possibility for
741
# denial-of-service by a server that just returns expired
742
# associations (or really short-lived associations)
744
'Association with %s expired' % (server_url,))
746
if not assoc.checkMessageSignature(message):
747
raise ProtocolError('Bad signature')
750
# It's not an association we know about. Stateless mode is our
538
751
# only possible path for recovery.
539
if self._checkAuth(query, endpoint.server_url):
540
return SuccessResponse.fromQuery(endpoint, query, signed)
752
# XXX - async framework will not want to block on this call to
754
if not self._checkAuth(message, server_url):
755
raise ProtocolError('Server denied check_authentication')
757
def _idResCheckForFields(self, message, signed_list):
758
# XXX: this should be handled by the code that processes the
759
# response (that is, if a field is missing, we should not have
760
# to explicitly check that it's present, just make sure that
761
# the fields are actually being used by the rest of the code
762
# in tests). Although, which fields are signed does need to be
764
basic_fields = ['return_to', 'assoc_handle', 'sig']
765
basic_sig_fields = ['return_to', 'identity']
768
OPENID2_NS: basic_fields + ['op_endpoint'],
769
OPENID1_NS: basic_fields + ['identity'],
773
OPENID2_NS: basic_sig_fields + ['response_nonce',
776
OPENID1_NS: basic_sig_fields,
779
for field in require_fields[message.getOpenIDNamespace()]:
780
if not message.hasKey(OPENID_NS, field):
781
raise ProtocolError('Missing required field %r' % (field,))
783
for field in require_sigs[message.getOpenIDNamespace()]:
784
# Field is present and not in signed list
785
if message.hasKey(OPENID_NS, field) and field not in signed_list:
786
raise ProtocolError('"%s" not signed' % (field,))
789
def _verifyReturnToArgs(query):
790
"""Verify that the arguments in the return_to URL are present in this
793
message = Message.fromPostArgs(query)
794
return_to = message.getArg(OPENID_NS, 'return_to')
796
# XXX: this should be checked by _idResCheckForFields
798
raise ProtocolError("no openid.return_to in query %r" % (query,))
799
parsed_url = urlparse(return_to)
800
rt_query = parsed_url[4]
801
for rt_key, rt_value in cgi.parse_qsl(rt_query):
803
value = query[rt_key]
804
if rt_value != value:
805
format = ("parameter %s value %r does not match "
806
"return_to's value %r")
807
raise ProtocolError(format % (rt_key, value, rt_value))
809
format = "return_to parameter %s absent from query %r"
810
raise ProtocolError(format % (rt_key, query))
812
_verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
814
def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
816
Extract the information from an OpenID assertion message and
817
verify it against the original
819
@param endpoint: The endpoint that resulted from doing discovery
820
@param resp_msg: The id_res message object
822
if resp_msg.getOpenIDNamespace() == OPENID2_NS:
823
return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
825
return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
828
def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
829
to_match = OpenIDServiceEndpoint()
830
to_match.type_uris = [OPENID_2_0_TYPE]
831
to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
832
to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
834
# Raises a KeyError when the op_endpoint is not present
835
to_match.server_url = resp_msg.getArg(
836
OPENID2_NS, 'op_endpoint', no_default)
838
# claimed_id and identifier must both be present or both
840
if (to_match.claimed_id is None and
841
to_match.local_id is not None):
843
'openid.identity is present without openid.claimed_id')
845
elif (to_match.claimed_id is not None and
846
to_match.local_id is None):
848
'openid.claimed_id is present without openid.identity')
850
# This is a response without identifiers, so there's really no
851
# checking that we can do, so return an endpoint that's for
852
# the specified `openid.op_endpoint'
853
elif to_match.claimed_id is None:
854
return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
856
# The claimed ID doesn't match, so we have to do discovery
857
# again. This covers not using sessions, OP identifier
858
# endpoints and responses that didn't match the original
861
oidutil.log('No pre-discovered information supplied.')
862
return self._discoverAndVerify(to_match)
864
elif to_match.claimed_id != endpoint.claimed_id:
865
oidutil.log('Mismatched pre-discovered session data. '
866
'Claimed ID in session=%s, in assertion=%s' %
867
(endpoint.claimed_id, to_match.claimed_id))
868
return self._discoverAndVerify(to_match)
870
# The claimed ID matches, so we use the endpoint that we
871
# discovered in initiation. This should be the most common
874
self._verifyDiscoverySingle(endpoint, to_match)
877
def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
880
'When using OpenID 1, the claimed ID must be supplied, '
881
'either by passing it through as a return_to parameter '
882
'or by using a session, and supplied to the GenericConsumer '
883
'as the argument to complete()')
885
to_match = OpenIDServiceEndpoint()
886
to_match.type_uris = [OPENID_1_1_TYPE]
887
to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
888
# Restore delegate information from the initiation phase
889
to_match.claimed_id = endpoint.claimed_id
891
if to_match.local_id is None:
892
raise ProtocolError('Missing required field openid.identity')
894
to_match_1_0 = copy.copy(to_match)
895
to_match_1_0.type_uris = [OPENID_1_0_TYPE]
898
self._verifyDiscoverySingle(endpoint, to_match)
899
except TypeURIMismatch:
900
self._verifyDiscoverySingle(endpoint, to_match_1_0)
904
def _verifyDiscoverySingle(self, endpoint, to_match):
905
"""Verify that the given endpoint matches the information
906
extracted from the OpenID assertion, and raise an exception if
909
@type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
910
@type to_match: openid.consumer.discover.OpenIDServiceEndpoint
914
@raises ProtocolError: when the endpoint does not match the
915
discovered information.
917
# Every type URI that's in the to_match endpoint has to be
918
# present in the discovered endpoint.
919
for type_uri in to_match.type_uris:
920
if not endpoint.usesExtension(type_uri):
921
raise TypeURIMismatch(
922
'Required type %r not present' % (type_uri,))
924
if to_match.claimed_id != endpoint.claimed_id:
926
'Claimed ID does not match (different subjects!), '
927
'Expected %s, got %s' %
928
(to_match.claimed_id, endpoint.claimed_id))
930
if to_match.getLocalID() != endpoint.getLocalID():
931
raise ProtocolError('local_id mismatch. Expected %s, got %s' %
932
(to_match.getLocalID(), endpoint.getLocalID()))
934
# If the server URL is None, this must be an OpenID 1
935
# response, because op_endpoint is a required parameter in
936
# OpenID 2. In that case, we don't actually care what the
937
# discovered server_url is, because signature checking or
938
# check_auth should take care of that check for us.
939
if to_match.server_url is None:
940
assert to_match.preferredNamespace() == OPENID1_NS, (
941
"""The code calling this must ensure that OpenID 2
942
responses have a non-none `openid.op_endpoint' and
943
that it is set as the `server_url' attribute of the
944
`to_match' endpoint.""")
946
elif to_match.server_url != endpoint.server_url:
947
raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
948
(to_match.server_url, endpoint.server_url))
950
def _discoverAndVerify(self, to_match):
951
"""Given an endpoint object created from the information in an
952
OpenID response, perform discovery and verify the discovery
953
results, returning the matching endpoint that is the result of
954
doing that discovery.
956
@type to_match: openid.consumer.discover.OpenIDServiceEndpoint
957
@param to_match: The endpoint whose information we're confirming
959
@rtype: openid.consumer.discover.OpenIDServiceEndpoint
960
@returns: The result of performing discovery on the claimed
961
identifier in `to_match'
963
@raises ProtocolError: when discovery fails.
965
oidutil.log('Performing discovery on %s' % (to_match.claimed_id,))
966
_, services = self._discover(to_match.claimed_id)
968
raise DiscoveryFailure('No OpenID information found at %s' %
969
(to_match.claimed_id,), None)
971
# Search the services resulting from discovery to find one
972
# that matches the information from the assertion
973
failure_messages = []
974
for endpoint in services:
976
self._verifyDiscoverySingle(endpoint, to_match)
977
except ProtocolError, why:
978
failure_messages.append(why[0])
542
return FailureResponse(endpoint,
543
'Server denied check_authentication')
545
if assoc.expiresIn <= 0:
546
# XXX: It might be a good idea sometimes to re-start the
547
# authentication with a new association. Doing it
548
# automatically opens the possibility for
549
# denial-of-service by a server that just returns expired
550
# associations (or really short-lived associations)
551
msg = 'Association with %s expired' % (endpoint.server_url,)
552
return FailureResponse(endpoint, msg)
554
# Check the signature
555
sig = query.get('openid.sig')
556
if sig is None or signed is None:
557
return FailureResponse(endpoint, 'Missing argument signature')
559
signed_list = signed.split(',')
561
# Fail if the identity field is present but not signed
562
if endpoint.identity_url is not None and 'identity' not in signed_list:
563
msg = '"openid.identity" not signed'
564
return FailureResponse(endpoint, msg)
566
v_sig = assoc.signDict(signed_list, query)
569
return FailureResponse(endpoint, 'Bad signature')
571
return SuccessResponse.fromQuery(endpoint, query, signed)
573
def _checkAuth(self, query, server_url):
574
request = self._createCheckAuthRequest(query)
980
# It matches, so discover verification has
981
# succeeded. Return this endpoint.
984
oidutil.log('Discovery verification failure for %s' %
985
(to_match.claimed_id,))
986
for failure_message in failure_messages:
987
oidutil.log(' * Endpoint mismatch: ' + failure_message)
989
raise DiscoveryFailure(
990
'No matching endpoint found after discovering %s'
991
% (to_match.claimed_id,), None)
993
def _checkAuth(self, message, server_url):
994
oidutil.log('Using OpenID check_authentication')
995
request = self._createCheckAuthRequest(message)
575
996
if request is None:
577
response = self._makeKVPost(request, server_url)
999
response = self._makeKVPost(request, server_url)
1000
except (fetchers.HTTPFetchingError, ServerError), e:
1001
oidutil.log('check_authentication failed: %s' % (e[0],))
580
return self._processCheckAuthResponse(response, server_url)
582
def _createCheckAuthRequest(self, query):
583
signed = query.get('openid.signed')
585
oidutil.log('No signature present; checkAuth aborted')
1004
return self._processCheckAuthResponse(response, server_url)
1006
def _createCheckAuthRequest(self, message):
1007
"""Generate a check_authentication request message given an
588
1010
# Arguments that are always passed to the server and not
589
1011
# included in the signature.
590
1012
whitelist = ['assoc_handle', 'sig', 'signed', 'invalidate_handle']
591
signed = signed.split(',') + whitelist
593
check_args = dict([(k, v) for k, v in query.iteritems()
594
if k.startswith('openid.') and k[7:] in signed])
596
check_args['openid.mode'] = 'check_authentication'
1016
val = message.getArg(OPENID_NS, k)
1020
signed = message.getArg(OPENID_NS, 'signed')
1022
for k in signed.split(','):
1024
check_args['ns'] = message.getOpenIDNamespace()
1027
val = message.getAliasedArg(k)
1029
# Signed value is missing
1031
oidutil.log('Missing signed field %r' % (k,))
1036
check_args['mode'] = 'check_authentication'
1037
return Message.fromOpenIDArgs(check_args)
599
1039
def _processCheckAuthResponse(self, response, server_url):
600
is_valid = response.get('is_valid', 'false')
1040
"""Process the response message from a check_authentication
1041
request, invalidating associations if requested.
1043
is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
602
invalidate_handle = response.get('invalidate_handle')
1045
invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
603
1046
if invalidate_handle is not None:
604
self.store.removeAssociation(server_url, invalidate_handle)
1048
'Received "invalidate_handle" from server %s' % (server_url,))
1049
if self.store is None:
1050
oidutil.log('Unexpectedly got invalidate_handle without '
1053
self.store.removeAssociation(server_url, invalidate_handle)
606
1055
if is_valid == 'true':
609
1058
oidutil.log('Server responds that checkAuth call is not valid')
612
def _getAssociation(self, server_url):
613
if self.store.isDumb():
616
assoc = self.store.getAssociation(server_url)
1061
def _getAssociation(self, endpoint):
1062
"""Get an association for the endpoint's server_url.
1064
First try seeing if we have a good association in the
1065
store. If we do not, then attempt to negotiate an association
1068
If we negotiate a good association, it will get stored.
1070
@returns: A valid association for the endpoint's server_url or None
1071
@rtype: openid.association.Association or NoneType
1073
assoc = self.store.getAssociation(endpoint.server_url)
618
1075
if assoc is None or assoc.expiresIn <= 0:
619
assoc_session, args = self._createAssociateRequest(server_url)
621
response = self._makeKVPost(args, server_url)
622
except fetchers.HTTPFetchingError, why:
623
oidutil.log('openid.associate request failed: %s' %
1076
assoc = self._negotiateAssociation(endpoint)
1077
if assoc is not None:
1078
self.store.storeAssociation(endpoint.server_url, assoc)
1082
def _negotiateAssociation(self, endpoint):
1083
"""Make association requests to the server, attempting to
1084
create a new association.
1086
@returns: a new association object
1088
@rtype: openid.association.Association
1090
@raises Exception: errors that the fetcher might raise. These are
1091
intended to be propagated up to the library's entrance point.
1093
# Get our preferred session/association type from the negotiatior.
1094
assoc_type, session_type = self.negotiator.getAllowedType()
1097
assoc = self._requestAssociation(
1098
endpoint, assoc_type, session_type)
1099
except ServerError, why:
1100
# Any error message whose code is not 'unsupported-type'
1101
# should be considered a total failure.
1102
if why.error_code != 'unsupported-type' or \
1103
why.message.isOpenID1():
1105
'Server error when requesting an association from %r: %s'
1106
% (endpoint.server_url, why.error_text))
1109
# The server didn't like the association/session type
1110
# that we sent, and it sent us back a message that
1111
# might tell us how to handle it.
1113
'Unsupported association type %s: %s' % (assoc_type,
1116
# Extract the session_type and assoc_type from the
1118
assoc_type = why.message.getArg(OPENID_NS, 'assoc_type')
1119
session_type = why.message.getArg(OPENID_NS, 'session_type')
1121
if assoc_type is None or session_type is None:
1122
oidutil.log('Server responded with unsupported association '
1123
'session but did not supply a fallback.')
1125
elif not self.negotiator.isAllowed(assoc_type, session_type):
1126
fmt = ('Server sent unsupported session/association type: '
1127
'session_type=%s, assoc_type=%s')
1128
oidutil.log(fmt % (session_type, assoc_type))
627
assoc = self._parseAssociation(
628
response, assoc_session, server_url)
632
def _createAssociateRequest(self, server_url):
633
proto = urlparse(server_url)[0]
635
session_type = PlainTextConsumerSession
637
session_type = DiffieHellmanConsumerSession
639
assoc_session = session_type()
1131
# Attempt to create an association from the assoc_type
1132
# and session_type that the server told us it
1135
assoc = self._requestAssociation(
1136
endpoint, assoc_type, session_type)
1137
except ServerError, why:
1138
# Do not keep trying, since it rejected the
1139
# association type that it told us to use.
1140
oidutil.log('Server %s refused its suggested association '
1141
'type: session_type=%s, assoc_type=%s'
1142
% (endpoint.server_url, session_type,
1150
def _requestAssociation(self, endpoint, assoc_type, session_type):
1151
"""Make and process one association request to this endpoint's
1154
@returns: An association object or None if the association
1157
@raises ServerError: when the remote OpenID server returns an error.
1159
assoc_session, args = self._createAssociateRequest(
1160
endpoint, assoc_type, session_type)
1163
response = self._makeKVPost(args, endpoint.server_url)
1164
except fetchers.HTTPFetchingError, why:
1165
oidutil.log('openid.associate request failed: %s' % (why[0],))
1169
assoc = self._extractAssociation(response, assoc_session)
1170
except KeyError, why:
1171
oidutil.log('Missing required parameter in response from %s: %s'
1172
% (endpoint.server_url, why[0]))
1174
except ProtocolError, why:
1175
oidutil.log('Protocol error parsing response from %s: %s' % (
1176
endpoint.server_url, why[0]))
1181
def _createAssociateRequest(self, endpoint, assoc_type, session_type):
1182
"""Create an association request for the given assoc_type and
1185
@param endpoint: The endpoint whose server_url will be
1186
queried. The important bit about the endpoint is whether
1187
it's in compatiblity mode (OpenID 1.1)
1189
@param assoc_type: The association type that the request
1191
@type assoc_type: str
1193
@param session_type: The session type that should be used in
1194
the association request. The session_type is used to
1195
create an association session object, and that session
1196
object is asked for any additional fields that it needs to
1198
@type session_type: str
1200
@returns: a pair of the association session object and the
1201
request message that will be sent to the server.
1202
@rtype: (association session type (depends on session_type),
1203
openid.message.Message)
1205
session_type_class = self.session_types[session_type]
1206
assoc_session = session_type_class()
642
'openid.mode': 'associate',
643
'openid.assoc_type':'HMAC-SHA1',
1209
'mode': 'associate',
1210
'assoc_type': assoc_type,
646
if assoc_session.session_type is not None:
647
args['openid.session_type'] = assoc_session.session_type
1213
if not endpoint.compatibilityMode():
1214
args['ns'] = OPENID2_NS
1216
# Leave out the session type if we're in compatibility mode
1217
# *and* it's no-encryption.
1218
if (not endpoint.compatibilityMode() or
1219
assoc_session.session_type != 'no-encryption'):
1220
args['session_type'] = assoc_session.session_type
649
1222
args.update(assoc_session.getRequest())
650
return assoc_session, args
652
def _parseAssociation(self, results, assoc_session, server_url):
654
assoc_type = results['assoc_type']
655
assoc_handle = results['assoc_handle']
656
expires_in_str = results['expires_in']
658
fmt = 'Getting association: missing key in response from %s: %s'
659
oidutil.log(fmt % (server_url, e[0]))
662
if assoc_type != 'HMAC-SHA1':
663
fmt = 'Unsupported assoc_type returned from server %s: %s'
664
oidutil.log(fmt % (server_url, assoc_type))
1223
message = Message.fromOpenIDArgs(args)
1224
return assoc_session, message
1226
def _getOpenID1SessionType(self, assoc_response):
1227
"""Given an association response message, extract the OpenID
1230
This function mostly takes care of the 'no-encryption' default
1231
behavior in OpenID 1.
1233
If the association type is plain-text, this function will
1234
return 'no-encryption'
1236
@returns: The association type for this message
1239
@raises KeyError: when the session_type field is absent.
1241
# If it's an OpenID 1 message, allow session_type to default
1242
# to None (which signifies "no-encryption")
1243
session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
1245
# Handle the differences between no-encryption association
1246
# respones in OpenID 1 and 2:
1248
# no-encryption is not really a valid session type for
1249
# OpenID 1, but we'll accept it anyway, while issuing a
1251
if session_type == 'no-encryption':
1252
oidutil.log('WARNING: OpenID server sent "no-encryption"'
1255
# Missing or empty session type is the way to flag a
1256
# 'no-encryption' response. Change the session type to
1257
# 'no-encryption' so that it can be handled in the same
1258
# way as OpenID 2 'no-encryption' respones.
1259
elif session_type == '' or session_type is None:
1260
session_type = 'no-encryption'
1264
def _extractAssociation(self, assoc_response, assoc_session):
1265
"""Attempt to extract an association from the response, given
1266
the association response message and the established
1267
association session.
1269
@param assoc_response: The association response message from
1271
@type assoc_response: openid.message.Message
1273
@param assoc_session: The association session object that was
1274
used when making the request
1275
@type assoc_session: depends on the session type of the request
1277
@raises ProtocolError: when data is malformed
1278
@raises KeyError: when a field is missing
1280
@rtype: openid.association.Association
1282
# Extract the common fields from the response, raising an
1283
# exception if they are not found
1284
assoc_type = assoc_response.getArg(
1285
OPENID_NS, 'assoc_type', no_default)
1286
assoc_handle = assoc_response.getArg(
1287
OPENID_NS, 'assoc_handle', no_default)
1289
# expires_in is a base-10 string. The Python parsing will
1290
# accept literals that have whitespace around them and will
1291
# accept negative values. Neither of these are really in-spec,
1292
# but we think it's OK to accept them.
1293
expires_in_str = assoc_response.getArg(
1294
OPENID_NS, 'expires_in', no_default)
668
1296
expires_in = int(expires_in_str)
669
except ValueError, e:
670
fmt = 'Getting Association: invalid expires_in field: %s'
671
oidutil.log(fmt % (e[0],))
674
session_type = results.get('session_type')
675
if session_type != assoc_session.session_type:
676
if session_type is None:
677
oidutil.log('Falling back to plain text association '
678
'session from %s' % assoc_session.session_type)
1297
except ValueError, why:
1298
raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
1300
# OpenID 1 has funny association session behaviour.
1301
if assoc_response.isOpenID1():
1302
session_type = self._getOpenID1SessionType(assoc_response)
1304
session_type = assoc_response.getArg(
1305
OPENID2_NS, 'session_type', no_default)
1307
# Session type mismatch
1308
if assoc_session.session_type != session_type:
1309
if (assoc_response.isOpenID1() and
1310
session_type == 'no-encryption'):
1311
# In OpenID 1, any association request can result in a
1312
# 'no-encryption' association response. Setting
1313
# assoc_session to a new no-encryption session should
1314
# make the rest of this function work properly for
679
1316
assoc_session = PlainTextConsumerSession()
681
oidutil.log('Session type mismatch. Expected %r, got %r' %
682
(assoc_session.session_type, session_type))
1318
# Any other mismatch, regardless of protocol version
1319
# results in the failure of the association session
1321
fmt = 'Session type mismatch. Expected %r, got %r'
1322
message = fmt % (assoc_session.session_type, session_type)
1323
raise ProtocolError(message)
1325
# Make sure assoc_type is valid for session_type
1326
if assoc_type not in assoc_session.allowed_assoc_types:
1327
fmt = 'Unsupported assoc_type for session %s returned: %s'
1328
raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
1330
# Delegate to the association session to extract the secret
1331
# from the response, however is appropriate for that session
686
secret = assoc_session.extractSecret(results)
1334
secret = assoc_session.extractSecret(assoc_response)
687
1335
except ValueError, why:
688
oidutil.log('Malformed response for %s session: %s' % (
689
assoc_session.session_type, why[0]))
691
except KeyError, why:
692
fmt = 'Getting association: missing key in response from %s: %s'
693
oidutil.log(fmt % (server_url, why[0]))
1336
fmt = 'Malformed response for %s session: %s'
1337
raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
696
assoc = Association.fromExpiresIn(
1339
return Association.fromExpiresIn(
697
1340
expires_in, assoc_handle, secret, assoc_type)
698
self.store.storeAssociation(server_url, assoc)
702
1342
class AuthRequest(object):
1343
"""An object that holds the state necessary for generating an
1344
OpenID authentication request. This object holds the association
1345
with the server and the discovered information with which the
1346
request will be made.
1348
It is separate from the consumer because you may wish to add
1349
things to the request before sending it on its way to the
1350
server. It also has serialization options that let you encode the
1351
authentication request as a URL or as a form POST.
703
1354
def __init__(self, endpoint, assoc):
705
1356
Creates a new AuthRequest object. This just stores each