How to test for AnonymousUser in Django Unittesting - django

I'm testing a form
A user may be logged in, or anonymous, when presented with the form
If the form succeeds, the user will be logged in
If the form fails, the user will remain as they were
I'd like to confirm this behaviour by unit testing.
django.test.Client does have a login method for this kind of thing, but given a response object how do I determine who is logged in?
data = {'email':'john#example.com','password':'abc'}
c = Client()
# here was can assume `request.user` is the AnonymousUser
# or I can use `c.login(..)` to log someone in
r = c.post('/myform/', data)
Can my unittest determine who the request.user would now be if I were to submit a second request?

You can do this:
client = Client()
# here was can assume `request.user` is the AnonymousUser
# or I can use `c.login(..)` to log someone in
from django.contrib import auth
user = auth.get_user(client) # it returns User or AnonymousUser
if user.is_anonymous():
...
It works because client keeps a user session (client.session).

Related

Unable to create an integration test for Django's reset password flow

I'm trying to implement an integration test for the password reset flow, but I'm stuck at the "password_reset_confirm" view. I already tested the flow manually, and it works fine. Unfortunately, the Django unit test client seems unable to follow correctly the redirects required in this view.
urls config
from django.contrib.auth import views as auth_views
url(r"^accounts/password_change/$",
auth_views.PasswordChangeView.as_view(),
name="password_change"),
url(r"^accounts/password_change/done/$",
auth_views.PasswordChangeDoneView.as_view(),
name="password_change_done"),
url(r"^accounts/password_reset/$",
auth_views.PasswordResetView.as_view(email_template_name="app/email/accounts/password_reset_email.html",
success_url=reverse_lazy("app:password_reset_done"),
subject_template_name="app/email/accounts/password_reset_subject.html"),
name="password_reset"),
url(r"^accounts/password_reset/done/$",
auth_views.PasswordResetDoneView.as_view(),
name="password_reset_done"),
url(r"^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
auth_views.PasswordResetConfirmView.as_view(
success_url=reverse_lazy("app:password_reset_complete"),
form_class=CustomSetPasswordForm),
name="password_reset_confirm"),
url(r"^accounts/reset/complete/$",
auth_views.PasswordResetCompleteView.as_view(),
name="password_reset_complete"),
Test code
import re
from django.urls import reverse, NoReverseMatch
from django.test import TestCase, Client
from django.core import mail
from django.test.utils import override_settings
from django.contrib.auth import authenticate
VALID_USER_NAME = "username"
USER_OLD_PSW = "oldpassword"
USER_NEW_PSW = "newpassword"
PASSWORD_RESET_URL = reverse("app:password_reset")
def PASSWORD_RESET_CONFIRM_URL(uidb64, token):
try:
return reverse("app:password_reset_confirm", args=(uidb64, token))
except NoReverseMatch:
return f"/accounts/reset/invaliduidb64/invalid-token/"
def utils_extract_reset_tokens(full_url):
return re.findall(r"/([\w\-]+)",
re.search(r"^http\://.+$", full_url, flags=re.MULTILINE)[0])[3:5]
#override_settings(EMAIL_BACKEND="anymail.backends.test.EmailBackend")
class PasswordResetTestCase(TestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.myclient = Client()
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, token),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
Now, the assert fails: the user is authenticated with the old password. From the log I'm able to detect that the change password is not executed.
A few extra useful information:
post returns a successful HTTP 200;
the response.redirect_chain is [('/accounts/reset/token_removed/set-password/', 302)] and I think this is wrong, as it should have another loop (in the manual case I see another call to the dispatch method);
I'm executing the test with the Django unit test tools.
Any idea on how to properly test this scenario? I need this to make sure emails and logging are properly executed (and never removed).
Many thanks!
EDIT: solution
As well explained by the accepted solution, here the working code for the test case:
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
self.myclient.get(PASSWORD_RESET_CONFIRM_URL(uidb64, token), follow=True)
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, "set-password"),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
This is very interesting; so it looks like Django has implemented a security feature in the password reset page to prevent the token from being leaked in the HTTP Referrer header. Read more about Referrer Header Leaks here.
TL;DR
Django is basically taking the sensitive token from the URL and placing it in Session and performing a internal redirect (same domain) to prevent you from clicking away to a different site and leaking the token via the Referer header.
Here's how:
When you hit /accounts/reset/uidb64/token/ (you should be doing a GET here, however you are doing a POST in your test case) the first time, Django pulls the token from the URL and sets it in session and redirects you to /accounts/reset/uidb64/set-password/.
This now loads the /accounts/reset/uidb64/set-password/ page, where you can set the passwords and perform a POST
When you POST from this page, the same View handles your POST request since the token URL param can handle both the token and the string set-password.
This time though, the view sees that you have accessed it with set-password and not a token, so it expects to pull your actual token from session, and then change the password.
Here's the flow as a chart:
GET /reset/uidb64/token/ --> Set token in session --> 302 Redirect to /reset/uidb64/set-token/ --> POST Password --> Get token from Session --> Token Valid? --> Reset password
Here's the code!
INTERNAL_RESET_URL_TOKEN = 'set-password'
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
#method_decorator(sensitive_post_parameters())
#method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
self.user = self.get_user(kwargs['uidb64'])
if self.user is not None:
token = kwargs['token']
if token == INTERNAL_RESET_URL_TOKEN:
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
if self.token_generator.check_token(self.user, session_token):
# If the token is valid, display the password reset form.
self.validlink = True
return super().dispatch(*args, **kwargs)
else:
if self.token_generator.check_token(self.user, token):
# Store the token in the session and redirect to the
# password reset form at a URL without the token. That
# avoids the possibility of leaking the token in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
redirect_url = self.request.path.replace(token, INTERNAL_RESET_URL_TOKEN)
return HttpResponseRedirect(redirect_url)
# Display the "Password reset unsuccessful" page.
return self.render_to_response(self.get_context_data())
Notice the comment in the code where this magic happens:
Store the token in the session and redirect to the
password reset form at a URL without the token. That
avoids the possibility of leaking the token in the
HTTP Referer header.
I think this makes it clear how you can fix your unit test; do a GET on the PASSWORD_RESET_URL which will give you the redirect URL, you can then POST to this redirect_url and perform password resets!

