Commits

Anonymous committed ea0a2f5 Merge

Comments (0)

Files changed (2)

openid2rp/__init__.py

     else:
         return url+"?"+urllib.urlencode(data)
 
+# 11.4.2 Verifying Directly with the OpenID Provider
+def verify_signature_directly(op_endpoint, response):
+    '''Request that the OP verify the signature via Direct Verification'''
+
+    request = [('openid.mode', 'check_authentication')]
+    # Exact copies of all fields from the authentication response, except for
+    # "openid.mode"
+    request.extend((k, v) for k, (v,) in response.items() if 'openid.mode' != k)
+    res = urllib.urlopen(op_endpoint, urllib.urlencode(request))
+    if 200 != res.getcode():
+        raise ValueError('OP refuses connection with status %d' % res.getcode())
+    response = parse_response(res.read())
+    if 'true' != response['is_valid']:
+        raise ValueError('OP doesn\'t assert that the signature of the verification request is valid')
+
 class NotAuthenticated(Exception):
     pass
 

openid2rp/testapp.py

 #!/usr/bin/env python
 ################ Test Server #################################
-import BaseHTTPServer, cgi, socket
+import BaseHTTPServer, cgi, Cookie, socket
 from openid2rp import *
 
 # supported providers
     ('Launchpad', 'https://login.launchpad.net/favicon.ico', 'https://login.launchpad.net/')
     )
              
-sessions = []
+# Mapping from Claimed Identifier to OP Endpoint URL.  Always updated on
+# initiation (end user enters User-Supplied Identifier) and used to avoid
+# repeating discovery when verifying assertions.  When verifying assertions, OP
+# Endpoint URL is used to get an association if one is stored, and to perform
+# Direct Verification otherwise
+disco = {}
+
+# Mapping from OP Endpoint URL and association handle to association response
+sessions = {}
+
 class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
 
     def write(self, payload, type):
                     session = associate(services, url)
                 except ValueError, e:
                     return self.error(str(e))
-                sessions.append(session)
+                sessions[url, session['assoc_handle']] = session
                 self.send_response(307) # temporary redirect - do not cache
                 self.send_header("Location", request_authentication
                                  (services, url, session['assoc_handle'],
                 if res is None:
                     return self.error('Discovery failed')
                 services, url, op_local = res
+
+                # Avoid repeating discovery when verifying assertions
+                disco[claimed] = url
+
                 try:
                     session = associate(services, url)
                 except ValueError, e:
                     return self.error(str(e))
-                sessions.append(session)
+                sessions[url, session['assoc_handle']] = session
                 self.send_response(307)
                 self.send_header("Location", request_authentication
                                  (services, url, session['assoc_handle'],
                                   self.base_url+"?returned=1",
                                   claimed, op_local))
+
+                # 1.1 compatibility: openid.claimed_id" is not defined by
+                # OpenID Authentication 1.1.  RPs MAY send the value when
+                # making requests, but MUST NOT depend on the value being
+                # present in authentication responses.  When the OP-Local
+                # Identifier ("openid.identity") is different from the Claimed
+                # Identifier, the RP MUST keep track of what Claimed Identifier
+                # was used to discover the OP-Local Identifier, for example by
+                # keeping it in session state.  Although the Claimed Identifier
+                # will not be present in the response, it MUST be used as the
+                # identifier for the user
+                self.send_header('Set-Cookie', 'openid.claimed_id='+claimed)
+
                 self.end_headers()
                 return                
             if 'returned' in query:
                     return self.rp_discovery()
                 if query['openid.mode'][0] == 'cancel':
                     return self.write('Login failed', 'text/plain')
+
+                try:
+                    claimed_id, = query['openid.claimed_id']
+                except KeyError:
+                    no_fragment = claimed_id = Cookie.SimpleCookie(self.headers['Cookie'])['openid.claimed_id'].value
+                else:
+
+                    # If the Claimed Identifier in the assertion is a URL and
+                    # contains a fragment, the fragment part and the fragment
+                    # delimiter character "#" MUST NOT be used for the purposes
+                    # of verifying the discovered information
+                    try:
+                        no_fragment = claimed_id[:claimed_id.index('#')]
+                    except ValueError:
+                        no_fragment = claimed_id
+
+                # If the Claimed Identifier is included in the assertion, it
+                # MUST have been discovered by the RP and the information in
+                # the assertion MUST be present in the discovered information.
+                # The Claimed Identifier MUST NOT be an OP Identifier
+                #
+                # If the Claimed Identifier was not previously discovered by
+                # the RP (the "openid.identity" in the request was
+                # "http://specs.openid.net/auth/2.0/identifier_select" or a
+                # different Identifier, or if the OP is sending an unsolicited
+                # positive assertion), the RP MUST perform discovery on the
+                # Claimed Identifier in the response to make sure that the OP
+                # is authorized to make assertions about the Claimed Identifier
+                try:
+                    op_endpoint = disco[no_fragment]
+                except KeyError:
+                    _, op_endpoint, _ = discover(no_fragment)
+
+                # If the RP has stored an association with the association
+                # handle specified in the assertion, it MUST check the
+                # signature on the assertion itself.  If it does not have an
+                # association stored, it MUST request that the OP verify the
+                # signature via Direct Verification
                 handle = query['openid.assoc_handle'][0]
-                for session in sessions:
-                    if session['assoc_handle'] == handle:
-                        break
+                try:
+                    session = sessions[op_endpoint, handle]
+                except KeyError:
+                    try:
+                        verify_signature_directly(op_endpoint, query)
+                    except Exception, e:
+                        return self.error('Authentication failed: '+repr(e))
+                    signed, = query['openid.signed']
+                    signed = signed.split(',')
                 else:
-                    session = None
-                if not session:
-                    return self.error('Not authenticated (no session)')
-                try:
-                    signed = authenticate(session, querystring)
-                except Exception, e:
-                    self.error("Authentication failed: "+repr(e))
-                    return
-                if 'openid.claimed_id' in query:
-                    if 'claimed_id' not in signed:
-                        return self.error('Incomplete signature')
-                    claimed = query['openid.claimed_id'][0]
-                else:
-                    # OpenID 1, claimed ID not reported - should set cookie
-                    if 'identity' not in signed:
-                        return self.error('Incomplete signature')
-                    claimed = query['openid.identity'][0]
-                payload = "Hello "+claimed+"\n"
+                    try:
+                        signed = authenticate(session, querystring)
+                    except Exception, e:
+                        return self.error('Authentication failed: '+repr(e))
+
+                payload = "Hello "+claimed_id+"\n"
                 ax = get_ax(querystring, get_namespaces(querystring), signed)
                 sreg = get_sreg(querystring, signed)
                 email = get_email(querystring)