Commits

Anonymous committed 6b63bd4

Introduced persistent sessions and nonces, and their checking. Still a bug in this version with the datetime checking

Comments (0)

Files changed (5)

 Changes
 -------
 
+1.6 (2010-07-29):
+
+* Add session and nounce checking to Django backend
+
 1.5 (2010-07-27):
 
 * Make openid2rp a package, move testapp into that package.
 Response Utility Functions
 --------------------------
 
-.. function:: parse_nonce(nonce) -> (timestamp, ID)
+.. function:: parse_nonce(nonce) -> timestamp
 
-   Split a response_nonce value into a datetime.datetime timestamp,
-   and the provider nonce ID. Applications should set a reasonable
+
+   Extract the datetme.datetime timestamp from a response_nonce value.
+   Applications should set a reasonable
    conservative timeout (e.g. one hour) for the transmission of a
    nonce from the provider to the RP. Nonces older than now()-timeout
    can be discarded as replays right away. Younger nonces should be

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 openid2rp.django.models import UserOpenID, OpenIDSession
 from django.http import HttpResponse
 from django.db.models import Q
 from django.contrib.auth.models import AnonymousUser
 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
+maxTimeShift=datetime.timedelta(minutes=5)
 
 AX = openid2rp.AX
-sessions=[]
 
 class IncorrectURI(Exception):
 	pass
 class AuthenticationError(Exception):
 	pass
 
+class ReplayAttackError(Exception):
+	pass
+
 class IncompleteAnswer(Exception):
 	pass
 	
 class MultipleClaimUsage(Exception):
 	pass
+
+def cleanup():
+	try:
+		# delete all expired nonces 
+		entries=OpenIDNonce.objects.filter(Q(expiration_date__lt = datetime.datetime.now()))
+		for e in entries:
+			e.delete()
+	except:
+		pass
+	try:
+		# delete all expired sessions
+		entries=OpenIDSession.objects.filter(Q(expiration_date__lt = datetime.datetime.now()))
+		for e in entries:
+			e.delete()
+	except:
+		pass
+	
+def storeNonce(nonce):
+	global maxNonceTransmission
+	db = OpenIDNonce()
+	db.nonce=nonce
+	db.expiration_date = datetime.datetime.now() + maxNonceTransmission
+	db.save()
+	
+def knownNonce(n):
+	try:
+		result = OpenIDNonce.objects.get(nonce=n)
+	except:
+		return False
+	return True
+	
+def storeSession(session, claim):
+	global maxTimeShift
+
+	db = OpenIDSession()
+	db.assoc_handle=session['assoc_handle']
+	db.mac_key=session['mac_key']
+	db.session_type=session['session_type']
+	db.assoc_type=session['assoc_type']
+	db.ns=session['ns']
+	db.claimedId=claim
+	# Expire session in provider-given amount of seconds, consider possible shift
+	db.expiration_date = datetime.datetime.now() + datetime.timedelta(seconds=long(session['expires_in'])) - maxTimeShift
+	db.save()
+
+def getSessionByHandle(handle):
+	try:
+		result = OpenIDSession.objects.filter(Q(assoc_handle=handle)).values()[0]
+	except:
+		return None
+	# we can live with the fact that the original expires_in field is missing, since openid2rp is not checking it anyway
+	return result
+
+def getSessionByClaim(claim):
+	try:
+		result = OpenIDSession.objects.filter(Q(claimedId=claim)).values()[0]
+	except:
+		return None
+	# we can live with the fact that the original expires_in field is missing, since openid2rp is not checking it anyway
+	return result
+
 	
 def storeOpenID(user, openid):
 	claim=UserOpenID(user=user, uri=openid)
 	
 	If something goes wrong, one of the following errors is raised: IncorrectURI, IncorrectClaim
 	"""
-	global sessions
+
+	cleanup()
 
 	try:
 		kind, claimedId = openid2rp.normalize_uri(uri)			
 	res = openid2rp.discover(claimedId)
 	if res != None:
 		services, url, op_local = res
-		session = openid2rp.associate(services, url)
-		sessions.append(session)
+		# re-use session in order to avoid provider roundtrip here
+		session = getSessionByClaim(claimedId)
+		if not session:
+			session = openid2rp.associate(services, url)
+			storeSession(session, claimedId)
 		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
 		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
+		Possible errors: MissingSession, AuthenticationError, IncompleteAnswer, MultipleClaimUsage, ReplayAttackError
 		"""
-
-		global sessions
+		
+		global maxTimeShift, maxNonceTransmission		
 
 		if not ("request" in credentials and "claim" in credentials):
 			raise TypeError
 		
+		import pdb; pdb.set_trace()
+		
 		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
+		session=getSessionByHandle(handle)
 		if not session:
 			raise MissingSession
 		try:
 			signed=openid2rp.authenticate(session, query)
 		except Exception, e:
 			raise AuthenticationError(str(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.now() - maxTimeShift: # ???
+			raise ReplayAttackError()
+		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:
 		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:
 			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)

openid2rp/django/models.py

 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)
+	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)
+
+class OpenIDSession(models.Model):
+	claimedId = models.CharField(max_length=255, blank=False, null=False)
+	assoc_handle = models.CharField(max_length=255, blank=False, null=False)
+	mac_key = models.CharField(max_length=255, blank=False, null=False)
+	session_type = models.CharField(max_length=255, blank=False, null=False)
+	expiration_date = models.DateTimeField(null=False, blank=False, editable=False)
+	assoc_type = models.CharField(max_length=255, blank=False, null=False)
+	ns = models.CharField(max_length=255, blank=False, null=False)
+	
+class OpenIDNonce(models.Model):
+	nonce = models.CharField(max_length=255, blank=False, null=False)
+	expiration_date = models.DateTimeField(null=False, blank=False, editable=False)
 except ImportError:
     pass
 
-version='1.5'
+version='1.6'
 setup(name='openid2rp',
       version=version,
       description='OpenID 2.0 Relying Party Support Library',