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.
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.
What about here? We can emit status and the other auth details?
https://github.com/evonove/django-oauth-toolkit/blob/3960084db942a4b7f803ff94eb5b7cff0cef1c10/oauth2_provider/oauth2_backends.py#L128
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()
Most helpful comment
Hello Everyone,
Thanks @LucasRoesler for the solution. This is another approach: