Django-oauth-toolkit: Record last_login against a User

Created on 23 Feb 2016  路  8Comments  路  Source: jazzband/django-oauth-toolkit

Is there a good way to record the last_login on the user model? It seems very difficult to hook into the view in any useful way. I was thinking about resorting to a response middleware.

Most helpful comment

Hello Everyone,

Thanks @LucasRoesler for the solution. This is another approach:

from oauth2_provider.models import AccessToken
from django.dispatch import receiver
from django.db.models.signals import post_save
from datetime import datetime


@receiver(post_save, sender=AccessToken, dispatch_uid="record_last_login")
def record_login(sender, instance, created, **kwargs):
    if created:
        instance.user.last_login = datetime.now()
        instance.user.save()

All 8 comments

I was wondering the same thing, maybe a signal whenever a accesstoken is generated, so you can do last_login, number of logins etc.

A signal is a great idea.

I can't work out the best place to put it. Views.base.TokenView seems like a good start but there's no indication of success there.

So if you follow this around long enough, this is where the magic happens:
https://github.com/idan/oauthlib/blob/79722b2aa0b191db4a345ce29095b70d92ded140/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py#L219

From that, it looks like status will only be 200 for a successful grant, so to keep it simple we can do something like this:

First we change the backend class to ours:

OAUTH2_PROVIDER = {
    'SCOPES': {
        'read': 'Read scope',
        'write': 'Write scope',
    },
    'OAUTH2_BACKEND_CLASS': 'abas.apps.api.oauth.TrackedLoginOAuthLibCore',
}

Then our backend class overwrites the method:

from oauth2_provider.oauth2_backends import OAuthLibCore
from django.utils import timezone
from django.contrib.auth import get_user_model
import logging

logger = logging.getLogger(__name__)
User = get_user_model()

class TrackedLoginOAuthLibCore(OAuthLibCore):
    def create_token_response(self, request):
        """
        A wrapper method that calls create_token_response on `server_class` instance.
        :param request: The current django.http.HttpRequest object
        """
        uri, http_method, body, headers = self._extract_params(request)
        extra_credentials = self._get_extra_credentials(request)

        headers, body, status = self.server.create_token_response(uri, http_method, body,
                                                                  headers, extra_credentials)
        uri = headers.get("Location", None)

        # Check for a valid token and emit a signal
        if status == 200 and 'access_token' in body:
            username = request.POST.get('username')
            cnt = User.objects.filter(email=username).update(last_login=timezone.now())
            logger.debug('Updated last login for username=%s (%d)' % (username, cnt))

        return uri, headers, body, status

I've done the update right there rather than emitting a signal for simplicity.

What I posted fails for the refresh token flow, this is what I'm doing now:

class TrackedLoginOAuthLibCore(OAuthLibCore):
    def create_token_response(self, request):
        """
        A wrapper method that calls create_token_response on `server_class` instance.
        :param request: The current django.http.HttpRequest object
        """
        uri, http_method, body, headers = self._extract_params(request)
        extra_credentials = self._get_extra_credentials(request)

        headers, body, status = self.server.create_token_response(uri, http_method, body,
                                                                  headers, extra_credentials)
        uri = headers.get("Location", None)

        # Check for a valid token and emit a signal
        if status == 200 and 'access_token' in body:
            access_token = json.loads(body).get('access_token')
            token = AccessToken.objects.get(token=access_token)
            api_login_success.send(sender=token.user.__class__, user=token.user, request=request)

        return uri, headers, body, status

Then the signal handler:

@receiver(api_login_success)
def handle_api_login(sender, user, request, **kwargs):
    user.last_login = timezone.now()
    user.save()

    logger.debug('Updated last login for account=%s username=%s' % (user.pk, user.email))
    ipaddr = get_ip_from_headers(request)
    UserLogin.objects.create(
        user=user,
        source='iPad {scheme}'.format(scheme=request.scheme),
        ipaddr=ipaddr,
    )

For future reference, I did not have much luck with @aidanlister's method. For some reason the OAUTH2_BACKEND_CLASS setting was simply ignored. I think the method should work, the source code certainly looks like it is supported, but I wasn't able to chase down why it didn't in my project.

Switching to a signal on on the AccessToken post-save worked perfectly for us.

def record_login(sender, instance, *args, **kwargs):
    instance.user.last_login = timezone.now()
    instance.user.save()

signals.post_save.connect(record_login, sender=AccessToken)

In our project we have a oauth app and I put the above signal in oauth/signals.py. I then updated the AppConfig in oauth/apps.py

from django.apps import AppConfig


class OauthAppConfig(AppConfig):
    name = 'oauth'
    verbose_name = 'OAuth'

    def ready(self):
        super(OauthAppConfig, self).ready()
        import oauth.signals

Everything worked as expected.

Hello Everyone,

Thanks @LucasRoesler for the solution. This is another approach:

from oauth2_provider.models import AccessToken
from django.dispatch import receiver
from django.db.models.signals import post_save
from datetime import datetime


@receiver(post_save, sender=AccessToken, dispatch_uid="record_last_login")
def record_login(sender, instance, created, **kwargs):
    if created:
        instance.user.last_login = datetime.now()
        instance.user.save()

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hackaprende picture hackaprende  路  5Comments

pawanvirsingh picture pawanvirsingh  路  6Comments

lokilak picture lokilak  路  4Comments

jlhawn picture jlhawn  路  3Comments

bryanveloso picture bryanveloso  路  6Comments