Commits

Martin von Löwis  committed bd5e80c Merge

Merge to restore __init__.py history.

  • Participants
  • Parent commits ab4415f, 8c759ba

Comments (0)

Files changed (10)

-include MANIFEST.in testapp.py
-recursive-include doc *
+include MANIFEST.in
+recursive-include doc *
 Changes
 -------
 
+1.5 (2010-07-27):
+
+* Make openid2rp a package, move testapp into that package.
+* Adding Django authentication backend (openid2rp.django)
+
 1.4 (2010-07-20):
+
 * Fix AX requests.
 
 1.3 (2010-07-16):

File doc/django.rst

+Django Authentication Module
+============================
+
+.. module:: openid2rp
+
+A small Django authentication backend for OpenID, based on the openid2rp package.
+It is automatically installed together with openid2rp. 
+
+In order to get the Django database magic right, you need to add 'openid2rp.django' to your 
+INSTALLED_APPS list in setup.py. You also need to add 'openid2rp.django.auth.Backend' to the
+list of authentication backends. Example::
+
+  INSTALLED_APPS = (
+	'django.contrib.auth',
+	'django.contrib.contenttypes',
+	'django.contrib.sessions',
+	'django.contrib.sites',
+	'django.contrib.admin',
+	'openid2rp.django',
+	'<yourapp>.front'
+  )
+
+  AUTHENTICATION_BACKENDS = (
+	'django.contrib.auth.backends.ModelBackend',
+	'openid2rp.django.auth.Backend'
+  )
+
+The database is extended with one table for the	 OpenID identifier storage. 
+Therefore, make sure that you call "python manage.py syncdb" ones 
+after installing this package.
+
+In contrast to most other Django OpenID authentication packages, this one
+does not try to cover any view aspects. It also keeps the nature of openid2rp
+by assuming that you know how OpenID works. 
+
+Since the Django authentication framework is not prepared for a multi-step auth scenario with several 
+inputs and outputs, you need to call a preparation function ("preAuthenticate") from the module
+before you can use the Django authenticate() method. make sure that you use the correct keyword
+arguments in the authenticate() call.
+
+Session storage is based on a module-scope variable. I was to lazy to decode the openid2rp session dict
+for the database storing and lifetime checking. There is also no Nonce checking so far.
+
+The explicit modeling of each exceptional case hopefully allows you to realize an according 
+reaction in your view rendering. 

File doc/index.rst

 the OpenID provider, as well as produce redirects to be sent to the
 user's browser, and process incoming redirects from the provider.
 
-The openid2rp module also includes a stand-alone server, as an example
-and a test.
+The openid2rp package also includes a stand-alone server, as an example
+and a test; run this as ``python -m openid2rp.testapp``.
+
+.. toctree::
+
+   django
 
 Terminology
 -----------

File openid2rp/django/__init__.py

Empty file added.

File openid2rp/django/auth.py

