Commits

Martin von Löwis  committed cad7248

Redo verification procedure, in openid2rp.verify.

  • Participants
  • Parent commits f7ce79e

Comments (0)

Files changed (2)

File openid2rp/__init__.py

     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())
+        raise NotAuthenticated('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')
+        raise NotAuthenticated('OP doesn\'t assert that the signature of the verification request is valid')
 
 class NotAuthenticated(Exception):
     pass
 
     return signed
 
+def verify(response, discovery_cache, find_association, nonce_seen):
+    response = _prepare_response(response)
+    mode = response['openid.mode'][0]
+    if mode == 'cancel':
+        raise NotAuthenticated('Login cancelled')
+    if mode != 'id_res':
+        raise NotAuthenticated('Unexpected mode ' + mode)
+    # Establish claimed ID
+    if 'openid.claimed_id' in response:
+        claimed_id = response['openid.claimed_id'][0]
+        # 11.2. Drop Fragment from claimed_id
+        fragment = claimed_id.find('#')
+        if fragment != -1:
+            claimed_id = claimed_id[:fragment]
+    elif 'openid1' in response:
+        claimed_id = response['openid1'][0]
+    else:
+        raise NotAuthenticated('Cannot determine claimed ID')
+    discovered = discovery_cache(claimed_id)
+    if not discovered:
+        discovered = discover(claimed_id)
+        if not discovered:
+            raise NotAuthenticated('Claimed ID %s cannot be rediscovered' % claimed_id)
+    services, op_endpoint, op_local = discovered
+    # For a provider-allocated claimed_id, there will be no op_local ID,
+    # and there is no point checking it.
+    if op_local and op_local != response['openid.identity'][0]:
+        raise NotAuthenticated('Discovered and asserted local identifiers differ')
+    # For OpenID 1.1, op_endpoint may not be included in the response
+    if ('openid.op_endpoint' in response and
+        op_endpoint != response['openid.op_endpoint'][0]):
+        raise NotAuthenticated('Discovered and asserted endpoints differ')
+    # XXX verify protocol version, verify claimed_id wrt. original request,
+    # verify return_to URL
+    
+    # verify the signature
+    assoc_handle = response['openid.assoc_handle'][0]
+    session = find_association(assoc_handle)
+    if session:
+        signed = authenticate(session, response)
+    else:
+        verify_signature_directly(op_endpoint, response)
+        signed = response['openid.signed'][0].split(',')
+
+    # Check the nonce. OpenID 1.1 doesn't have them
+    if 'openid.response_nonce' in response:
+        nonce = response['openid.response_nonce'][0]
+        timestamp = parse_nonce(nonce)
+        if (datetime.datetime.utcnow() - timestamp).total_seconds() > 10:
+            # allow for at most 10s transmission time and time shift
+            raise NotAuthenticated('Replay attack detected')
+        if nonce_seen(nonce):
+            raise NotAuthenticated('Replay attack deteced')
+    return signed, claimed_id
+
 def parse_nonce(nonce):
     '''Extract a datetime.datetime stamp from the nonce'''
     stamp = nonce.split('Z', 1)[0]

File openid2rp/testapp.py

     ('Launchpad', 'https://login.launchpad.net/favicon.ico', 'https://login.launchpad.net/')
     )
              
-# Mapping from Claimed Identifier to OP Endpoint URL and OP-Local Identifier.
-# 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.  OP-Local Identifier is used to
-# prevent a malicious user from impersonating Claimed Identifiers that the OP
-# is authorized to make assertions about, but that the user doesn't control
-disco = {}
+
+# Cache discovered information, for later validation
+discovered = {}
 
 # Mapping from OP Endpoint URL to association responses;
 # most recent association is last
 sessions = collections.defaultdict(list)
+# Associations by assoc_handle
+associations = {}
 
 class _Expired(Exception):
     'Local exception class to indicate expired sessions.'
     pass
 
+nonces = set()
+def nonce_seen(nonce):
+    if nonce in nonces:
+        return True
+    nonces.add(nonce)
+    return False
+
 class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
 
     def write(self, payload, type):
                 prov = [p for p in providers if p[0]  == query['provider'][0]]
                 if len(prov) != 1:
                     return self.not_found()
-                prov = prov[0]
-                services, url, op_local = discover(prov[2])
+                prov = prov[0][2]
+                services, url, op_local = discovered[prov] = discover(prov)
 
                 # Get most recent association.  Establish association if it
                 # expired, or if none exist
                     except ValueError, e:
                         return self.error(str(e))
                     sessions[url].append((session, now+int(session['expires_in'])))
+                    associations[session['assoc_handle']] = session
 
                 self.send_response(307) # temporary redirect - do not cache
                 self.send_header("Location", request_authentication
                     res = discover(claimed)
                 if res is None:
                     return self.error('Discovery failed')
+                discovered[claimed] = res
                 services, url, op_local = res
 
-                # Avoid repeating discovery when verifying assertions
-                disco[claimed] = url, op_local or claimed
-
                 # Get most recent association.  Establish association if it
                 # expired, or if none exist
                 now = time()
                     except ValueError, e:
                         return self.error(str(e))
                     sessions[url].append((session, now+int(session['expires_in'])))
+                    associations[session['assoc_handle']] = session
 
                 self.send_response(307)
                 self.send_header("Location", request_authentication
             if 'returned' in query:
                 if 'openid.mode' not 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:
-                    # The library put in the openid1 field. 
-                    # XXX need to introduce library function to extract claimed ID
-                    claimed_id, = query['openid1']
-                # 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:
-                    disco_endpoint, disco_identity = disco[no_fragment]
-                except KeyError:
-                    _, disco_endpoint, disco_identity = discover(no_fragment)
-                    if not disco_identity:
-                        disco_identity = no_fragment
-
-                # Prevent a malicious user who controls an OP-Local Identifier
-                # from impersonating Claimed Identifiers that the OP is
-                # authorized to make assertions about, but that the user
-                # doesn't control.  A user who controls an OP-Local Identifier
-                # can obtain assertions from the OP about Claimed Identifiers
-                # that the user doesn't control
-                identity, = query['openid.identity']
-                if disco_identity != identity:
-                    return self.error('OP-Local Identifier in the assertion not present in the discovered information')
-
-                # 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
-                try:
-                    session = sessions[disco_endpoint]
-                except KeyError:
-                    try:
-                        verify_signature_directly(disco_endpoint, query)
-                    except Exception, e:
-                        return self.error('Verifying signature directly failed: '+repr(e))
+                    signed, claimed_id = verify(query, discovered.get,
+                                                associations.get, nonce_seen)
+                except NotAuthenticated, e:
+                    return self.write('Login failed: %s' % e, 'text/plain')
                     signed, = query['openid.signed']
                     signed = signed.split(',')
-                else:
-                    assoc_handle, = query['openid.assoc_handle']
-                    for session, _ in session:
-                        if assoc_handle == session['assoc_handle']:
-                            try:
-                                signed = authenticate(session, querystring)
-                            except Exception, e:
-                                return self.error('Verifying signature with an association failed: '+repr(e))
-                            break
-                    else:
-                        try:
-                            verify_signature_directly(disco_endpoint, query)
-                        except Exception, e:
-                            return self.error('Verifying signature directly failed: '+repr(e))
-                        signed, = query['openid.signed']
-                        signed = signed.split(',')
-
                 payload = "Hello "+claimed_id+"\n"
                 ax = get_ax(querystring, get_namespaces(querystring), signed)
                 sreg = get_sreg(querystring, signed)