Opened 6 years ago

Closed 6 years ago

#25392 closed New feature (needsinfo)

Allow contrib.auth to work without sessions

Reported by: Roman Odaisky Owned by: nobody
Component: contrib.auth Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Currently, the authentication middleware requires the session middleware, while in many cases, particularly when building an API-only backend, sessions are completely unnecessary, and what’s worse, they cause a write query during handling of otherwise read-only requests.

Instead, the authentication system should permit operation without sessions altogether:

  1. If sessions are unavailable, request.user should stay AnonymousUser until user calls auth.login.
  2. If sessions are unavailable, auth.login should be fine not saving anything to sessions.

Additionally, in cases both a traditional Web interface and an API (where authentication is handled by keys in HTTP headers or request parameters) are present, it would be useful to be able to turn the session mechanism on an off (or rather, to instruct the authentication mechanism not to touch sessions) on per-request basis.

Additionally, when sessions are, in fact, made use of, they should not perform write queries until something is actually saved into a session, for performance reasons.

Change History (9)

comment:1 Changed 6 years ago by Tim Graham

Thanks for the proposal. Are you interested in providing a patch for this? Without look at the details, I'm wondering if this might be handled better as a separate app since contrib.auth seems somewhat tightly coupled to sessions in my mind. A separate app might work better than trying to have one application cater to both use cases (although again, it's hard for me to say how much code duplication would be required). Are there any third-party applications that provide this functionality? It would probably be a good idea to raise the idea on the DevelopersMailingList to get feedback on how to proceed.

comment:2 Changed 6 years ago by Tim Graham

Summary: Auth without sessionsAllow contrib.auth to work without sessions
Type: UncategorizedNew feature
Version: 1.8master

comment:3 Changed 6 years ago by Roman Odaisky

Doing this would not be complex because the middleware in question isn’t complex. In my project, I work around the issues with just the code below.

And maybe the problem of the session DB backend making a write query upon first attempt to read something from it (the insanity of this rivals the invention of atime) should be separated into a standalone issue.

class FastSessionMiddleware(django.contrib.sessions.middleware.SessionMiddleware):
    def process_request(self, request):
        if is_api_request(request) and settings.SESSION_COOKIE_NAME not in request.COOKIES:
            return

        super(FastSessionMiddleware, self).process_request(request)

    def process_response(self, request, response):
        if is_api_request(request):
            return response

        return super(FastSessionMiddleware, self).process_response(request, response)

class FastAuthenticationMiddleware(django.contrib.auth.middleware.AuthenticationMiddleware):
    def process_request(self, request):
        if hasattr(request, "session"):
            return super(FastAuthenticationMiddleware, self).process_request(request)

        request.user = django.contrib.auth.models.AnonymousUser()

    assert not hasattr(django.contrib.auth.middleware.AuthenticationMiddleware, "process_response")

def fast_login(request, user, remember=False):
    if not hasattr(request, "session"):
        assert not remember
        request.user = user
        return

    auth.login(request, user)

    request.session.set_expiry(None if remember else 0)

# also related, would appreciate this controlled by a settings item:
django.contrib.auth.signals.user_logged_in.disconnect(django.contrib.auth.models.update_last_login)

comment:4 Changed 6 years ago by Tim Graham

Could you sketch out your idea for translating your code into a generic patch for Django?

We try to include features in Django that meet the 80% use case test. If this can be done outside of Django and doesn't meet that criteria, maybe it's best to leave it like that. I suggested to write to the mailing list to help gauge if it's a common issue faced by other developers.

comment:5 Changed 6 years ago by Roman Odaisky

The changes don’t look profound to me.

Firstly, contrib.auth has only a few references to request.session, none of which are essential. Just putting an if hasattr(request, "session") prior to such code will do. This makes, for example, auth.login only set a non-persistent request.user, but additional functionality such as emitting a signal is preserved.

Secondly, when session middleware is used for some requests, but not others (e. g. using the Django admin on an otherwise API-only site), there should be a way to login a user without touching the session. Currently, request.user = get_whatever_user() works just fine, but using an officially supported function would be better, something like auth.login(request, user_I_just_found_in_the_DB, persist_in_session=False). Would also have the benefit of not having to bother with the concept of authentication backends, not to mention being much more future-proof in case some new logic appears in auth.login.

Finally, the session DB backend seems to go out of its way to ensure a 32-character base-36 session key is unique. That’s about 160 bits, same length as SHA-1 output! If everyone in the world started generating a key each millisecond, with 99.999995% probability no-one would have a collision within a million years. Just generate a random key and store the record in process_response, which gives the option of not storing it at all in case no data got written to the session (currently a write request is always performed on first access, even if that’s read access).

As for how common the issue is — it’s primarily a performance issue. Django, being the core library that it is, should have as high performance as possible out of the box, and removing unnecessary DB write queries that it currently issues in its default configuration (the other culprit is update_last_login) should be high on the list.

comment:6 Changed 6 years ago by Collin Anderson

Hi,

"currently a write request is always performed on first access, even if that’s read access" -- I think this is a bug that's been recently fixed in 1.8.4 and 1.7.10:

"The SessionMiddleware has been modified to no longer create empty session records."

What version of django are you seeing this behavior?

comment:7 Changed 6 years ago by Roman Odaisky

Good to know. That does alleviate much of the concern.

Still, some cleanup of contrib.auth, nothing major, would be nice, such as getting the aforementioned official auth.login(persist=False) function.

In general, much of Django is built around the traditional, pre-REST, pre-AJAX ways and interaction of Django components follows corresponding workflows. Nothing wrong with that, but as a result some parts of Django got too tightly coupled so when one thing becomes unnecessary, getting other things in isolation can be cumbersome.

comment:8 Changed 6 years ago by Collin Anderson

Would it work to just set request.user = user and not use login() at all? It sounds like you want to manually attach a user to a request without using django at all.

It's also pretty easy to swap in your own subclass of the middleware like shown above, without needing to modify django.

Also, django handles the session and user lazily, so if request.SESSION is not accessed, the session table is never queried, and if request.user is never accessed, then the user table won't be automatically queried, and request.SESSION won't be accessed. They're only queried as needed.

comment:9 Changed 6 years ago by Tim Graham

Resolution: needsinfo
Status: newclosed

rdaysky (or anyone else interested in this), if you have a patch to share, please send a pull request and reopen this ticket. Absent that, it's unclear to me what particular changes should be done (especially given Collin's comments about simply not using Django's built in functionality and building something that matches your needs).

Note: See TracTickets for help on using tickets.
Back to Top