+""" 
+Written by Peter Troeger <peter@troeger.eu>.
+
+A small Django authentication backend for OpenID, based on the openid2rp package.
+It is automatically installed together with openid2rp. 
+
+In order to get the Django database magic right, you need to add 'openid2rp.django' to your 
+INSTALLED_APPS list in setup.py. You also need to add 'openid2rp.django.auth.Backend' to the
+list of authentication backends. Example:
+
+INSTALLED_APPS = (
+	'django.contrib.auth',
+	'django.contrib.contenttypes',
+	'django.contrib.sessions',
+	'django.contrib.sites',
+	'django.contrib.admin',
+	'openid2rp.django',
+	'<yourapp>.front'
+)
+
+AUTHENTICATION_BACKENDS = (
+	'django.contrib.auth.backends.ModelBackend',
+	'openid2rp.django.auth.Backend'
+)
+
+The database is extended with one table for the	 OpenID identifier storage. 
+Therefore, make sure that you call "python manage.py syncdb" ones 
+after installing this package.
+
+In contrast to most other Django OpenID authentication packages, this one
+does not try to cover any view aspects. It also keeps the nature of openid2rp
+by assuming that you know how OpenID works. 
+
+Since the Django authentication framework is not prepared for a multi-step auth scenario with several 
+inputs and outputs, you need to call a preparation function ("preAuthenticate") from the module
+before you can use the Django authenticate() method. make sure that you use the correct keyword
+arguments in the authenticate() call.
+
+Session storage is based on a module-scope variable. I was to lazy to decode the openid2rp session dict
+for the database storing and lifetime checking. There is also no Nonce checking so far.
+
+The explicit modeling of each exceptional case hopefully allows you to realize an according 
+reaction in your view rendering. 
+
+"""
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from openid2rp.django.models import UserOpenID
+from django.http import HttpResponse
+from django.db.models import Q
+from django.contrib.auth.models import AnonymousUser
+import openid2rp
+
+AX = openid2rp.AX
+sessions=[]
+
+class IncorrectURI(Exception):
+	pass
+
+class IncorrectClaim(Exception):
+	pass
+	
+class MissingSession(Exception):
+	pass
+
+class AuthenticationError(Exception):
+	pass
+
+class IncompleteAnswer(Exception):
+	pass
+	
+class MultipleClaimUsage(Exception):
+	pass
+	
+def storeOpenID(user, openid):
+	claim=UserOpenID(user=user, uri=openid)
+	claim.save()
+
+def getOpenIDs(user):
+	return UserOpenID.objects.filter(user=user).values_list('uri', flat=True)
+
+def preAuthenticate(uri, answer_url, 
+					sreg = (('nickname', 'email'), ()),
+					ax = ((openid2rp.AX.email, openid2rp.AX.first, openid2rp.AX.last), ())):
+	"""
+	Initializes the OpenID authentication. 
+	The input are the OpenID URI the user wants to be authorized with. You get
+	that from your login screen. The answer_url is the one the provider should call when 
+	finished, and the sreg / ax parameters as with the original openid2rp.request_authentication() call. 
+	In the view for the answer_url, you will call then "authenticate".
+
+	The output are two values: response and claim.
+	The first value is the HttpResponse object with the neccessary redirect to the providers
+	login site. You just return that from your view.
+	The second value is the (normalized) claim URI from the original URI. 
+	You need that later in the authenticate call, so store it somewhere in the session.
+	
+	If something goes wrong, one of the following errors is raised: IncorrectURI, IncorrectClaim
+	"""
+	global sessions
+
+	try:
+		kind, claimedId = openid2rp.normalize_uri(uri)			
+	except Exception, e:
+		raise IncorrectURI(str(e))
+	res = openid2rp.discover(claimedId)
+	if res != None:
+		services, url, op_local = res
+		session = openid2rp.associate(services, url)
+		sessions.append(session)
+		redirect_url=openid2rp.request_authentication( services, url, session['assoc_handle'], answer_url, claimedId, op_local, sreg=sreg, ax=ax )
+		response=HttpResponse()
+		response['Location']=redirect_url
+		response.status_code=307
+		return response, claimedId
+	else:
+		raise IncorrectClaim()
+
+class Backend:	
+	def get_user(self, user_id):
+		try:
+			return User.objects.get(pk=user_id)
+		except User.DoesNotExist:
+			return None
+
+	def authenticate(self, **credentials):
+		"""
+		This finalizes the OpenID authentication. Input kwargs parameters:
+		- request: Django request object, which has all the GET parameters being given by the OpenID provider
+		- claim: Output from the preAuthenicate() call we asked you to store somewhere.
+		
+		The result of this call is either:
+		- A User object with additional attributes.
+		- A AnonymousUser object with additional attributes, in case the OpenID authentication was
+		  good, but no matching user could be found.
+		- One of the exceptions.
+
+		The additional attributes are:
+		- openid_email: The eMail address, or None.
+		- openid_claim: The real claimId string for this user. You might want to use that in the storeID() call.
+		- openid_sreg: A dictionary of received SREG values.
+		- openid_ax: A dictionary of received AX values.
+	
+		If you get an AnonymousUser object as result, you need to assign the returned claim string first to some
+		existing Django user. This backend will not create the according User object for you,
+		since this is application-specific. You can use the AX / SREG data to pre-fill some registration
+		form. If you somehow came to a Django user object for the returned claim string, use the storeID() call.
+		
+		Possible errors: MissingSession, AuthenticationError, IncompleteAnswer, MultipleClaimUsage
+		"""
+
+		global sessions
+
+		if not ("request" in credentials and "claim" in credentials):
+			raise TypeError
+		
+		request=credentials['request']
+		claimedId=credentials['claim']
+		
+		query=request.META['QUERY_STRING']
+		handle = request.GET['openid.assoc_handle']
+		for session in sessions:
+			if session['assoc_handle'] == handle:
+				break
+		else:
+			session=None
+		if not session:
+			raise MissingSession
+		try:
+			signed=openid2rp.authenticate(session, query)
+		except Exception, e:
+			raise AuthenticationError(str(e))
+		# provider-based auth returns claim id, OpenID not (if I got that right) - in this case we take the original value
+		if 'openid.claimed_id' in request.GET:
+			if 'claimed_id' not in signed:
+				raise IncompleteAnswer()
+			claimedId = request.GET['openid.claimed_id']
+		else:
+			if 'identity' not in signed:
+				raise IncompleteAnswer()
+		# look up OpenID claim string in local database
+		idrecord=UserOpenID.objects.filter(Q(uri=claimedId))
+		if len(idrecord)>1:
+			# more than one user has this claimID, which is definitly wrong
+			raise MultipleClaimUsage()
+		elif len(idrecord)<1:
+			# No user has this OpenID claim string assigned
+			user = AnonymousUser()
+		else:
+			user=idrecord[0].user
+		# inactive users are handled by the later login() method, so we can return them here too
+		user.openid_claim = claimedId
+		user.openid_ax = openid2rp.get_ax(query, openid2rp.get_namespaces(query), signed)
+		user.openid_sreg = openid2rp.get_sreg(query, signed)
+		user.openid_email = openid2rp.get_email(query)
+		return user
+		

