Skip to content

Instantly share code, notes, and snippets.

@d-demirci
Created September 17, 2020 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d-demirci/bc5a755b555e369d2c38c81c0663eb02 to your computer and use it in GitHub Desktop.
Save d-demirci/bc5a755b555e369d2c38c81c0663eb02 to your computer and use it in GitHub Desktop.
tutor middleware sso openedx
""" sso openedx test for replay attack"""
"""
Cache-backed ``AuthenticationMiddleware``
-----------------------------------------
``CacheBackedAuthenticationMiddleware`` is an
``django.contrib.auth.middleware.AuthenticationMiddleware`` replacement to
avoid querying the database for a ``User`` instance in each request.
Whilst the built-in ``AuthenticationMiddleware`` mechanism will only obtain the
``User`` instance when it is required, the vast majority of sites will do so on
every page to render "Logged in as 'X'" text as well to evaluate the result of
``user.is_authenticated`` and ``user.is_superuser`` to provide conditional
functionality.
This middleware eliminates the cost of retrieving this ``User`` instance by
caching it using the ``cache_toolbox`` instance caching mechanisms.
Depending on your average number of queries per page, saving one query per
request can---in aggregate---reduce load on your database. In addition,
avoiding the database entirely for pages can avoid incurring any connection
latency in your environment, resulting in faster page loads for your users.
Saving this data in the cache can also be used as a way of authenticating users
in systems outside of Django that should not access your database. For
example, a "maintenance mode" page would be able to render a personalised
message without touching the database at all but rather authenticating via the
cache.
``CacheBackedAuthenticationMiddleware`` is ``AUTHENTICATION_BACKENDS`` agnostic.
Implementation
~~~~~~~~~~~~
The cache and session backends are still accessed on each request - we are
simply assuming that they are cheaper (or otherwise more preferable) to access
than your database. (In the future, signed cookies may allow us to avoid this
lookup altogether -- whilst we could not safely save ``User.password`` in a
cookie, we could use delayed loading to pull it out when needed.)
Another alternative solution would be to store the attributes in the user's
session instead of in the cache. This would save the cache hit on every request
as all the relevant data would be pulled in one go from the session backend.
However, this has two main disadvantages:
* Session keys are not deterministic -- after making changes to an
``auth_user`` row in the database, you cannot determine the user's session
key to flush the now out-of-sync data (and doing so would log them out
anyway).
* Stores data per-session rather than per-user -- if a user logs in from
multiple computers the data is duplicated in each session. This problem is
compounded by most projects wishing to avoid expiring session data as long
as possible (in addition to storing sessions in persistent stores).
Usage
~~~
To use, find ``MIDDLEWARE_CLASSES`` in your ``settings.py`` and replace::
MIDDLEWARE_CLASSES = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]
with::
MIDDLEWARE_CLASSES = [
...
'openedx.core.djangoapps.cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
...
]
You should confirm you are using a ``SESSION_ENGINE`` that doesn't query the
database for each request. The built-in ``cached_db`` engine is the safest
choice for most environments but you may be happy with the trade-offs of the
``memcached`` backend - see the Django documentation for more details.
"""
from logging import getLogger
from django.conf import settings
from django.contrib.auth import HASH_SESSION_KEY
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.auth.models import AnonymousUser, User
from django.utils.crypto import constant_time_compare
from django.utils.deprecation import MiddlewareMixin
from django.contrib import auth
from openedx.core.djangoapps.safe_sessions.middleware import SafeSessionMiddleware
import six
from six import text_type
from .model import cache_model
log = getLogger(__name__)
class CacheBackedAuthenticationMiddleware(AuthenticationMiddleware, MiddlewareMixin):
"""
See documentation above.
"""
def __init__(self, *args, **kwargs):
cache_model(User)
super(CacheBackedAuthenticationMiddleware, self).__init__(*args, **kwargs)
def process_request(self, request):
try:
# Try and construct a User instance from data stored in the cache
session_user_id = SafeSessionMiddleware.get_user_id_from_session(request)
cached_user = User.get_cached(session_user_id)
if cached_user is None :
request.user = User.objects.get(id=int(session_user_id))
log.error(u"pp::: cached_user line 113 '%s'.", cached_user)
else:
request.user = cached_user
log.error(u"pp::: get_user_id_from_session line 116 '%s'.", session_user_id)
if request.user.id != session_user_id:
log.error(
u"pp:: CacheBackedAuthenticationMiddleware cached user '%s' does not match requested user '%s'.",
request.user.id,
session_user_id,
)
# Raise an exception to fall through to the except clause below.
raise Exception
#self._verify_session_auth(request)
if not CacheBackedAuthenticationMiddleware._is_user_authenticated(request):
user = auth.authenticate(request.user)
if user is None:
return HttpResponseForbidden()
request.user = user
auth.login(request, user)
except Exception as e: # pylint: disable=bare-except
# Fallback to constructing the User from the database.
log.error(
u"pp::: cache error {0!r}: {1}".format( # pylint: disable=logging-format-interpolation
six.text_type(self),
text_type(e),
)
)
super(CacheBackedAuthenticationMiddleware, self).process_request(request)
@staticmethod
def _is_user_authenticated(request):
user = request.user
return user and user.is_authenticated
def _verify_session_auth(self, request):
"""
Ensure that the user's session hash hasn't changed.
"""
# Auto-auth causes issues in Bok Choy tests because it resets
# the requesting user. Since session verification is a
# security feature, we can turn it off when auto-auth is
# enabled since auto-auth is highly insecure and only for
# tests.
auto_auth_enabled = settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING', True)
if not auto_auth_enabled and hasattr(request.user, 'get_session_auth_hash'):
session_hash = request.session.get(HASH_SESSION_KEY)
if not (session_hash and constant_time_compare(session_hash, request.user.get_session_auth_hash())):
# The session hash has changed due to a password
# change. Log the user out.
request.session.flush()
request.user = AnonymousUser()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment