Commits

Anonymous committed a31625a

Adding exhaustive Sphinx information

Comments (0)

Files changed (3)

 Django Authentication Module
 ============================
 
-.. module:: openid2rp
+.. module:: openid2rp.django
 
-A small Django authentication backend for OpenID, based on the openid2rp package.
-It is automatically installed together with openid2rp. 
+This is the implementation of a `Django <http://www.djangoproject.com/>`_ 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::
+In contrast to most (all ?) other Django OpenID authentication packages, this one does not try to cover any view aspects. All error cases are reported by exceptions, which you can render in whatever way you prefer. 
 
-  INSTALLED_APPS = (
-	'django.contrib.auth',
-	'django.contrib.contenttypes',
-	'django.contrib.sessions',
-	'django.contrib.sites',
-	'django.contrib.admin',
-	'openid2rp.django',
-	'<yourapp>.front'
-  )
+Using the authentication backend
+********************************
+It is assumed that you have a working Django app with standard login functionality.
 
-  AUTHENTICATION_BACKENDS = (
-	'django.contrib.auth.backends.ModelBackend',
-	'openid2rp.django.auth.Backend'
-  )
+Add ``openid2rp.django`` to ``INSTALLED_APPS``. Example::
 
-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.
+	INSTALLED_APPS = (
+		'django.contrib.auth',
+		'django.contrib.contenttypes',
+		'django.contrib.sessions',
+		'django.contrib.sites',
+		'django.contrib.admin',
+		'openid2rp.django',
+		'<yourapp>.front' )
 
-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. 
+Add ``openid2rp.django.auth.Backend`` to ``AUTHENTICATION_BACKENDS``.Example::
 
-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.
+	AUTHENTICATION_BACKENDS = (
+		'django.contrib.auth.backends.ModelBackend',
+		'openid2rp.django.auth.Backend' )
 
-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.
+Do once a ``python manage.py syncdb`` to create the relevant tables.
 