File openid2rp/django/models.py

+from django.db import models
+from django.contrib.auth.models import User
+
+class UserOpenID(models.Model):
+    user = models.ForeignKey(User, related_name='openids')
+    uri = models.CharField(max_length=255, blank=False, null=False)
+    insert_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, editable=False)
+    last_modified = models.DateTimeField(null=False, blank=False, auto_now=True, editable=False)

File openid2rp/testapp.py

+#!/usr/bin/env python
+################ Test Server #################################
+import BaseHTTPServer, cgi
+from openid2rp import *
+
+# supported providers
+providers = (
+    ('Google', 'http://www.google.com/favicon.ico', 'https://www.google.com/accounts/o8/id'),
+    ('Yahoo', 'http://www.yahoo.com/favicon.ico', 'http://yahoo.com/'),
+    ('Verisign', 'http://pip.verisignlabs.com/favicon.ico', 'http://pip.verisignlabs.com'),
+    ('myOpenID', 'https://www.myopenid.com/favicon.ico', 'https://www.myopenid.com/'),
+    ('Launchpad', 'https://login.launchpad.net/favicon.ico', 'https://login.launchpad.net/')
+    )
+             
+sessions = []
+class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    def write(self, payload, type):
+        self.send_response(200)
+        self.send_header("Content-type", type)
+        self.send_header("Content-length", str(len(payload)))
+        self.end_headers()
+        self.wfile.write(payload)
+
+    def do_GET(self):
+        if self.path == '/':
+            return self.root()
+        path = self.path
+        i = path.rfind('?')
+        if i >= 0:
+            querystring = path[i+1:]
+            query = cgi.parse_qs(querystring)
+            path = path[:i]
+        else:
+            query = {}
+        if path == '/':
+            if 'provider' in query:
+                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])
+                try:
+                    session = associate(services, url)
+                except ValueError, e:
+                    return self.error(str(e))
+                sessions.append(session)
+                self.send_response(307) # temporary redirect - do not cache
+                self.send_header("Location", request_authentication
+                                 (services, url, session['assoc_handle'],
+                                  self.base_url+"?returned=1"))
+                self.end_headers()
+                return
+            if 'claimed' in query:
+                kind, claimed = normalize_uri(query['claimed'][0])
+                if kind == 'xri':
+                    return self.error('XRI resolution not supported')
+                res = discover(claimed)
+                if res is None:
+                    return self.error('Discovery failed')
+                services, url, op_local = res
+                try:
+                    session = associate(services, url)
+                except ValueError, e:
+                    return self.error(str(e))
+                sessions.append(session)
+                self.send_response(307)
+                self.send_header("Location", request_authentication
+                                 (services, url, session['assoc_handle'],
+                                  self.base_url+"?returned=1",
+                                  claimed, op_local))
+                self.end_headers()
+                return                
+            if 'returned' in query:
+                if 'openid.identity' not in query:
+                    return self.rp_discovery()
+                handle = query['openid.assoc_handle'][0]
+                for session in sessions:
+                    if session['assoc_handle'] == handle:
+                        break
+                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"
+                ax = get_ax(querystring, get_namespaces(querystring), signed)
+                sreg = get_sreg(querystring, signed)
+                email = get_email(querystring)
+                if email:
+                    payload += 'Your email is '+email+"\n"
+                else:
+                    payload += 'No email address is known\n'
+                if 'nickname' in sreg:
+                    username = sreg['nickname']
+                elif "http://axschema.org/namePerson/first" in ax:
+                    username = ax["http://axschema.org/namePerson/first"]
+                    if "http://axschema.org/namePerson/last" in ax:
+                        username += "." + ax["http://axschema.org/namePerson/last"]
+                else:
+                    username = None
+                if username:
+                    payload += 'Your nickname is '+username+'\n'
+                else:
+                    payload += 'No nickname is known\n'
+                if isinstance(payload, unicode):
+                    payload = payload.encode('utf-8')
+                return self.write(payload, "text/plain")
+                
+        return self.not_found()
+
+    
+
+    def debug(self, value):
+        payload = repr(value)
+        if isinstance(payload, unicode):
+            payload = payload.encode('utf-8')
+        self.write(payload, "text/plain")
+
+    def error(self, text):
+        if isinstance(text, unicode):
+            text = text.encode('utf-8')
+        self.write(text, "text/plain")
+
+    def root(self):
+        payload = u"<html><head><title>OpenID login</title></head><body>\n"
+        
+        for name, icon, provider in providers:
+            payload += u"<p><a href='%s?provider=%s'><img src='%s' alt='%s'></a></p>\n" % (
+                self.base_url, name, icon, name)
+        payload += u"<form>Type your OpenID:<input name='claimed'/><input type='submit'/></form>\n"
+        payload += u"</body></html>"
+        self.write(payload.encode('utf-8'), "text/html")
+
+    def rp_discovery(self):
+        payload = '''<xrds:XRDS  
+                xmlns:xrds="xri://$xrds"  
+                xmlns="xri://$xrd*($v*2.0)">  
+                <XRD>  
+                     <Service priority="1">  
+                              <Type>http://specs.openid.net/auth/2.0/return_to</Type>  
+                              <URI>%s</URI>  
+                     </Service>  
+                </XRD>  
+                </xrds:XRDS>
+        ''' % (self.base_url+"/?returned=1")
+        self.write(payload, 'application/xrds+xml')
+
+    def not_found(self):
+        self.send_response(404)
+        self.end_headers()
+        
+# OpenID providers often attempt relying-party discovery
+# This requires the test server to use a globally valid URL
+# If Python cannot correctly determine the base URL, you
+# can pass it as command line argument
+def test_server():
+    import socket, sys
+    if len(sys.argv) > 1:
+        base_url = sys.argv[1]
+    else:
+        base_url = "http://" + socket.getfqdn() + ":8000/"
+    print "Listening on", base_url
+    Handler.base_url = base_url
+    #BaseHTTPServer.HTTPServer.address_family = socket.AF_INET6
+    httpd = BaseHTTPServer.HTTPServer(('', 8000), Handler)
+    httpd.serve_forever()
+
+if __name__ == '__main__':
+    test_server()
 except ImportError:
     pass
 
