OAuth2: authenticate with email instead of username - django

I'm using OAuth2 with django-oauth-toolkit django-rest-framework.
I usually authenticate my users the following way, in order to get a token:
curl -X POST -d "grant_type=password&username=new_user&password=new_user" -u "GZwzDjPM89BceT8a6ypKGMbXnE4jWSzsyqbM3dlK:" http://localhost:8000/o/token/
Is there a way to use the email instead of the username to authenticate my users?
Thanks!

Yes! it is possible by setting the email as username in User model.
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(
verbose_name='email address',
max_length=255,
unique=True,
)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
USERNAME_FIELD = 'email'
then email can now be used as username in the request.
curl -X POST -d "grant_type=password&username=test#test.com&password=new_user" -u "GZwzDjPM89BceT8a6ypKGMbXnE4jWSzsyqbM3dlK:" http://localhost:8000/o/token/

If you don't want to (or can't) create a custom User model, here's a simple solution: Just override the rest_framework_social_oauth2.views.TokenView like this:
from django.contrib.auth import get_user_model
import rest_framework_social_oauth2.views
class TokenView(rest_framework_social_oauth2.views.TokenView):
def create_token_response(self, request):
email = request.POST.pop('email', None)
if email:
username = get_user_model().objects.filter(email=email[0]).values_list('username', flat=True).last()
request.POST['username'] = username
return super(TokenView, self).create_token_response(request)
Then, in your urls.conf wire up this customized view to the oauth/token pattern, e.g.:
urlpatterns = [
url(r'^oauth/token', my_auth.TokenView.as_view()), # The customized TokenView
url(r'^oauth/', include('rest_framework_social_oauth2.urls')), # Original URLs
]

I don't know why this question was never answered, but anyways, I hope it would be useful my answer for whoever encounter this problem too:
The ugly but quick way:
Send the email as username in your POST request, then find its true username and replace your POST request with this data instead; all this in your middleware:
class DisableCSRF(object):
"""Middleware for disabling CSRF in a specified app name.
"""
def process_request(self, request):
"""Preprocess the request.
"""
if (resolve(request.path_info).app_name == "api") or (
resolve(request.path_info).namespace == "oauth2_provider"):
if resolve(request.path_info).namespace == "oauth2_provider":
post_data = request.POST.copy()
true_username = User.objects.get(
email=request.POST.get("username")
).username
post_data["username"] = true_username
request.POST = post_data
setattr(request, '_dont_enforce_csrf_checks', True)
else:
pass # check CSRF token validation
The elegant and good practice way:
Create your own class to request auth tokens so you can receive the email as your username. You can even add more custom authentications like Facebook or Google logins. This is definitely the best way to do it but it takes some more time to develop, so its up to you if you have enough time to implement it.
Hope it's not too late for answering this.

knaperek's answer did not work out for me.
Based on his code, I ended up with the following solution that worked great for my case:
import json
from django.http import HttpResponse
from django.contrib.auth import get_user_model
from oauth2_provider.views.base import TokenView
from oauth2_provider.models import get_access_token_model
from oauth2_provider.signals import app_authorized
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.debug import sensitive_post_parameters
from django.contrib.auth import get_user_model
#method_decorator(csrf_exempt, name="dispatch")
class TokenView(TokenView):
#method_decorator(sensitive_post_parameters("password"))
def post(self, request, *args, **kwargs):
request.POST = request.POST.copy()
email = request.POST.pop('email', None)
request.POST['username'] = get_user_model().objects.filter(email=email[0]).values_list('username', flat=True).last()
url, headers, body, status = self.create_token_response(request)
if status == 200:
access_token = json.loads(body).get("access_token")
if access_token is not None:
token = get_access_token_model().objects.get(token=access_token)
app_authorized.send(sender=self, request=request, token=token)
response = HttpResponse(content=body, status=status)
for k, v in headers.items():
response[k] = v
return response

Related

How can I use email instead of username in djangoreostframework-simplejwt?

