Django unit testing - authenticated user check in model method - django

I am using Django 3.2
I am trying to test a model that checks if a user is authenticated. The model code looks something like this:
class Foo(Model):
def do_something(user, **kwargs):
if user.is_authenticated():
do_one_thing(user, **kwargs)
else:
do_another_thing(**kwargs)
My test code looks something like this:
from model_bakery import baker
from django.contrib.auth import authenticate, get_user_model
User = get_user_model()
class FooModelTest(TestCase):
def setUp(self):
self.user1 = baker.make(User, username='testuser_1', password='testexample1', email='user1#example.com', is_active = True, is_superuser=False)
baker.make(User, username='testuser_2', password='testexample2', email='admin#example.com', is_active = True, is_superuser=True)
self.unauthorised_user = authenticate(username='testuser_2', password='wrongone')
def test_user(self):
self.assertTrue(self.user1.is_validated) # why is this true? - I haven't "logged in" yet?
self.assertFalse(self.unauthorised.is_validated) # Barfs here since authenticate returned a None
So, How am I to mock an unauthenticated user, for testing purposes? - since it appears that the is_authenticated property is read only?
[[ Update ]]
I've just done a little more research + testing, and it looks like the solution to this would be to use AnnonymousUser as follows:
from django.contrib.auth.models import AnonymousUser
self.unaunthorised_user = AnonymousUser()
I am waiting to see if someone can confirm this (preferably, with a link to documentation)

Related

In django, how to sign in using Oauth, if signed up using CustomForm

My Django app has an option for login/register using CustomForm (inherited from the UserCreationForm) as well as Outh. Now the problem is if a user has already signed up using the CustomForm and if the next time he tries to log in using google Oauth then instead of logging in, google Oauth is redirecting to some other signup form (not created by me) which looks like:
But as the user is already registered, if he enters the same username/email here then it displays says username taken. So how can I resolve this issue? I mean I want the user to be able to login using Oauth even if he has signed up using the CustomForm, how can I implement that? Or even if I ask the user to fill this form to be able to use OAuth login, the problem is that his email/username are already present in the db, so he won't be able to sign up using this form.
Edit:
If that's difficult to implement then instead how can I just show a message when the user tries to login using oauth after signing up with the CustomForm, something like "You signed up using username rather than google account", rather than taking him to the socialaccount signup form?
My register function in views.py:
def register(request):
if request.method == 'POST':
form = CustomForm(request.POST or None)
if form.is_valid():
form.save()
return redirect('login')
else:
return redirect('register')
else:
return render(request, 'accounts/register.html')
forms.py looks something like this:
class CustomForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = ("username", "email")
You can use pre_social_login signal
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.signals import pre_social_login
from allauth.account.utils import perform_login
from allauth.utils import get_user_model
from django.dispatch import receiver
from django.shortcuts import redirect
from django.conf import settings
#receiver(pre_social_login)
def link_to_local_user(sender, request, sociallogin, **kwargs):
email_address = sociallogin.account.extra_data['email']
User = get_user_model()
users = User.objects.filter(email=email_address)
if users:
perform_login(request, users[0], email_verification=settings.EMAIL_VERIFICATION)
raise ImmediateHttpResponse(redirect(settings.LOGIN_REDIRECT_URL))
See https://github.com/pennersr/django-allauth/issues/215

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

OAuth2: authenticate with email instead of username

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

testing Django REST Framework

I'm working on my first project which uses Django REST Framework, and I'm having issues testing the API. I'm getting 403 Forbidden errors instead of the expected 200 or 201. However, the API works as expected.
I have been going through DRF Testing docs, and it all seems straightforward, but I believe my client is not being logged in. Normally in my Django projects I use a mixture of factory boy and django webtest which I've had a lot of happy success with. I'm not finding that same happiness testing the DRF API after a couple days of fiddling around.
I'm not sure if this is a problem relating to something I'm doing wrong with DRF APITestCase/APIClient or a problem with the django test in general.
I'm just pasting the following code and not posting the serializers/viewsets because the API works in the browser, it seems I'm just having issues with the APIClient authentication in the APITestCase.
# settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
# tests.py
from django.test import TestCase
from rest_framework.test import APITestCase, APIClient
from accounts.models import User
from .factories import StaffUserFactory
class MainSetUp(TestCase):
def setUp(self):
self.user = StaffUserFactory
self.api_root = '/api/v0/'
self.client = APIClient()
class APITests(MainSetUp, APITestCase):
def test_create_feedback(self):
"""
Ensure we can create a new account object.
"""
self.client.login(username='staffuser', password='staffpassword')
url = '%sfeedback/' % self.api_root
data = {
'feedback': 'this is test feedback'
}
response = self.client.post(url, data, user=self.user)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data, data)
# factories.py
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFactory(DjangoModelFactory):
class Meta:
model = User
class StaffUserFactory(UserFactory):
username = 'staffuser'
password = 'staffpassword'
email = 'staff#email.com'
first_name = 'Staff'
last_name = 'User'
is_staff = True
I've never used DjangoModelFactory before, but it appears that you have to call create after setting your user to the StaffUserFactory. http://factoryboy.readthedocs.org/en/latest/_modules/factory/django.html#DjangoModelFactory
class MainSetUp(TestCase):
def setUp(self):
self.user = StaffUserFactory
self.user.create()
self.api_root = '/api/v0/'
self.client = APIClient()
I bet your User's password is not being set properly. You should use set_password. As a start, trying changing your setUp to this:
def setUp(self):
self.user = StaffUserFactory
self.user.set_password('staffpassword')
self.user.save() # You could probably omit this, but set_password does't call it
self.api_root = '/api/v0/'
self.client = APIClient()
If that works, you probably want to override _generate() in your factory to add that step.
Another thing to check would be that SessionAuthentication is in your DEFAULT_AUTHENTICATION_CLASSES setting.
I think you must instantiate the Factory to get real object, like this:
self.user = StaffUserFactory()
Hope that help.
Moreover, you don't need to create a seperate class for staff, just set is_staff=True is enough. Like this:
self.user = UseFactory(is_staff=True)