-version='1.4'
+version='1.5'
 setup(name='openid2rp',
       version=version,
       description='OpenID 2.0 Relying Party Support Library',
       author='Martin v. Loewis',
       author_email='martin@v.loewis.de',
       long_description=open('README').read(),
-      homepage='http://pypi.python.org/pypi/openid2rp',
+      url='http://pypi.python.org/pypi/openid2rp',
       download_url='http://pypi.python.org/packages/source/o/openid2rp/openid2rp-%s.tar.gz' % version,
       classifiers = [
         "Development Status :: 5 - Production/Stable",
         "Programming Language :: Python :: 3.1",
         "Programming Language :: Python :: 3.2",
         ],
-      py_modules=['openid2rp'],
+      packages=['openid2rp','openid2rp.django'],
       cmdclass = cmdclass
       )

File testapp.py

-#!/usr/bin/env python
-################ Test Server #################################
-import BaseHTTPServer, cgi
-from openid2rp import *
-
-# supported providers
-providers = (
-    ('Google', 'http://www.google.com/favicon.ico', 'https://www.google.com/accounts/o8/id'),
-    ('Yahoo', 'http://www.yahoo.com/favicon.ico', 'http://yahoo.com/'),
-    ('Verisign', 'http://pip.verisignlabs.com/favicon.ico', 'http://pip.verisignlabs.com'),
-    ('myOpenID', 'https://www.myopenid.com/favicon.ico', 'https://www.myopenid.com/'),
-    ('Launchpad', 'https://login.launchpad.net/favicon.ico', 'https://login.launchpad.net/')
-    )
-             
-sessions = []
-class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
-
-    def write(self, payload, type):
-        self.send_response(200)
-        self.send_header("Content-type", type)
-        self.send_header("Content-length", str(len(payload)))
-        self.end_headers()
-        self.wfile.write(payload)
-
-    def do_GET(self):
-        if self.path == '/':
-            return self.root()
-        path = self.path
-        i = path.rfind('?')
-        if i >= 0:
-            querystring = path[i+1:]
-            query = cgi.parse_qs(querystring)
-            path = path[:i]
-        else:
-            query = {}
-        if path == '/':
-            if 'provider' in query:
-                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])
-                try:
-                    session = associate(services, url)
-                except ValueError, e:
-                    return self.error(str(e))
-                sessions.append(session)
-                self.send_response(307) # temporary redirect - do not cache
-                self.send_header("Location", request_authentication
-                                 (services, url, session['assoc_handle'],
-                                  self.base_url+"?returned=1"))
-                self.end_headers()
-                return
-            if 'claimed' in query:
-                kind, claimed = normalize_uri(query['claimed'][0])
-                if kind == 'xri':
-                    return self.error('XRI resolution not supported')
-                res = discover(claimed)
-                if res is None:
-                    return self.error('Discovery failed')
-                services, url, op_local = res
-                try:
-                    session = associate(services, url)
-                except ValueError, e:
-                    return self.error(str(e))
-                sessions.append(session)
-                self.send_response(307)
-                self.send_header("Location", request_authentication
-                                 (services, url, session['assoc_handle'],
-                                  self.base_url+"?returned=1",
-                                  claimed, op_local))
-                self.end_headers()
-                return                
-            if 'returned' in query:
-                if 'openid.identity' not in query:
-                    return self.rp_discovery()
-                handle = query['openid.assoc_handle'][0]
-                for session in sessions:
-                    if session['assoc_handle'] == handle:
-                        break
-                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"
-                ax = get_ax(querystring, get_namespaces(querystring), signed)
-                sreg = get_sreg(querystring, signed)
-                email = get_email(querystring)
-                if email:
-                    payload += 'Your email is '+email+"\n"
-                else:
-                    payload += 'No email address is known\n'
-                if 'nickname' in sreg:
-                    username = sreg['nickname']
-                elif "http://axschema.org/namePerson/first" in ax:
-                    username = ax["http://axschema.org/namePerson/first"]
-                    if "http://axschema.org/namePerson/last" in ax:
-                        username += "." + ax["http://axschema.org/namePerson/last"]
-                else:
-                    username = None
-                if username:
-                    payload += 'Your nickname is '+username+'\n'
-                else:
-                    payload += 'No nickname is known\n'
-                if isinstance(payload, unicode):
-                    payload = payload.encode('utf-8')
-                return self.write(payload, "text/plain")
-                
-        return self.not_found()
-
-    
-
-    def debug(self, value):
-        payload = repr(value)
-        if isinstance(payload, unicode):
-            payload = payload.encode('utf-8')
-        self.write(payload, "text/plain")
-
-    def error(self, text):
-        if isinstance(text, unicode):
-            text = text.encode('utf-8')
-        self.write(text, "text/plain")
-
-    def root(self):
-        payload = u"<html><head><title>OpenID login</title></head><body>\n"
-        
-        for name, icon, provider in providers:
-            payload += u"<p><a href='%s?provider=%s'><img src='%s' alt='%s'></a></p>\n" % (
-                self.base_url, name, icon, name)
-        payload += u"<form>Type your OpenID:<input name='claimed'/><input type='submit'/></form>\n"
-        payload += u"</body></html>"
-        self.write(payload.encode('utf-8'), "text/html")
-
-    def rp_discovery(self):
-        payload = '''<xrds:XRDS  
-                xmlns:xrds="xri://$xrds"  
-                xmlns="xri://$xrd*($v*2.0)">  
-                <XRD>  
-                     <Service priority="1">  
-                              <Type>http://specs.openid.net/auth/2.0/return_to</Type>  
-                              <URI>%s</URI>  
-                     </Service>  
-                </XRD>  
-                </xrds:XRDS>
-        ''' % (self.base_url+"/?returned=1")
-        self.write(payload, 'application/xrds+xml')
-
-    def not_found(self):
-        self.send_response(404)
-        self.end_headers()
-        
-# OpenID providers often attempt relying-party discovery
-# This requires the test server to use a globally valid URL
-# If Python cannot correctly determine the base URL, you
-# can pass it as command line argument
-def test_server():
-    import socket, sys
-    if len(sys.argv) > 1:
-        base_url = sys.argv[1]
-    else:
-        base_url = "http://" + socket.getfqdn() + ":8000/"
-    print "Listening on", base_url
-    Handler.base_url = base_url
-    #BaseHTTPServer.HTTPServer.address_family = socket.AF_INET6
-    httpd = BaseHTTPServer.HTTPServer(('', 8000), Handler)
-    httpd.serve_forever()
-
-if __name__ == '__main__':
-    test_server()