I am going to use email instead of username when I get access token and refresh token using djangoreostframework-simplejwt. So after writing the code, I could access my browser and confirm that the field that was username was renamed to email.But when I post the email of user in email field, the following error appears.
"detail": "No active account found with the Given Credentials"
Can you tell me what is wrong with my code? Here is my code.
Serializers.py
from rest_framework_simplejwt.serializers import TokenObtainSerializer
from django.contrib.auth.models import User
class EmailTokenObtainSerializer(TokenObtainSerializer):
username_field = User.EMAIL_FIELD
class CustomTokenObtainPairSerializer(EmailTokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data["refresh"] = str(refresh)
data["access"] = str(refresh.access_token)
return data
views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomTokenObtainPairSerializer
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
thanks
You override the validate function of the class CustomTokenObtainPairSerializer.
In the Validate function, you call the validate super(). In TokenObtainSerializer the validate function uses the Django authenticate function. That uses a username and password to validate the user. You might want to create a custom authentication backend that uses email to authenticate.

django-axes is not getting the request argument

I recently added django-axes to my Django project. It is suppose to work out the box with django-restframework. However, I am using django-rest-framework-simplejwt to handle authentication. But it should still work out the box since the only thing that is required for django-axes is passing Django's authentication method the request object which it does in it's source code (line 39 and 43).
When I try to authenticate, I get this error from django-axes:
axes.exceptions.AxesBackendRequestParameterRequired: AxesBackend requires a request as an argument to authenticate
You need to add requests to the authentication function. See sample code below.
serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def _authenticate_user_email(self, email, password, request):
# This is key: Pass request to the authenticate function
self.user = authenticate(email=email, password=password, request=request)
return self.user
def validate(self, attrs):
password = attrs.get('password')
email = attrs.get('email')
request = self.context.get('request') # <<=== Grab request
self.user = self._authenticate_user_email(email, password, request)
# All error handling should be done by this code line
refresh = self.get_token(self.user)
data = {}
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
return data
views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from authy.serializers import MyTokenObtainPairSerializer
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
urls.py
from authy.views import MyTokenObtainPairView
url(r'^/auth/api/token/$', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
It is also worth mentioning that the simple jwt lib uses the authenticate function, however it does not pass the request parameter. Therefore you need call authenticate, get_token and return data object yourself.
In addition, if you have extended the AbstractBaseUser model of django. And set the USERNAME_FIELD. Then use the param username instead of email. E.g: authenticate(username=email, password=password, request=request)
Use this:
from axes.backends import AxesBackend
class MyBackend(AxesBackend)
def authenticate(self, request=None, *args, **kwargs):
if request:
return super().authenticate(request, *args, **kwargs)
This would skip the AxesBackend in AUTHENTICATION_BACKENDS if the request is unset and would weaken your security setup.
source: https://github.com/jazzband/django-axes/issues/478

How can I use `email` in "django-rest-framework-simplejwt" instead of `username` to generate token?

In django-rest-framework-simplejwt plugin username and password are used by default. But I wanted to use email instead of username. So, I did like below:
In serializer:
class MyTokenObtainSerializer(Serializer):
username_field = User.EMAIL_FIELD
def __init__(self, *args, **kwargs):
super(MyTokenObtainSerializer, self).__init__(*args, **kwargs)
self.fields[self.username_field] = CharField()
self.fields['password'] = PasswordField()
def validate(self, attrs):
# self.user = authenticate(**{
# self.username_field: attrs[self.username_field],
# 'password': attrs['password'],
# })
self.user = User.objects.filter(email=attrs[self.username_field]).first()
print(self.user)
if not self.user:
raise ValidationError('The user is not valid.')
if self.user:
if not self.user.check_password(attrs['password']):
raise ValidationError('Incorrect credentials.')
print(self.user)
# Prior to Django 1.10, inactive users could be authenticated with the
# default `ModelBackend`. As of Django 1.10, the `ModelBackend`
# prevents inactive users from authenticating. App designers can still
# allow inactive users to authenticate by opting for the new
# `AllowAllUsersModelBackend`. However, we explicitly prevent inactive
# users from authenticating to enforce a reasonable policy and provide
# sensible backwards compatibility with older Django versions.
if self.user is None or not self.user.is_active:
raise ValidationError('No active account found with the given credentials')
return {}
#classmethod
def get_token(cls, user):
raise NotImplemented(
'Must implement `get_token` method for `MyTokenObtainSerializer` subclasses')
class MyTokenObtainPairSerializer(MyTokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super(MyTokenObtainPairSerializer, self).validate(attrs)
refresh = self.get_token(self.user)
data['refresh'] = text_type(refresh)
data['access'] = text_type(refresh.access_token)
return data
In view:
class MyTokenObtainPairView(TokenObtainPairView):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
"""
serializer_class = MyTokenObtainPairSerializer
And it works!!
Now my question is, how can I do it more efficiently? Can anyone give suggestion on this? Thanks in advance.
This answer is for future readers and therefore contains extra information.
In order to simplify the authentication backend, you have multiple classes to hook into. I would suggest to do option 1 (and optionally option 3, a simplified version of yours) below. Couple of notes before you read on:
Note 1: django does not enforce email as required or being unique on user creation (you can override this, but it's off-topic)! Option 3 (your implementation) might therefore give you issues with duplicate emails.
Note 1b: use User.objects.filter(email__iexact=...) to match the emails in a case insensitive way.
Note 1c: use get_user_model() in case you replace the default user model in future, this really is a life-saver for beginners!
Note 2: avoid printing the user to console. You might be printing sensitive data.
As for the 3 options:
Adjust django authentication backend with f.e. class EmailModelBackend(ModelBackend) and replace authenticate function.
Does not adjust token claims
Not dependent on JWT class/middleware (SimpleJWT, JWT or others)
Also adjusts other authentication types (Session/Cookie/non-API auth, etc.)
The required input parameter is still username, example below. Adjust if you dont like it, but do so with care. (Might break your imports/plugins and is not required!)
Replace django authenticate(username=, password=, **kwarg) from django.contrib.auth
Does not adjust token claims
You need to replace token backend as well, since it should use a different authentication, as you did above.
Does not adjust other apps using authenticate(...), only replaces JWT auth (if you set it up as such)
parameters is not required and therefore this option is less adviced).
Implement MyTokenObtainPairSerializer with email as claim.
Now email is sent back as JWT data (and not id).
Together with option 1, your app authentication has become username agnostic.
Option 1 (note that this also allows username!!):
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
class EmailorUsernameModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
try:
user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password):
return user
return None
Option 2:
Skipped, left to reader and not adviced.
Option 3:
You seem to have this covered above.
Note: you dont have to define MyTokenObtainPairView, you can use TokenObtainPairView(serializer_class=MyTokenObtainPairSerializer).as_view() in your urls.py. Small simplification which overrides the used token serializer.
Note 2: You can specify the identifying claim and the added data in your settings.py (or settings file) to use email as well. This will make your frontend app use the email for the claim as well (instead of default user.id)
SIMPLE_JWT = {
'USER_ID_FIELD': 'id', # model property to attempt claims for
'USER_ID_CLAIM': 'user_id', # actual keyword in token data
}
However, heed the uniqueness warnings given by the creators:
For example, specifying a "username" or "email" field would be a poor choice since an account's username or email might change depending on how account management in a given service is designed.
If you can guarantee uniqueness, you are all set.
Why did you copy and paste so much instead of subclassing? I got it to work with:
# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainSerializer
class EmailTokenObtainSerializer(TokenObtainSerializer):
username_field = User.EMAIL_FIELD
class CustomTokenObtainPairSerializer(EmailTokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data["refresh"] = str(refresh)
data["access"] = str(refresh.access_token)
return data
And
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
And of course
#urls.py
from rest_framework_simplejwt.views import TokenRefreshView
from .views import EmailTokenObtainPairView
url("token/", EmailTokenObtainPairView.as_view(), name="token_obtain_pair"),
url("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
The question has been a while but, I add +1 for #Mic's answer. By the way, wasn't it sufficient to update to TokenObtainPairSerializer only as following?:
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import (
TokenObtainPairSerializer, User
)
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.EMAIL_FIELD
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
Let summarize the above solutions:
1- Create two app by Django command. One for the new token and the other for the user:
python manage.py startapp m_token # modified token
python manage.py startapp m_user # modified user
2- In the m_token, create the serializers.py and override the serializer to replace username with email field:
# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, User
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.EMAIL_FIELD
3- In the m_token, override the view to replace the serializer with the new one:
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializer import CustomTokenObtainPairSerializer
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
4- In the m_token, create the urls.py and give the paths as follows:
# urls.py
from django.urls import path
from .views import TokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
urlpatterns = [
path(r'token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path(r'token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path(r'token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
5- In the m_user, override the user model as follows:
# models.py
from django.contrib.auth.models import AbstractUser
class MUser(AbstractUser):
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['username']
6- In the django project root, add AUTH_USER_MODEL = 'm_user.MUser' to setting.py.
I tested it in my project and it worked perfectly. I hope I did not miss anything. This way the swagger also shows "email" instead of "username" in the token parameters:
And in addition to #Mic's answer, remember to set USERNAME_FIELD = 'email' and may be REQUIRED_FIELDS = ['username'] in the User model.
For those using a custom User model, you simply can add those lines:
class User(AbstractUser):
...
email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
Then, in urls.py:
from rest_framework_simplejwt.views import TokenObtainPairView
urlpatterns = [
...
path('api/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
Using this code you can allow users to login using either username or email in the username field. You can add some lines to validate the email.
class TokenPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
raw_username = attrs["username"]
users = User.objects.filter(email=raw_username)
if(users):
attrs['username'] = users.first().username
# else:
# raise serializers.ValidationError("Only email is allowed!")
data = super(TokenPairSerializer, self).validate(attrs)
return data

Django Rest JWT login using username or email?

I am using django-rest-jwt for authentication in my app.
By default it user username field to autenticate a user but I want let the users login using email or username.
Is there any mean supported by django-rest-jwt to accomplish this.
I know the last option would be write my own login method.
No need to write a custom authentication backend or custom login method.
A Custom Serializer inheriting JSONWebTokenSerializer, renaming the 'username_field' and overriding def validate() method.
This works perfectly for 'username_or_email' and 'password' fields where the user can enter its username or email and get the JSONWebToken for correct credentials.
from rest_framework_jwt.serializers import JSONWebTokenSerializer
from django.contrib.auth import authenticate, get_user_model
from django.utils.translation import ugettext as _
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
User = get_user_model()
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
class CustomJWTSerializer(JSONWebTokenSerializer):
username_field = 'username_or_email'
def validate(self, attrs):
password = attrs.get("password")
user_obj = User.objects.filter(email=attrs.get("username_or_email")).first() or User.objects.filter(username=attrs.get("username_or_email")).first()
if user_obj is not None:
credentials = {
'username':user_obj.username,
'password': password
}
if all(credentials.values()):
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
else:
msg = _('Account with this email/username does not exists')
raise serializers.ValidationError(msg)
In urls.py:
url(r'{Your url name}$', ObtainJSONWebToken.as_view(serializer_class=CustomJWTSerializer)),
Building on top of Shikhar's answer and for anyone coming here looking for a solution for rest_framework_simplejwt (since django-rest-framework-jwt seems to be dead, it's last commit was 2 years ago) like me, here's a general solution that tries to alter as little as possible the original validation from TokenObtainPairSerializer:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomJWTSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
credentials = {
'username': '',
'password': attrs.get("password")
}
# This is answering the original question, but do whatever you need here.
# For example in my case I had to check a different model that stores more user info
# But in the end, you should obtain the username to continue.
user_obj = User.objects.filter(email=attrs.get("username")).first() or User.objects.filter(username=attrs.get("username")).first()
if user_obj:
credentials['username'] = user_obj.username
return super().validate(credentials)
And in urls.py:
url(r'^token/$', TokenObtainPairView.as_view(serializer_class=CustomJWTSerializer)),
Found out a workaround.
#permission_classes((permissions.AllowAny,))
def signin_jwt_wrapped(request, *args, **kwargs):
request_data = request.data
host = request.get_host()
username_or_email = request_data['username']
if isEmail(username_or_email):
# get the username for this email by model lookup
username = Profile.get_username_from_email(username_or_email)
if username is None:
response_text = {"non_field_errors":["Unable to login with provided credentials."]}
return JSONResponse(response_text, status=status.HTTP_400_BAD_REQUEST)
else:
username = username_or_email
data = {'username': username, 'password':request_data['password']}
headers = {'content-type': 'application/json'}
url = 'http://' + host + '/user/signin_jwt/'
response = requests.post(url,data=dumps(data), headers=headers)
return JSONResponse(loads(response.text), status=response.status_code)
I check that whether the text that I received is a username or an email.
If email then I lookup the username for that and then just pass that to /signin_jwt/
authentication.py
from django.contrib.auth.models import User
class CustomAuthBackend(object):
"""
This class does the athentication-
using the user's email address.
"""
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
settings.py
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'app_name.authentication.CustomAuthBackend',
]
How it works:
If user try to authenticate using their username django will look at the ModelBackend class. However, if the user adds its email instead, django will try ModelBackend but will not find the logic needed, then will try the CustomAuthBackend class making it work the authentication.
Alternatively, this new DRF Auth project dj-rest-auth seems to provide support for log in by username or email through djangorestframework-simplejwt.
dj-rest-auth works better for authentication and authorization. By default dj-rest-auth provides - username, email and password fields for login. User can provide email and password or username and password. Token will be generated, if the provided values are valid.
If you need to edit these login form, extend LoginSerializer and modify fields. Later make sure to add new custom serializer to settings.py.
REST_AUTH_SERIALIZERS = {
'LOGIN_SERIALIZER': 'yourapp.customlogin_serializers.CustomLoginSerializer'
}
Configuring dj-rest-auth is bit tricky, since it has an open issue related to the refresh token pending. There is workaround suggested for that issue, so you can follow below links and have it configured.
https://medium.com/geekculture/jwt-authentication-in-django-part-1-implementing-the-backend-b7c58ab9431b
https://github.com/iMerica/dj-rest-auth/issues/97
If you use the rest_framework_simplejwt this is a simple mode. views.py
from rest_framework_simplejwt.tokens import RefreshToken
from django.http import JsonResponse
from rest_framework import generics
class EmailAuthToken(generics.GenericAPIView):
def post(self, request):
user_data = request.data
try:
user = authenticate(request, username=user_data['username_or_email'], password=user_data['password'])
if user is not None:
login(request, user)
refresh = RefreshToken.for_user(user)
return JsonResponse({
'refresh': str(refresh),
'access': str(refresh.access_token),
}, safe=False, status=status.HTTP_200_OK)
else:
return JsonResponse({
"detail": "No active account found with the given credentials"
}, safe=False, status=status.HTTP_200_OK)
except:
return Response({'error': 'The format of the information is not valid'}, status=status.HTTP_401_UNAUTHORIZED)

Getting a related model class from auth.models.User

I'm working on an app that has three "profile" models types. Each of these models has a foreign key to the contrib.auth.models User model.
I would like to have a single sign on for each model type and provide a redirect via classmethod depending on which "profile" model type is related to the logged in user.
Here's some pseudo-code to illustrate what I would like to do:
from django.contrib.auth import authenticate, login
from django.http import HttpResponse, HttpresponseRedirect
from lib.forms import Loginform #extends the built-in AuthenticationForm
def login_user(request):
if request.method == 'POST':
form = LoginForm(data=request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
user = authenticate(username=cleaned_data.get('username'), \
password=cleaned_data.get('password'))
if user:
login(request, user)
#Determine the content type of the model related to the user
#get the correct redirect value from an #classmethod
#called: after_login_redirect for that type and...
return HttpResponseRedirect(klass.after_login_redirect())
else:
response = form.errors_as_json()
return HttpResponse(json.dumps(response, ensure_ascii=False), \
mimetype='application/json')
Is it possible to leverage the ContentTypes framework to do this? or am I better off just writing a class resolver that loops over an array of the "profile" classes? I'm not seeing a way I can do this with ContentTypes from the User class, unless someone knows a workaround.
Thanks in advance,
Brandon
[SOLVED]
So, what I ended up doing was creating a UserContentType junction model that looks like this:
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
class UserContentType(models.Model):
user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType)
Then, I made a pre-save signal to fire a get or create if the instance of one of the three "profile" models I have doesn't have an id:
from django.contrib.contenttypes.models import ContentType
from lib.models import UserContentType
def set_content_type_for_user(sender, **kwargs):
instance = kwargs.get('instance')
content_type = ContentType.objects.get_for_model(instance)
user = instance.user
if not instance.id:
user_content_type, \
created = UserContentType.objects.get_or_create(user=user, \
content_type=content_type, \
defaults={'user' : user, 'content_type' : content_type})
Then, in my custom login view which is posted to via ajax, I can get the content type associated with the User instance and get the URL to redirect to from the 'after_login_redirect()' classmethod on that content type. :)
def login_user(request):
if request.method == 'POST':
form = LoginForm(data=request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
user = authenticate(username=cleaned_data.get('username', ''),
password=cleaned_data.get('password', ''))
if user:
login(request, user)
user_content_type = UserContentType.objects.get(user=user)
redirect_path =
user_content_type.content_type.\
model_class().after_login_redirect_path()
response = {'redirect_path' : redirect_path}
else:
response = form.errors_as_json()
return HttpResponse(json.dumps(response, \
ensure_ascii=False), mimetype='application/json')
This way, I don't have to monkeypatch User. I hope this helps someone out. If there's a way I can improve on this, I'd love to hear some ideas.
Thanks,
Brandon