Dynamic USERNAME_FIELD djangorestframework and simplejwt - django

I am working on a authentication application where user's can login via
(email or mobile) and (password or otp)
Framework/ Library Used
Django Rest Framework and djangorestframework-simplejwt
I am trying to add multiple claims in the jwt token getting generated.
Below is my LoginView and LoginSerializer.
View
class Login(TokenObtainPairView):
serializer_class = LoginSerializer
Serializer
class LoginSerializer(TokenObtainPairSerializer):
mobile = serializers.CharField(allow_blank=True)
email = serializers.EmailField(allow_blank=True)
password = serializers.CharField(allow_blank=True)
otp = serializers.CharField(allow_blank=True)
#classmethod
def get_token(cls, user):
token = super().get_token(user)
token['name'] = user.first_name
return token
def validate(self, attrs):
mobile = attrs.get("mobile", None)
email = attrs.get("email", None)
password = attrs.get("password", None)
otp = attrs.get("otp", None)
user = authenticate(mobile=mobile, email=email, password=password, otp=otp)
if user is None and self.password:
raise serializers.ValidationError(
detail="Incorrect Username or Password.", code=HTTP_401_UNAUTHORIZED
)
if user.is_active:
refresh = self.get_token(user)
data = dict()
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
return data
if user.is_locked:
raise serializers.ValidationError(
detail="Account Locked. Contact Support.", code=HTTP_423_LOCKED
)
raise serializers.ValidationError(
detail="User Account is Deactivated.",
code=HTTP_401_UNAUTHORIZED
)
But i am getting email can't be blank error when sending a valid phone number and password in request. This is because of TokenObtainPairSerializer which checks for User.USERNAME_FIELD (which in my case is email.)
How can i handle this situation or get it working ?

Related

Django Rest Framework Testing

