Commits

Martin von Löwis committed 6c98c47

Implement XRI resolution.

Comments (0)

Files changed (3)

 
 .. function:: normalize_uri(uri) -> kind, url
 
-  Returns either 'xri' or 'uri' as kind; XRIs are not further
-  supported. Applications should always normalize URIs claimed by the
-  end user, and perform identity comparisons on the normalized URIs
-  only.
+  Returns either 'xri' or 'uri' as kind. Applications should always
+  normalize URIs claimed by the end user, and perform identity
+  comparisons on the normalized URIs only.
 
 .. function:: discover(url) -> (services, op_endpoint, op_local)
 
   and only identify the user by that string; the op_identifier is then
   not further relevant to the application.
 
+.. function:: resolve_xri(xri, proxy='xri.net') -> (canonical_id, services, op_endpoint, op_local)
+
+  Perform OpenID discovery for XRI identifiers, i.e. XRI resolution.
+  This has the same result as discover, but also returns the canonical
+  identifier of the user, which is the one that must be used to identify
+  an account. Users may desire that the original XRI identifier is also
+  displayed. Optionally, a XRI proxy resolver may be specified; the
+  proxy resolution uses HTTP.
+
 .. function:: associate(services, url) -> dict
 
   Setup an association between the service and the provider. services

openid2rp/__init__.py

         if attrs.get('http-equiv','').lower() == 'x-xrds-location':
             self.xrds_location = attrs['content']
 
+def _extract_services(doc):
+    for svc in doc.findall(".//{xri://$xrd*($v*2.0)}Service"):
+        services = [x.text for x in svc.findall("{xri://$xrd*($v*2.0)}Type")]
+        if 'http://specs.openid.net/auth/2.0/server' in services:
+            # 7.3.2.1.1 OP Identifier Element
+            uri = svc.find("{xri://$xrd*($v*2.0)}URI")
+            if uri is not None:
+                op_local = None
+                op_endpoint = uri.text
+                break
+        elif 'http://specs.openid.net/auth/2.0/signon' in services:
+            # 7.3.2.1.2.  Claimed Identifier Element
+            op_local = svc.find("{xri://$xrd*($v*2.0)}LocalID")
+            if op_local is not None:
+                op_local = op_local.text
+            uri = svc.find("{xri://$xrd*($v*2.0)}URI")
+            if uri is not None:
+                op_endpoint = uri.text
+                break
+        elif 'http://openid.net/server/1.0' in services or \
+                'http://openid.net/server/1.1' in services or \
+                'http://openid.net/signon/1.0' in services or \
+                'http://openid.net/signon/1.1' in services:
+            # 14.2.1 says we also need to check for the 1.x types;
+            # XXX should check 1.x only if no 2.0 service is found
+            op_local = svc.find("{http://openid.net/xmlns/1.0}Delegate")
+            if op_local is not None:
+                op_local = op_local.text
+            uri = svc.find("{xri://$xrd*($v*2.0)}URI")
+            if uri is not None:
+                op_endpoint = uri.text
+                break
+    else:
+        return None # No OpenID 2.0 service found
+    return services, op_endpoint, op_local
+
 def discover(url):
     '''Perform service discovery on the OP URL.
     Return list of service types, and the auth/2.0 URL,
     elif content_type == 'application/xrds+xml':
         # Yadis 6.2.5 option 4
         doc = ElementTree.fromstring(data)
-        for svc in doc.findall(".//{xri://$xrd*($v*2.0)}Service"):
-            services = [x.text for x in svc.findall("{xri://$xrd*($v*2.0)}Type")]
-            if 'http://specs.openid.net/auth/2.0/server' in services:
-                # 7.3.2.1.1 OP Identifier Element
-                uri = svc.find("{xri://$xrd*($v*2.0)}URI")
-                if uri is not None:
-                    op_local = None
-                    op_endpoint = uri.text
-                    break
-            elif 'http://specs.openid.net/auth/2.0/signon' in services:
-                # 7.3.2.1.2.  Claimed Identifier Element
-                op_local = svc.find("{xri://$xrd*($v*2.0)}LocalID")
-                if op_local is not None:
-                    op_local = op_local.text
-                uri = svc.find("{xri://$xrd*($v*2.0)}URI")
-                if uri is not None:
-                    op_endpoint = uri.text
-                    break
-            elif 'http://openid.net/server/1.0' in services or \
-                 'http://openid.net/server/1.1' in services or \
-                 'http://openid.net/signon/1.0' in services or \
-                 'http://openid.net/signon/1.1' in services:
-                # 14.2.1 says we also need to check for the 1.x types;
-                # XXX should check 1.x only if no 2.0 service is found
-                op_local = svc.find("{http://openid.net/xmlns/1.0}Delegate")
-                if op_local is not None:
-                    op_local = op_local.text
-                uri = svc.find("{xri://$xrd*($v*2.0)}URI")
-                if uri is not None:
-                    op_endpoint = uri.text
-                    break
-        else:
-            return None # No OpenID 2.0 service found
+        return _extract_services(doc)
     else:
         # unknown content type
         return None
     return services, op_endpoint, op_local
 
+def resolve_xri(xri, proxy='xri.net'):
+    '''Perform XRI resolution of xri using a proxy resolver.
+    Return canonical ID, services, op endpoint, op local;
+    return None if an error occurred'''
+    xri = urllib.quote(xri, safe='=@*!+()')
+    conn = httplib.HTTPConnection(proxy)
+    try:
+        conn.connect()
+    except:
+        # DNS or TCP error
+        return None
+    conn.putrequest("GET", '/'+xri+'?_xrd_r=application/xrds+xml')
+    conn.endheaders()
+
+    res = conn.getresponse()
+    data = res.read()
+    conn.close()
+
+    doc = ElementTree.fromstring(data)
+    res = _extract_services(doc)
+    if res is None:
+        # No OpenID service found
+        return None
+    services, op_endpoint, op_local = res
+    canonical_id = doc.find(".//{xri://$xrd*($v*2.0)}CanonicalID")
+    if canonical_id is None:
+        return None
+    return canonical_id.text, services, op_endpoint, op_local
+
 def is_compat_1x(services):
     for uri in ('http://specs.openid.net/auth/2.0/signon',
                 'http://specs.openid.net/auth/2.0/server'):

openid2rp/testapp.py

             if 'claimed' in query:
                 kind, claimed = normalize_uri(query['claimed'][0])
                 if kind == 'xri':
-                    return self.error('XRI resolution not supported')
-                res = discover(claimed)
+                    res = resolve_xri(claimed)
+                    if res:
+                        # A.5: XRI resolution requires to use canonical ID
+                        # Original claimed ID may be preserved for display
+                        # purposes
+                        claimed = res[0]
+                        res = res[1:]
+                else:
+                    res = discover(claimed)
                 if res is None:
                     return self.error('Discovery failed')
                 services, url, op_local = res