permission_required decorator not working for me

I can't figure out why the permission required decorator isn't working. I would like to allow access to a view only for staff members. I have tried
#permission_required('request.user.is_staff',login_url="../admin")
def series_info(request):
...
and also
#permission_required('user.is_staff',login_url="../admin")
def series_info(request):
...
As the superuser, I can access the view, but any users I create as staff can't access it and are redirected to the login url page. I tested the login_required decorator and that works fine.
permission_required() must be passed a permission name, not a Python expression in a string. Try this instead:
from contrib.auth.decorators import user_passes_test
def staff_required(login_url=None):
return user_passes_test(lambda u: u.is_staff, login_url=login_url)
#staff_required(login_url="../admin")
def series_info(request)
...
Thanks. That does work. Do you have an
example of how to use
permission_required? From the
documentation
docs.djangoproject.com/en/1.0/… and
djangobook.com/en/2.0/chapter14 I
thought what I had should work.
Re-read the links you posted; permission_required() will test if a user has been granted a particular permission. It does not test the attributes of the user object.
From http://www.djangobook.com/en/2.0/chapter14/:
def vote(request):
if request.user.is_authenticated() and request.user.has_perm('polls.can_vote'):
# vote here
else:
return HttpResponse("You can't vote in this poll.")
#
#
# # #
###
#
def user_can_vote(user):
return user.is_authenticated() and user.has_perm("polls.can_vote")
#user_passes_test(user_can_vote, login_url="/login/")
def vote(request):
# vote here
#
#
# # #
###
#
from django.contrib.auth.decorators import permission_required
#permission_required('polls.can_vote', login_url="/login/")
def vote(request):
# vote here
This is how I would do it:
from django.contrib.admin.views.decorators import staff_member_required
#staff_member_required
def series_info(request):
...
The documentation says about staff_member_required:
Decorator for views that checks that the user is logged in and is a staff member, displaying the login page if necessary.
Here is an example of behavior I don't understand. I create a user, request and decorate a test function with permission_required checking for 'is_staff'. If the user is superuser, then access is granted to the test function. If the user only has is_staff = True, access is not granted.
from django.http import HttpRequest
from django.contrib.auth.models import User
from django.contrib.auth.decorators import permission_required
#permission_required('is_staff')
def test(dummy='dummy'):
print 'In test'
mb_user = User.objects.create_user('mitch', 'mb#home.com', 'mbpassword')
mb_user.is_staff = True
req = HttpRequest()
req.user = mb_user
test(req) # access to test denied - redirected
req.user.is_staff = False
test(req) # same as when is_staff is True
req.user.is_superuser = True
test(req) # access to test allowed
By the way, if you're using class-based views, you should wrap your decorator in the method_decorator decorator (go figure):
class MyView(DetailView):
...
#method_decorator(permission_required('polls.can_vote', login_url=reverse_lazy('my_login')))
def dispatch(self, request, *args, **kwargs):
.... blah ....
class MyModel(models.Model):
...
def has_perm(self perm, obj=None):
if perm == 'polls.canvote':
return self.can_vote()
This works for me on my 'project' table/model:
#permission_required('myApp.add_project')
def create(request):
# python code etc...
Obviously change the add_project to the add_[whatever your model/table is]. To edit it would be:
#permission_required('myApp.edit_project')
and to delete:
#permission_required('myApp.delete_project')
But I found that the key thing is to make sure your auth tables are set up correctly. This is what caused me problems. Here is a MySQL SQL query I wrote to check permissions if you are using groups. This should work in most dBs:
select usr.id as 'user id',usr.username,grp.id as 'group id',grp.name as 'group name',grpu.id as 'auth_user_groups',grpp.id as 'auth_group_permissions',perm.name,perm.codename
from auth_user usr
left join auth_user_groups grpu on usr.id = grpu.user_id
left join auth_group grp on grpu.group_id = grp.id
left join auth_group_permissions grpp on grp.id = grpp.group_id
left join auth_permission perm on grpp.permission_id = perm.id
order by usr.id;
I found that my permissions were not set up correctly, and also watch out for the django_content_type table which must have rows for each app and table for each of add, edit, delete. So if you have a project table you should see this in django_content_type:
id [generated by dB]
app_label myApp
model project
If you are having trouble another good idea is to enable and use the django admin app. This will show you where your problems are, and by setting up some test permissions, users and groups you can then examine the tables discussed above to see what is being inserted where. This will give you a good idea of how auth permissions works.
I write this to maybe save someone from having to spend a few hours figuring out what I did!