-The explicit modeling of each exceptional case hopefully allows you to realize an according 
-reaction in your view rendering. 
+The next step is to modify your existing login view. You need accept an OpenID URI as alternative to username / password. There are JavaScript support libraries available to help you with the URIs for the different providers and their logos.
+
+Add a second view, which will receive the answer from the OpenID authentication provider.
+
+In your original login view code, call ``openid2rp_django.auth.preAuthenticate`` instead of Django's ``authenticate`` if you want to perform an OpenID authentication. Input parameters are the user-provided OpenID URI, and the (absolute !) URL of the newly created view. The results of this call are an ``HttpResponse`` object you must return as view result, and the normalized OpenID URI ('claim') you must store in the user session.
+
+After authentication, the provider will send the users browser back to your newly created second login view. Call Django's ``authenticate`` in there with two keyword arguments for the request object and the claim stored in the session. The result is a Django ``User`` object (if a user record in the database has this claim attached), a Django ``AnonymousUser`` object (if the claim could not be found, but OpenID authentication was ok), or an Exception if anything went wrong. In the second case, you might offer some user registration facility. You can use ``openid2rp_django.auth.linkOpenID`` to assign claims to Django user objects, so that ``authenticate`` is successfull the next time. 
+
+Functions
+*********
+
+The Django application can use the following API:
+
+.. function:: openid2rp_django.auth.preAuthenticate(uri, answer_url, sreg = (('nickname', 'email'), ()), ax = ((openid2rp.AX.email, openid2rp.AX.first, openid2rp.AX.last), ())) -> response, claimedID
+
+``uri`` is the OpenID URI input from the user. ``answer_url`` is the absolute address of the view that will later call ``authenticate()``. You can realize this view in whatever way you prefer, for example also by using the original login view with another GET parameter. ``sreg`` resp. ``ax`` allow you to request a set of information attributes from the authentication provider. Check the openid2rp and OpenID documentation for details.
+
+The first result is the ``HttpResponse`` object you should directly return from the view code after calling ``preAuthenticate``. It contains a 307 redirection to the authentication provider URL, so that the user's browser goes forward to the actual provider authentication screen. The second result is the normalized version of the OpenID URI, called a 'claim'. You need this in the following call to ``authenticate()``, so store it somewhere (e.g. in the user session). 
+	
+If something goes wrong, one of the following errors is raised:
+	
+* ``IncorrectURIError``: The provided URI couldn't be normalized. 
+* ``IncorrectClaimError``: The OpenID discovery step for this claim failed. This might be a typo, but can also be reasoned by an unavailable provider.
+
+.. function:: django.contrib.auth.authenticate(request=None, claim=None) -> user
+
+In the handling of the providers redirection back to your site, you need to call Django's ``authenticate`` function with two keyword parameters. ``request`` is the ``HttpRequest`` input object for your view code. The authentication provider fetches all relevant information from it. The second parameter must be ``claim``, which is one of the results from ``preAuthenticate`` we asked you to store somewhere.
+	
+The result of this call is either:
+
+* A Django ``User`` object. In this case, the OpenID claim was successfully authenticated, and the backend found a user in the database with this claim attached. The object as additional attributes:
+	
+	* ``openid_claim``: The claim that was finally authenticated. Depending on the OpenID provider, this might or might not be the original method input. In a later call to ``linkOpenID``, use only this one.
+	* ``openid_ax``: A dictionary of received AX values.
+	* ``openid_sreg``: A dictionary of received SREG values.	
+		
+* A Django ``AnonymousUser`` object. In this case, the claim could not be related to any user in the database, but the OpenID authentication was ok. The object has the same additional attributes as above. In this situation, you should normally proceed with some new user registration functionality. You can use the AX / SREG data to pre-fill some registration form. 
+
+* One of the following exceptions:
+	
+	* ``MissingSessionError``: There is no stored session for this result. This typically means that you forgot to start with ``preAuthenticate``.
+	* ``AuthenticationError``: Something went wrong in the OpenID authentication process. The exception message contains more information.
+	* ``IncompleteAnswerError``: This is normally the providers fault.
+	* ``MultipleClaimUsageError``: The authenticated claim was linked to multiple users, which is not valid. You need to correct your database.
+	* ``ReplayAttackError``: The nonce checking mechanisms identified an answer  that was already given before. 
+	* ``TookTooLongError``: The authentication at the provider side took too long. You can override the default value (5 min) in your settings file with the parameter ``OPENID2RP_MAXLOGINDELAY``.
+	
+.. function:: openid2rp_django.auth.getOpenIDs(user) -> ids
+
+Returns a string list of stored OpenID claim URIs for this Django user object. This is intended for your user settings view.
+	
+.. function:: openid2rp_django.auth.linkOpenID(user, claim) -> 
+
+Links the given Django user object to the given OpenID claim.
+
+.. function:: openid2rp_django.auth.unlinkOpenID(user, claim) -> 
+
+Unlinks the given Django user object from the given OpenID claim. This is intended for your user settings view.
+
+Time, clocks, and the ordering of events
+****************************************
+For the different timestamp checks, the authentication backend allows a maximum derivation of authentication provider clock and relaying party clock of 5 min. You can override this default value in your Django settings file with the parameter ``OPENID2RP_MAXTIMESHIFT``.
 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 package also includes a stand-alone server, as an example
+The openid2rp package includes a stand-alone server, as an example
 and a test; run this as ``python -m openid2rp.testapp``.
 
+The openid2rp package also provides a Django authentication backend, which is described separately:
+
 .. toctree::
 
    django

openid2rp/django/auth.py

+# -*- coding: utf-8 -*-
+# Django authentication backend, based on openid2rp
+# Copyright Peter Tröger, 2010
+# Licensed under the Academic Free License, version 3
 
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.http import HttpResponse
 from django.db.models import Q
 from django.contrib.auth.models import AnonymousUser
+from django.conf import settings
 import openid2rp
 import datetime
 
 # we need to accept some difference between provider time and our time, for nonce and session checking
-#Martin says: Use the Kerberos default
+# Martin says: Use the Kerberos default
 maxTimeShift=datetime.timedelta(minutes=5)
