I'm trying to write a unit test for a login endpoint in Django using as much of the built in functionality as possible.
There are existing tests that confirm that the account create endpoint is functioning properly.
In the login view, however, the check_password() function will return True for this test, but the authenticate() function returns None.
Is it safe to use the check_password() function instead?
Otherwise, how do I update this code to use the authenticate() function?
accounts.py
class Account(AbstractUser):
username = models.CharField(max_length=150, unique=True, null=False, default='')
password = models.CharField(max_length=100)
...
REQUIRED_FIELDS = ['username', 'password']
class Meta:
app_label = 'accounts'
db_table = 'accounts_account'
objects = models.Manager()
test_login.py
def test_login(self):
# Create account
request_create = self.factory.post('/accounts/account',
self.request_data,
content_type='application/json')
view = AccountView.as_view()
response_create = view(request_create)
# Login account
request_login = self.factory.post('/accounts/login',
self.request_data,
content_type='application/json')
view = LoginView.as_view()
response = view(request_login)
views.py
class LoginView(View):
def post(self, request):
r = json.loads(request.body)
username = r.get('username')
password = r.get('password')
cp = check_password(password, Account.objects.get(username=username).password)
user = authenticate(username=username, password=password)
P.S. I've checked this thread and is_active is set to true.
It is safe. The main difference is that by using check_password() you are manually checking an User from an authentication backend, thus you have to retrieve the user object and compare its password with the plain text like you do at:
check_password(password, Account.objects.get(username=username).password)
While, with authenticate() it is possible to check credentials against several authentication backends. Meaning that with the former you couldn't hook your application with other authentication sources
You are missing some code in tests.py and views.py. That being said, here is a full test of this LoginView:
tests.py
class TestClientLogin(TestCase):
def setUp(self):
User = get_user_model()
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='test_user',
email='test_user#example.com',
password='super_secret'
)
def test_user_login(self):
request_data = {'username': 'test_user', 'password': 'super_secret'}
request = self.factory.post(
'/accounts/login/',
request_data ,
content_type='application/json'
)
response = LoginView.as_view()(request)
data = json.loads(response.content)
self.assertEqual(self.user.username, data['username'])
self.assertEqual(response.status_code, 200)
views.py
class LoginView(View):
def post(self, request):
data = json.loads(request.body)
username = data.get('username')
password = data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
return JsonResponse({'username': user.username})
else:
return JsonResponse({'username': None})
If you want use as much of the built in functionality as possible:
settings.py:
AUTH_USER_MODEL = 'accounts.Account'
accounts/models.py:
class Account(AbstractUser):
# you dont need to define username or password again
...
REQUIRED_FIELDS = ['username', 'password']
class Meta:
# you don't need to define default app_label, db_table
objects = UserManager() # otherwise don't work "python manage.py createsuperuser"
accounts/views.py:
from django.contrib.auth.views import LoginView
class ClientLogin(LoginView):
pass
accounts/tests.py:
from django.test import TestCase, Client
from django.utils.crypto import get_random_string
class TestClientLogin(TestCase):
def setUp(self):
User = get_user_model()
self.username, self.password = get_random_string(20), get_random_string(20) # don't use constants in tests
self.user = User.objects.create_user(username=self.username, password=self.password )
def test_user_login(self):
request_data = {'username': self.username, 'password': self.password}
response = Client().post('/accounts/login/', request_data)
self.assertEqual(response.status_code, 302)
BUT:
you don't need to test ClientLogin, you don't have any code in view.
Related
I'm a newbie in django and I'm having some problems, what I'm trying to do is a simple login system in Django with a custom backend and using postgresql as the main db.
The problem is, my authentication and login function is apparently working normally but the user is not actually logged in, I wrote a custom message to let me know when the user is logged in and my index is protected against anonymous user, so I basically can't access.
This code from my views.py
#login_required(login_url='signin')
def index(request):
return render(request, 'index.html')
def signin(request):
now = datetime.now()
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
print(username, password)
user = UserBackend.authenticate(UserBackend(), username=username, password=password)
if user is not None:
login(request, user, backend='core.backends.UserBackend')
print('Logged In') #### This print/checking is working fine!
return redirect('/')
#return render(request, 'index.html')
else:
messages.info(request, 'Invalid Username or Password! Try again')
return redirect("signin")
#else:
return render(request,'signin.html')
#user = auth.authenticate(username=username, password=password)
return render(request, 'signin.html')
This is my user class from models.py
class user(AbstractBaseUser):
id = models.AutoField(primary_key=True)
username = models.TextField()
real_name = models.TextField()
email = models.TextField()
password = models.TextField()
description = models.TextField()
last_login = models.DateField()
created_at = models.DateField()
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
class Meta:
managed = False
db_table = 'user'
def __str__(self) -> str:
return self.username
And my user backend from backends.py
class UserBackend(ModelBackend):
def authenticate(self, **kwargs):
username = kwargs['username']
password = kwargs['password']
print('user: ', username)
print('pass: ', password)
#try:
user_ = user.objects.get(username=username)
try:
print(user_.check_password(password))
if user_.check_password(password) is True:
return user_
except user.DoesNotExist:
pass
def get_user(self, user_id):
try:
return user.objects.get(pk=user_id)
except user.DoesNotExist:
return None
I tried changing the return on views.py to something like
return redirect(request.GET.get('next'))
but still not working :(
what should I do?
The key point, is how you are using your Backend. Although, lets start from the beginning...
Starting with your models, you are unnecessarily overwriting a lot of fields. Django's AbstractUser has these fields you are creating. In fact, the model contains the following fields with validators where appropriate:
username
first_name
last_name
email
is_staff as default false
is_active as default true
date_joined as default timezone.now()
last_login
a built-in function get_full_name (equivalent to your real_name field)
Also, id = models.AutoField(primary_key=True) is not necessary Django by default creates this kind of field automatically for every model. Most of the times used when you want a different kind of field as PK. Thus a simple model, would fulfill your requirements:
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
description = models.TextField()
The backend is pretty straight forward, check credentials and return an User object instance if everything is correct. ModelBackend already has a get_user method defined.
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.hashers import check_password
from django.contrib.auth import get_user_model
User = get_user_model()
class UserBackend(ModelBackend):
def authenticate(self, request, **kwargs):
username = kwargs['username']
password = kwargs['password']
try:
user = User.objects.get(username=username)
pwd_valid = check_password(password, user.password)
if pwd_valid:
return user
else:
return None
except User.DoesNotExist:
return None
Now, about on how to use it. Quotting the documentation:
Behind the scenes, Django maintains a list of “authentication
backends” that it checks for authentication. When somebody calls
django.contrib.auth.authenticate() Django tries authenticating across
all of its authentication backends. If the first authentication method
fails, Django tries the second one, and so on, until all backends have
been attempted.
The list of authentication backends to use is specified in the
AUTHENTICATION_BACKENDS setting. This should be a list of Python path
names that point to Python classes that know how to authenticate.
These classes can be anywhere on your Python path.
So, first we need to add AUTHENTICATION_BACKENDS in settings.py:
AUTH_USER_MODEL = 'core.User'
LOGIN_URL = '/signin'
AUTHENTICATION_BACKENDS = [
'core.auth.backends.UserBackend',
'django.contrib.auth.backends.ModelBackend'
]
And lastly in views.py, we call the functions exactly as you would in a normal login function:
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from django.contrib import messages
# Create your views here.
#login_required
def index(request):
return render(request, 'index.html')
def signin(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Confirm that backend is UserBackend
print(user.backend)
return redirect('core:index')
else:
messages.info(request, 'Invalid Username or Password! Try again')
return redirect("core:signin")
return render(request, 'signin.html')
I am very new to Django Rest Framework and am confused about coding the following task: when a user on the front-end enters an email address, without any other user information, the API should determine whether or not the email already exists. Likewise, when the user enters a username, without any other user information, the API should determine whether or not the username already exists. This is so the user has feedback on whether their email or username is valid before proceeding to the next sign up stage.
I am aware of the existence of validators, but I don't understand how to use them on partial data.
Here is my serializers.py class
class CustomUserSerializer(serializers.ModelSerializer):
email = serializers.EmailField(required=True)
username = serializers.CharField(min_length=2)
password = serializers.CharField(min_length=8, write_only=True)
first_name = serializers.CharField(min_length=2)
last_name = serializers.CharField(min_length=2)
class Meta:
model = CustomUser
fields = ('pk', 'email', 'username', 'password', 'created_at', 'first_name', 'last_name')
extra_kwargs = {'password': {'write_only': True}}
def validate_email(self, value):
if CustomUser.objects.filter(email=value).exists():
raise serializers.ValidationError("An account already exists with that email address.")
return value
def validate_username(self, value):
if CustomUser.objects.filter(username=value).exists():
raise serializers.ValidationError("An account aready exists with that username.")
return value
def create(self, validated_data):
password = validated_data.pop('password', None)
instance = self.Meta.model(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
And here is my views.py class
class CustomUserCreate(APIView):
permission_classes = (permissions.AllowAny,)
authentication_classes = ()
def post(self, request, format='json'):
"""
Receives an HTTP request. Serializes and saves a user object.
"""
serializer = CustomUserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
json = serializer.data
return Response(json, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CustomUserGet(APIView):
def get_user(self, username):
try:
return CustomUser.objects.get(username=username)
except CustomUser.DoesNotExist:
raise Http404
def get(self, request, format='json'):
user = self.get_user(request.user.username)
serializer = CustomUserSerializer(user)
return Response(serializer.data)
class LogoutAndBlacklistRefreshTokenForUserView(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request):
try:
refresh_token = request.data['refresh_token']
token = RefreshToken(refresh_token)
token.blacklist()
return Response(status=status.HTTP_205_RESET_CONTENT)
except:
return Response(status=status.HTTP_400_BAD_REQUEST)
CustomUser is simply an implementation of AbstractUser.
Again, this successfully validates when a user is created with complete data. However, I want to validate on partial data, such as a lone email address and lone username without the presence of other attributes such as first_name, last_name, etc. What is the proper way of approaching this?
I have a custom passwordless user model built in django 1.11. user model looks like this
class User(models.Model):
email = models.EmailField(primary_key=True)
REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
is_anonymous = False
is_authenticated = True
It's a custom user model and depends on a custom auth backend, given like this
class PasswordlessAuthenticationBackend():
def authenticate(self, uid):
try:
token = Token.objects.get(uid=uid)
return User.objects.get(email=token.email)
except User.DoesNotExist:
return User.objects.create(email=token.email)
except Token.DoesNotExist:
return None
def get_user(self, email):
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None
The auth is registered and working fine. The token is just this
class Token(models.Model):
email = models.EmailField()
uid = models.CharField(default=uuid.uuid4, max_length=40)
The problem is, when I try to call auth.login in my TestCase, it always throws this error:
ValueError: The following fields do not exist in this model or are m2m fields: last_login
What are m2m fields? How and where do I specify this last_login?
Edit:
The failing test looks like this:
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.contrib import auth
from accounts.models import Token
User = get_user_model()
email = 'test#testing.com'
class UserModelTestcase(TestCase):
def test_user_is_valid_with_email_only(self):
user = User(email=email)
user.full_clean()
def test_email_is_primary_key(self):
user = User(email=email)
self.assertEqual(user.pk,email)
def test_links_user_with_auto_generated_uid(self):
token1 = Token.objects.create(email=email)
token2 = Token.objects.create(email=email)
self.assertNotEqual(token1.uid, token2.uid)
def test_no_problem_with_auth_login(self):
user = User.objects.create(email=email)
user.backend = ''
request = self.client.request().wsgi_request
auth.login(request, user) #should not raise, fails!
this link Error about Django custom authentication and login?
has an in-depth explanation about your problem an has multiple solutions you can pick.
I have used Django Rest Framework for Rest API and django-oauth-toolkit for token based authentication. I have designed and api for user registration. When user is registered, a token is generated and saved to the database. I want user login from that token. I mean a token based authentication because i want to develop a mobile application. I can get access_token using curl when sending request for logging in but how do i implement in view so that app sends a post request to 127.0.0.1:8000/o/token asking for the token so that the request contains username, password, client_id and client_secret. The server then receives the credentials and if they are valid it returns the access_token. The rest of the time it should query the server using that token.
views.py
class UserLoginAPI(APIView):
permission_classes = [AllowAny]
serializer_class = UserLoginSerializer
def post(self, request, *args, **kwargs):
access_token = AccessToken.objects.get(token=request.POST.get('access_token'), expires__gt=timezone.now()) # error is shown here. I get None
data = request.data
serializer = UserLoginSerializer(data=data)
if serializer.is_valid(raise_exception=True):
new_data = serializer.data
return Response(new_data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class UserCreateSerializer(ModelSerializer):
class Meta:
model = User
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
username = validated_data['username']
first_name = validated_data['first_name']
last_name = validated_data['last_name']
email = validated_data['email']
password = validated_data['password']
confirm_password = validated_data['password']
user_obj = User(
username = username,
first_name = first_name,
last_name = last_name,
email = email
)
user_obj.set_password(password)
user_obj.save()
if user_obj:
expire_seconds = oauth2_settings.user_settings['ACCESS_TOKEN_EXPIRE_SECONDS']
scopes = oauth2_settings.user_settings['SCOPES']
application = Application.objects.get(name="Foodie")
expires = datetime.now() + timedelta(seconds=expire_seconds)
access_token = AccessToken.objects.create(user=user_obj,
application=application,
token = generate_token(),
expires=expires,
scope=scopes)
return validated_data
class UserLoginSerializer(ModelSerializer):
# token = CharField(allow_blank=True, read_only=True)
username = CharField()
class Meta:
model = User
fields = [
'username',
'password',
# 'token',
]
extra_kwargs = {"password":
{"write_only": True}
}
So if you want a api to get token depend on username and password will look like this:
def get_token(request):
username = request.POST.get("username")
password = request.POST.get("password")
.... # other parameters
try:
user = User.objects.get(username=username, password=password)
except ObjectDoesNotExist:
return HttpResponse("Can't find this user")
else:
try:
access_token = AccessToken.objects.get(user=user)
except ObjectDoesNotExist:
return HttpResponse("Haven't set any token")
else:
return HttpResponse(access_token)
If you want to use DRF to handle this:
#api_view(['POST'])
def get_token(request):
# get token by query just like above
serializer = TokenSerializer(data=access_token.token) #you can pass more parameters to data if you want, but you also have to edit your TokenSerializer
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Your TokenSerializer:
class TokenSerializer(ModelSerializer):
class Meta:
model = AccessToken
field = (token,)
EDIT
It depends
Web, you post your username and password to login api, your browser store session in your cookies.
Mobile, you post your username and password to login api, server response the token then you store it in your mobile, maybe keychain when you are developing IOS app.And send it as http header when you want to access the server, how-can-i-get-all-the-request-headers-in-django
I am creating a an API for registering user based on oauth token. My app has functionality like registration and login, adding restaurant etc. I created user registration part but i get error while login. I want login based on token.
I have used django-oauth-toolkit for this and DRF.
What i have done is
Login based on token
class UserCreateAPI(CreateAPIView):
serializer_class = UserCreateSerializer
queryset = User.objects.all()
permission_classes = [AllowAny]
class UserLoginAPI(APIView):
permission_classes = [AllowAny]
serializer_class = UserLoginSerializer
def post(self, request, *args, **kwargs):
access_token = AccessToken.objects.get(token=request.POST.get('access_token'), expires__gt=timezone.now()) # error is shown here
data = request.data
serializer = UserLoginSerializer(data=data)
if serializer.is_valid(raise_exception=True):
new_data = serializer.data
return Response(new_data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
For creating user with a token
class UserCreateSerializer(ModelSerializer):
class Meta:
model = User
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
username = validated_data['username']
first_name = validated_data['first_name']
last_name = validated_data['last_name']
email = validated_data['email']
password = validated_data['password']
confirm_password = validated_data['password']
user_obj = User(
username = username,
first_name = first_name,
last_name = last_name,
email = email
)
user_obj.set_password(password)
user_obj.save()
if user_obj:
expire_seconds = oauth2_settings.user_settings['ACCESS_TOKEN_EXPIRE_SECONDS']
scopes = oauth2_settings.user_settings['SCOPES']
application = Application.objects.get(name="Foodie")
expires = datetime.now() + timedelta(seconds=expire_seconds)
access_token = AccessToken.objects.create(user=user_obj,
application=application,
token = generate_token(),
expires=expires,
scope=scopes)
return validated_data
class UserLoginSerializer(ModelSerializer):
# token = CharField(allow_blank=True, read_only=True)
username = CharField()
class Meta:
model = User
fields = [
'username',
'password',
# 'token',
]
extra_kwargs = {"password":
{"write_only": True}
}
I have excluded fields part and validation part for shortening the code
Am i doing the right way?
Try to use request.data instead of request.post in order to get the access_token(http://www.django-rest-framework.org/tutorial/2-requests-and-responses/):
access_token = AccessToken.objects.get(token=request.data.get('access_token'), expires__gt=timezone.now())
use request.DATA if you are using the Version-2 of DRF. request.data is for version-3
============UPDATE========================================
You should implement your own login procedure or modify the existing one; because when user log in, the access_token is not actually sent to the server.
The login procedure should look like this :
When the user enter his login and password, your app should send a post request to 127.0.0.1:8000/o/token asking for the token. The request should contains the username, password, client_id and client_secret.
the server then receives the credentials and if they are valid it returns the access_token.
The rest of the time you should query the server using that token.
But, i'm not sure that it is what you are actually doing.