From 4840de8c881e6ec3108510599119f68819e48d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 3 Oct 2012 16:54:07 -0400 Subject: [PATCH 1/2] Create Django cache backed OpenID provider store --- .../djangoapps/external_auth/djangostore.py | 123 ++++++++++++++++++ common/djangoapps/external_auth/views.py | 4 +- 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 common/djangoapps/external_auth/djangostore.py diff --git a/common/djangoapps/external_auth/djangostore.py b/common/djangoapps/external_auth/djangostore.py new file mode 100644 index 0000000000..d4b8aba859 --- /dev/null +++ b/common/djangoapps/external_auth/djangostore.py @@ -0,0 +1,123 @@ +"""A openid store using django cache""" + +from openid.store.interface import OpenIDStore +from openid.store import nonce + +from django.core.cache import cache + +import logging +import time + +DEFAULT_ASSOCIATION_TIMEOUT = 60 +DEFAULT_NONCE_TIMEOUT = 600 + +ASSOCIATIONS_KEY_PREFIX = 'openid.provider.associations.' +NONCE_KEY_PREFIX = 'openid.provider.nonce.' + +log = logging.getLogger('DjangoOpenIDStore') + + +def get_url_key(server_url): + key = ASSOCIATIONS_KEY_PREFIX + server_url + return key + + +def get_nonce_key(server_url, timestamp, salt): + key = '{prefix}{url}.{ts}.{salt}'.format(prefix=NONCE_KEY_PREFIX, + url=server_url, + ts=timestamp, + salt=salt) + return key + + +class DjangoOpenIDStore(OpenIDStore): + def __init__(self): + log.info('DjangoStore cache:' + str(cache.__class__)) + + def storeAssociation(self, server_url, association): + key = get_url_key(server_url) + + log.info('storeAssociation {0}'.format(key)) + + associations = cache.get(key, {}) + associations[association.handle] = association + + cache.set(key, associations, DEFAULT_ASSOCIATION_TIMEOUT) + + def getAssociation(self, server_url, handle=None): + key = get_url_key(server_url) + + log.info('getAssociation {0}'.format(key)) + + associations = cache.get(key) + + association = None + + if associations: + if handle is None: + # get best association + valid = [a for a in associations if a.getExpiresIn() > 0] + if valid: + association = valid[0] + else: + association = associations.get(handle) + + # check expiration and remove if it has expired + if association and association.getExpiresIn() <= 0: + if handle is None: + cache.delete(key) + else: + associations.pop(handle) + cache.set(key, association, DEFAULT_ASSOCIATION_TIMEOUT) + association = None + + return association + + def removeAssociation(self, server_url, handle): + key = get_url_key(server_url) + + log.info('removeAssociation {0}'.format(key)) + + associations = cache.get(key) + + removed = False + + if associations: + if handle is None: + cache.delete(key) + removed = True + else: + association = associations.pop(handle) + if association: + cache.set(key, association, DEFAULT_ASSOCIATION_TIMEOUT) + removed = True + + return removed + + def useNonce(self, server_url, timestamp, salt): + key = get_nonce_key(server_url, timestamp, salt) + + log.info('useNonce {0}'.format(key)) + + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + anonce = cache.get(key) + + found = False + + if anonce is None: + cache.set(key, '-', DEFAULT_NONCE_TIMEOUT) + found = False + else: + found = True + + return found + + def cleanupNonces(self): + # not necesary, keys will timeout + return 0 + + def cleanupAssociations(self): + # not necesary, keys will timeout + return 0 diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 5cf21ca68d..a43645bf1d 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -7,6 +7,7 @@ import string import fnmatch from external_auth.models import ExternalAuthMap +from external_auth.djangostore import DjangoOpenIDStore from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login @@ -30,7 +31,6 @@ from openid.consumer.consumer import SUCCESS from openid.server.server import Server from openid.server.trustroot import TrustRoot -from openid.store.filestore import FileOpenIDStore from openid.extensions import ax, sreg import student.views as student_views @@ -400,7 +400,7 @@ def provider_login(request): return default_render_failure(request, "Invalid OpenID request") # initialize store and server - store = FileOpenIDStore('/tmp/openid_provider') + store = DjangoOpenIDStore() server = Server(store, endpoint) # handle OpenID request From e62968d5e154fd1c7ad3ba0e05866381ad245828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 3 Oct 2012 17:16:22 -0400 Subject: [PATCH 2/2] Get OpenID XRDS url host from request Replaced the previous method of getting it from HTTP_POST to use django's mechanism, which may or may not use HTTP_HOST. However if an attacker changes the request header, there is not much he can do since he cannot recreate the association nonce. --- common/djangoapps/external_auth/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index a43645bf1d..6f1e0bc5c4 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -271,10 +271,7 @@ def get_xrds_url(resource, request): """ Return the XRDS url for a resource """ - host = request.META['HTTP_HOST'] - - if not host.endswith('edx.org'): - return None + host = request.get_host() location = host + '/openid/provider/' + resource + '/'