django admin page and JWT

We are using django-rest-framework with django-rest-framework-jwt for authentication and it works everywhere except the django admin page at ip:port/admin/. That still wants username and password.
Is there a setting or way to bypass that so it recognizes the JWT?
Is the /admin/ page always required to use name/password? I think the built in token auth works with it.
jwt is the only auth set in the settings.py file. Session authentication is not in there anymore.
The issue is that Django isn't aware of djangorestframework-jwt, but only djangorestframework, itself. The solution that worked for me was to create a simple middleware that leveraged the auth of djangorestframework-jwt
In settings.py:
MIDDLEWARE = [
# others
'myapp.middleware.jwt_auth_middleware',
]
Then in my myapp/middleware.py
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from django.contrib.auth.models import AnonymousUser
from rest_framework import exceptions
def jwt_auth_middleware(get_response):
"""Sets the user object from a JWT header"""
def middleware(request):
try:
authenticated = JSONWebTokenAuthentication().authenticate(request)
if authenticated:
request.user = authenticated[0]
else:
request.user = AnonymousUser
except exceptions.AuthenticationFailed as err:
print(err)
request.user = AnonymousUser
response = get_response(request)
return response
return middleware
Important Note:
This is a naive approach that you shouldn't run in production so I only enable this middleware if DEBUG. If running in production, you should probably cache and lazily evaluate the user as done by the builtin django.contrib.auth module.
The problem can be not in the authentication method you use. If you customize User model, it can happen that create_superuser method doesn't update is_active flag in user instance details to True. This case django authentication backend (if you use ModelBackend) can recognize that user is not active and do not allow to authenticate. Simple check - just see what value has is_active field of the superuser you create. If it False, update it manually to True, and try to login. If it is the reason of your problem you need to override create_superuser and create_user method of UserManager class.

django user auth without typing password

I want a feature workflows as follows:
1) user login from web page ( typing user name/password),and auth success, then we can got user's password from database(not from web page), we will use it later.
2) user confirm start a small bot service that provide by us, once user confirm that, then the service will execute and callback
3) since the bot service is another independent app, so it have to use user account callback login action (auth) and record information under the user account.
my question is, while I use the user's account login in bot service app, it failed, since the password has been hash.
is there any way solve this issue ?
I trace django source code ,
djanofrom django.contrib import auth
auth.login(request,authenticate)
seem no other way solve this issue unless modify the source code, I meaning add new function in framework? but obviously, it is not best idea
anyone give tips on this issue, thanks
You should write a custom auth backend.
https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#writing-an-authentication-backend
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class PasswordlessAuthBackend(ModelBackend):
def authenticate(self, username=None):
try:
return User.objects.get(username=username)
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
then add this to your settings.py
AUTHENTICATION_BACKENDS = (
# ... your other backends
'yourapp.auth_backend.PasswordlessAuthBackend',
)
after that you can login in your views with
user = authenticate(username=user.username)
login(request, user)

testing django user login by username?

I am trying to run a test case on whether the register user is logged in or not
tests.py
class RegistrationFormTestCase(TestCase):
def test_valid_form(self):
response = self.client.post(reverse('user-registration'),
{
'username':'admin99',
'password1':'qwertyui',
'password2':'qwertyui',
'email':'admin#example.com',
})
user = User.objects.get(username='admin99')
self.assertEqual(response.status_code,302)
The following test case is getting passed and I want to check whether the user exists or not,
response is HttpResponse object. I have retrive the obtained username from response and compare it to the user
eg: self.assertEqual(response(username),user)
Any help would be apprectiated.Thanks in advance...:)

Django auth.models.User isn't authenticated, but exists in the auth database

Here is the example, which should, by my current understandings, work. The code bellow is directly copied from testing in ./manage.py shell
There is a registered user in the auth database:
from django.contrib.auth.models import User
user_in_database = User.objects.all()[0]
print user_in_database.username #username
print user_in_database.password #pass
Authentification test:
from django.contrib.auth import authenticate
authenticated_user = authenticate(username = user_in_database.username,
password = user_in_database.password)
type(authenticated_user) #NoneType
It returns None, I would have expected it to return the user from the database. Am I missing something crucial here?
What you're missing is that user_in_database.password isn't the user's password. Instead, it's a hash of the actual password (just print it to check yourself).
Essentially, when you call authenticate, you're not using the right password.
As such, authentication fails, and returns None (because that's what django returns when you pass incorrect credentials).