+if settings.OPENID2RP_MAXTIMESHIFT:
+	maxTimeShift=datetime.timedelta(minutes=int(settings.OPENID2RP_MAXTIMESHIFT))
+maxLoginDelay=datetime.timedelta(minutes=5)
+if settings.OPENID2RP_MAXLOGINDELAY:
+	maxLoginDelay=datetime.timedelta(minutes=int(settings.OPENID2RP_MAXLOGINDELAY))
 
 AX = openid2rp.AX
 
-class IncorrectURI(Exception):
+class IncorrectURIError(Exception):
 	pass
 
-class IncorrectClaim(Exception):
+class IncorrectClaimError(Exception):
 	pass
 	
-class MissingSession(Exception):
+class MissingSessionError(Exception):
 	pass
 
 class AuthenticationError(Exception):
 class ReplayAttackError(Exception):
 	pass
 
-class IncompleteAnswer(Exception):
+class IncompleteAnswerError(Exception):
 	pass
 	
-class MultipleClaimUsage(Exception):
+class MultipleClaimUsageError(Exception):
+	pass
+	
+class TookTooLongError(Exception):
 	pass
 
 def cleanup():
 		pass
 	
 def storeNonce(nonce):
-	global maxNonceTransmission
+	global maxTimeShift, maxLoginDelay
 	db = OpenIDNonce()
 	db.nonce=nonce
-	db.expiration_date = datetime.datetime.utcnow() + maxTimeShift
+	db.expiration_date = datetime.datetime.utcnow() + maxTimeShift + maxLoginDelay
 	db.save()
 	
 def knownNonce(n):
 	return result
 
 	
-def storeOpenID(user, openid):
+def linkOpenID(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 unlinkOpenID(user, openid):
+	res = UserOpenID.objects.filter(Q(user=user, uri=openid))
+	for r in res:
+		r.delete()
+
 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
-	"""
 
 	cleanup()
-
 	try:
 		kind, claimedId = openid2rp.normalize_uri(uri)			
 	except Exception, e:
-		raise IncorrectURI(str(e))
+		raise IncorrectURIError(str(e))
 	res = openid2rp.discover(claimedId)
 	if res != None:
 		services, url, op_local = res
 		response.status_code=307
 		return response, claimedId
 	else:
-		raise IncorrectClaim()
+		raise IncorrectClaimError()
 
 class Backend:	
 	def get_user(self, user_id):
 			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.
+		global maxTimeShift, maxLoginDelay
 
-		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, ReplayAttackError
-		"""
-		
-		global maxTimeShift, maxNonceTransmission		
-
+		# the default way for telling Django that this auth backend does not fit
 		if not ("request" in credentials and "claim" in credentials):
 			raise TypeError
 
 		handle = request.GET['openid.assoc_handle']
 		session=getSessionByHandle(handle)
 		if not session:
-			raise MissingSession
+			raise MissingSessionError
 		try:
 			signed=openid2rp.authenticate(session, query)
 		except Exception, e:
 		# check for replay attack
 		nonce = request.GET['openid.response_nonce']
 		timestamp = openid2rp.parse_nonce(nonce)		
-		# provider timestamp was signed (=not forged), with replay, it would be too old; consider time shift
-		if timestamp < datetime.datetime.utcnow() - maxTimeShift: 
-			raise ReplayAttackError()
+		if timestamp < datetime.datetime.utcnow() - maxTimeShift - maxLoginDelay: 
+			raise TookTooLongError()
 		elif knownNonce(nonce):
 			raise ReplayAttackError()
 		storeNonce(nonce)
 		# 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()
+				raise IncompleteAnswerError()
 			claimedId = request.GET['openid.claimed_id']
 		else:
 			if 'identity' not in signed:
-				raise IncompleteAnswer()
+				raise IncompleteAnswerError()
 
 		# 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()
+			raise MultipleClaimUsageError()
 		elif len(idrecord)<1:
 			# No user has this OpenID claim string assigned
 			user = AnonymousUser()