I have a LoginSerializer that has the block of code as below
def validate(self, attrs):
username = attrs.get('username', '')
password = attrs.get('password', '')
user = auth.authenticate(username=username, password=password)
if user:
if user.is_active is False:
raise AuthenticationFailed(
'Account is disabled, contact admin')
if not user.is_verified:
raise AuthenticationFailed('Email is not verified')
return {
'username': user.username,
'firstname': user.firstname,
'lastname': user.lastname,
'role': user.role,
'tokens': user.tokens
}
else:
raise AuthenticationFailed('Invalid credentials, try again')
and a test case as below;
class UserLoginTest(BaseTest):
def test_inactive_user_can_login(self):
self.client.post(
self.register_public, data=valid_user, format='json')
user = User.objects.get(username=valid_user['username'])
user.is_verified = True
user.is_active = False
user.save()
response = self.client.post(
self.login_url, valid_login_user, format='json')
print(response.data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
When I run the test with is_active = False I get Invalid credentials, try again. Why is it that when is_active=False the user is not found even though the user is there? Same with when I try to login from swagger.
EDIT
I have read that I can use
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.AllowAllUsersModelBackend']
then I will be able to check for is_active manually otherwise django handles that and returns a None. What are the dangers of doing this?
This happens because you are using .authenticate() which by default goes through all backends listed in AUTHENTICATION_BACKENDS. If not listed in settings, django.contrib.auth.backends.ModelBackend is used, this backend verifies .is_active() field:
...
def user_can_authenticate(self, user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_active = getattr(user, "is_active", None)
return is_active or is_active is None
...
That snippet runs before your verification and returns None to user variable. So, it fails on if user condition (user is None) thus raise AuthenticationFailed('Invalid credentials, try again')
About AllowAllUsersModelBackend the only thing it does is override this method to allow inactive users to login:
class AllowAllUsersModelBackend(ModelBackend):
def user_can_authenticate(self, user):
return True
The only risk i can see, is using this backend and not checking .is_active() field manually. Unless if it is intended that inactive users can login into your system.

DRF, allauth and dj-rest-auth - Authenticate with inactive users return None instead the user object

I'm using all-auth and dj-rest-auth to implement registration and login via email. Everything work fine but when making test, inactive users return invalid credentials message instead inactive account message. In the LoginSerializer, it seems that django.contrib.auth authenticate method doesn't return the user, but None. Here is the code:
settings.py
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.AllowAllUsersModelBackend",
"allauth.account.auth_backends.AuthenticationBackend"
]
REST_AUTH_REGISTER_SERIALIZERS = {
'REGISTER_SERIALIZER': 'user.serializers.RegisterSerializer',
}
REST_AUTH_SERIALIZERS = {
'LOGIN_SERIALIZER': 'user.serializers.LoginSerializer',
'USER_DETAILS_SERIALIZER': 'user.serializers.UserDetailSerializer',
}
serializers.py
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField(required=True, allow_blank=False)
password = serializers.CharField(style={'input_type': 'password'})
def authenticate(self, **kwargs):
return authenticate(self.context['request'], **kwargs)
def _validate_email(self, email, password):
user = None
if email and password:
user = self.authenticate(email=email, password=password)
else:
msg = _('Must include "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
user = None
if 'allauth' in settings.INSTALLED_APPS:
from allauth.account import app_settings
# Authentication through email
if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:
user = self._validate_email(email, password)
# Did we get back an inactive user?
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.ValidationError(msg)
else:
msg = _('Unable to log in with provided credentials.')
raise exceptions.ValidationError(msg)
# If required, is the email verified?
if 'dj_rest_auth.registration' in settings.INSTALLED_APPS:
from allauth.account import app_settings
if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:
try:
email_address = user.emailaddress_set.get(email=user.email)
except:
raise serializers.ValidationError(_('E-mail is not registered.'))
else:
if not email_address.verified:
raise serializers.ValidationError(_('E-mail is not verified.'))
attrs['user'] = user
return attrs
tests.py
########################################################################
# LOG IN WITH INACTIVE USER
login_data = {
'email': 'inactive#inactive.com',
'password': '9I8u7Y6t5R4e'
}
response = self.client.post('http://localhost:8000/api/auth/login/', login_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
expected_error = {
'non_field_errors': 'User account is disabled.'
}
response_error = {
'non_field_errors': response.data['non_field_errors'][0]
}
self.assertEqual(response_error, expected_error)
Is there something that I missing?
Thanks in advance.
In case anyone is interested, I found the problem: the allauth authentication backend override the django model backend. In order to resolve this, I create a class that inherit from allauth backend and add the function that allow all users to log in:
backend.py
from allauth.account.auth_backends import AuthenticationBackend
class AllowAllUsersModelBackend(AuthenticationBackend):
def user_can_authenticate(self, user):
return True
Then add it to settings:
settings.py
AUTHENTICATION_BACKENDS = [
"user.backends.AllowAllUsersModelBackend",
]

How to store simplejwt token into database

I'm using django-rest-framework-simplejwt for user registration.
Following this tutorial enter link description here
I code like following:
class RegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(
style={'input_type': 'password'}, write_only=True,
)
password2 = serializers.CharField(
style={'input_type': 'password'},max_length=20
)
tokens = serializers.SerializerMethodField()
class Meta:
model = UserProfile
fields = ['username', 'email', 'password', 'password2', 'tokens']
def get_tokens(self, user):
user = UserProfile(
email=self.validated_data['email'],
username=self.validated_data['username']
)
password = self.validated_data['password']
password2 = self.validated_data['password2']
if password != password2:
raise serializers.ValidationError({'password': 'Passwords must match.'})
user.set_password(password)
tokens = RefreshToken.for_user(user)
refresh = text_type(tokens)
access = text_type(tokens.access_token)
data = {
"refresh": refresh,
"access": access
}
return data
def save(self):
user = UserProfile(
email=self.validated_data['email'],
username=self.validated_data['username']
)
password = self.validated_data['password']
password2 = self.validated_data['password2']
if password != password2:
raise serializers.ValidationError({'password': 'Passwords must match.'})
user.set_password(password)
user.save()
return user
in view:
class UserCreateView(generics.CreateAPIView):
'''create user'''
serializer_class = RegistrationSerializer
The problem is each time I create a user,I can get the return of the 2 two tokens,however in data base I can't find the token.
So I guess I didn't store them,so should I store the token?
JWT can be used for database-less authentication. because it encodes data needed for authentication in tokens. Your app will be able to authenticate users after decoding tokens with data embedded in it.
But if you want to store tokens in simplejwt you can use OutstandingingToken model which is implemented in simplejwt to store tokens in database.
Before using OutstandingToken, make sure you put rest_framework_simplejwt.token_blacklist in your INSTALLED_APPS list of your project settings.

How to log a user in by getting data from an API?

I am writing an API view where am accessing API to POST email and password to the address and fetch response.So i want if response is 200 or repose message is 'Success' then to login with available email and password datas, but i'm not able to do so. How to achieve such?
class ApiLoginView(TemplateView):
template_name = 'index.html'
def post(self,request):
email = request.POST.get('login-email')
print(email)
password = request.POST.get('login-password')
print(password)
API_KEY = '*********************'
API_URL = 'http://devstudio.com/rest/storeLogin'
parameter = {
'authToken':API_KEY,
'email':email,
'password':password,
}
r = session.post(url = API_URL, params=parameter)
return HttpResponse(r)
if you are using djagno's built-in auth and user system then you can use something like this
from django.contrib.auth import authenticate, login
class ApiLoginView(TemplateView):
template_name = 'index.html'
def post(self,request):
email = request.POST.get('login-email')
print(email)
password = request.POST.get('login-password')
print(password)
API_KEY = '*********************'
API_URL = 'http://devstudio.com/rest/storeLogin'
parameter = {
'authToken':API_KEY,
'email':email,
'password':password,
}
r = session.post(url = API_URL, params=parameter)
if r.status_code==200:
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
else:
# Return an 'invalid login' error message.
return HttpResponse(r)
for reference check this https://docs.djangoproject.com/en/2.1/topics/auth/default/#topic-authorization
hope this helps..

Allauth Social Login with DRF JWT

I am using DRF, DRF-JWT, Allauth and Res-auth, and djangorestframework-jwt-refresh-token in my Django Application.
I have a custom JWT Register Serializer to collect some additional user info and create and create a refresh-token that is used to refresh expired JWT tokens. We have that working across back-end and iOS Application with no problems for email signup. I am now trying to implement the JWT with the sociallogin element of allauth in particular Facebook as a provider.
I can create a refresh token against a Facebook user by overriding the DefaultSocialAccountAdapter but I'm struggling to return a JSON response with a JWT with said refresh token to mobile client.
This creates refresh token:
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def save_user(self, request, sociallogin, form):
user = super(CustomSocialAccountAdapter, self).save_user(request, sociallogin, form)
app = 'users'
user.refresh_tokens.create(app=app)
return user
I can create JWT manually with this:
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
I'm just having difficulty putting it all together, should I be overriding the adapter or using pre_social_login signal.
Any pointers appreciated.
I went with the following to return the long lived refresh token along with avatar url on the Facebook Login:
class CustomJWTSerializer(JWTSerializer):
"""
OVERIDE JWTSerializer Base Serializer for JWT authentication to
add long refresh_token to returned JSON
"""
refresh_token = serializers.CharField()
avatar_url = serializers.CharField()
class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter
def process_login(self):
get_adapter(self.request).login(self.request, self.user)
user = self.request.user
app = 'users'
try:
refresh_token = user.refresh_tokens.get(app=app).key
except RefreshToken.DoesNotExist:
refresh_token = None
if refresh_token == None:
app = 'users'
user.refresh_tokens.create(
app=app
)
preferred_avatar_size_pixels = 256
facebook_social_account = SocialAccount.objects.get(user=user)
uid = facebook_social_account.uid
picture_url = "http://graph.facebook.com/{0}/picture?width={1}&height={1}".format(
uid, preferred_avatar_size_pixels)
profile = Profile(user=user, avatar_url=picture_url)
profile.save()
def get_response(self):
serializer_class = CustomJWTSerializer
refresh_token = RefreshToken.objects.get(user=self.user)
profile = Profile.objects.get(user=self.user)
avatar_url = profile.avatar_url
if getattr(settings, 'REST_USE_JWT', False):
data = {
'user': self.user,
'token': self.token,
'refresh_token': refresh_token,
'avatar_url': avatar_url
}
serializer = serializer_class(instance=data,
context={'request': self.request})
else:
serializer = serializer_class(instance=self.token,
context={'request': self.request})
return Response(serializer.data, status=status.HTTP_200_OK)
I don't know if this was the proper way to do this, but it works for now.