test_views.py 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import datetime
  4. import itertools
  5. import os
  6. import re
  7. from importlib import import_module
  8. from django.apps import apps
  9. from django.conf import settings
  10. from django.contrib.admin.models import LogEntry
  11. from django.contrib.auth import REDIRECT_FIELD_NAME, SESSION_KEY
  12. from django.contrib.auth.forms import (
  13. AuthenticationForm, PasswordChangeForm, SetPasswordForm,
  14. )
  15. from django.contrib.auth.models import User
  16. from django.contrib.auth.tests.custom_user import CustomUser
  17. from django.contrib.auth.views import login as login_view, redirect_to_login
  18. from django.contrib.sessions.middleware import SessionMiddleware
  19. from django.contrib.sites.requests import RequestSite
  20. from django.core import mail
  21. from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
  22. from django.db import connection
  23. from django.http import HttpRequest, QueryDict
  24. from django.middleware.csrf import CsrfViewMiddleware, get_token
  25. from django.test import TestCase, override_settings
  26. from django.test.utils import patch_logger
  27. from django.utils.encoding import force_text
  28. from django.utils.http import urlquote
  29. from django.utils.six.moves.urllib.parse import ParseResult, urlparse
  30. from django.utils.translation import LANGUAGE_SESSION_KEY
  31. from .models import UUIDUser
  32. from .settings import AUTH_TEMPLATES
  33. @override_settings(
  34. LANGUAGES=[
  35. ('en', 'English'),
  36. ],
  37. LANGUAGE_CODE='en',
  38. TEMPLATES=AUTH_TEMPLATES,
  39. USE_TZ=False,
  40. PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
  41. ROOT_URLCONF='auth_tests.urls',
  42. )
  43. class AuthViewsTestCase(TestCase):
  44. """
  45. Helper base class for all the follow test cases.
  46. """
  47. @classmethod
  48. def setUpTestData(cls):
  49. cls.u1 = User.objects.create(
  50. password='sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161',
  51. last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), is_superuser=False, username='testclient',
  52. first_name='Test', last_name='Client', email='testclient@example.com', is_staff=False, is_active=True,
  53. date_joined=datetime.datetime(2006, 12, 17, 7, 3, 31)
  54. )
  55. cls.u2 = User.objects.create(
  56. password='sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161',
  57. last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), is_superuser=False, username='inactive',
  58. first_name='Inactive', last_name='User', email='testclient2@example.com', is_staff=False, is_active=False,
  59. date_joined=datetime.datetime(2006, 12, 17, 7, 3, 31)
  60. )
  61. cls.u3 = User.objects.create(
  62. password='sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161',
  63. last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), is_superuser=False, username='staff',
  64. first_name='Staff', last_name='Member', email='staffmember@example.com', is_staff=True, is_active=True,
  65. date_joined=datetime.datetime(2006, 12, 17, 7, 3, 31)
  66. )
  67. cls.u4 = User.objects.create(
  68. password='', last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), is_superuser=False,
  69. username='empty_password', first_name='Empty', last_name='Password', email='empty_password@example.com',
  70. is_staff=False, is_active=True, date_joined=datetime.datetime(2006, 12, 17, 7, 3, 31)
  71. )
  72. cls.u5 = User.objects.create(
  73. password='$', last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), is_superuser=False,
  74. username='unmanageable_password', first_name='Unmanageable', last_name='Password',
  75. email='unmanageable_password@example.com', is_staff=False, is_active=True,
  76. date_joined=datetime.datetime(2006, 12, 17, 7, 3, 31)
  77. )
  78. cls.u6 = User.objects.create(
  79. password='foo$bar', last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), is_superuser=False,
  80. username='unknown_password', first_name='Unknown', last_name='Password',
  81. email='unknown_password@example.com', is_staff=False, is_active=True,
  82. date_joined=datetime.datetime(2006, 12, 17, 7, 3, 31)
  83. )
  84. def login(self, username='testclient', password='password'):
  85. response = self.client.post('/login/', {
  86. 'username': username,
  87. 'password': password,
  88. })
  89. self.assertIn(SESSION_KEY, self.client.session)
  90. return response
  91. def logout(self):
  92. response = self.client.get('/admin/logout/')
  93. self.assertEqual(response.status_code, 200)
  94. self.assertNotIn(SESSION_KEY, self.client.session)
  95. def assertFormError(self, response, error):
  96. """Assert that error is found in response.context['form'] errors"""
  97. form_errors = list(itertools.chain(*response.context['form'].errors.values()))
  98. self.assertIn(force_text(error), form_errors)
  99. def assertURLEqual(self, url, expected, parse_qs=False):
  100. """
  101. Given two URLs, make sure all their components (the ones given by
  102. urlparse) are equal, only comparing components that are present in both
  103. URLs.
  104. If `parse_qs` is True, then the querystrings are parsed with QueryDict.
  105. This is useful if you don't want the order of parameters to matter.
  106. Otherwise, the query strings are compared as-is.
  107. """
  108. fields = ParseResult._fields
  109. for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
  110. if parse_qs and attr == 'query':
  111. x, y = QueryDict(x), QueryDict(y)
  112. if x and y and x != y:
  113. self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))
  114. @override_settings(ROOT_URLCONF='django.contrib.auth.urls')
  115. class AuthViewNamedURLTests(AuthViewsTestCase):
  116. def test_named_urls(self):
  117. "Named URLs should be reversible"
  118. expected_named_urls = [
  119. ('login', [], {}),
  120. ('logout', [], {}),
  121. ('password_change', [], {}),
  122. ('password_change_done', [], {}),
  123. ('password_reset', [], {}),
  124. ('password_reset_done', [], {}),
  125. ('password_reset_confirm', [], {
  126. 'uidb64': 'aaaaaaa',
  127. 'token': '1111-aaaaa',
  128. }),
  129. ('password_reset_complete', [], {}),
  130. ]
  131. for name, args, kwargs in expected_named_urls:
  132. try:
  133. reverse(name, args=args, kwargs=kwargs)
  134. except NoReverseMatch:
  135. self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name)
  136. class PasswordResetTest(AuthViewsTestCase):
  137. def test_email_not_found(self):
  138. """If the provided email is not registered, don't raise any error but
  139. also don't send any email."""
  140. response = self.client.get('/password_reset/')
  141. self.assertEqual(response.status_code, 200)
  142. response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
  143. self.assertEqual(response.status_code, 302)
  144. self.assertEqual(len(mail.outbox), 0)
  145. def test_email_found(self):
  146. "Email is sent if a valid email address is provided for password reset"
  147. response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
  148. self.assertEqual(response.status_code, 302)
  149. self.assertEqual(len(mail.outbox), 1)
  150. self.assertIn("http://", mail.outbox[0].body)
  151. self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
  152. # optional multipart text/html email has been added. Make sure original,
  153. # default functionality is 100% the same
  154. self.assertFalse(mail.outbox[0].message().is_multipart())
  155. def test_extra_email_context(self):
  156. """
  157. extra_email_context should be available in the email template context.
  158. """
  159. response = self.client.post(
  160. '/password_reset_extra_email_context/',
  161. {'email': 'staffmember@example.com'},
  162. )
  163. self.assertEqual(response.status_code, 302)
  164. self.assertEqual(len(mail.outbox), 1)
  165. self.assertIn('Email email context: "Hello!"', mail.outbox[0].body)
  166. def test_html_mail_template(self):
  167. """
  168. A multipart email with text/plain and text/html is sent
  169. if the html_email_template parameter is passed to the view
  170. """
  171. response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
  172. self.assertEqual(response.status_code, 302)
  173. self.assertEqual(len(mail.outbox), 1)
  174. message = mail.outbox[0].message()
  175. self.assertEqual(len(message.get_payload()), 2)
  176. self.assertTrue(message.is_multipart())
  177. self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
  178. self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
  179. self.assertNotIn('<html>', message.get_payload(0).get_payload())
  180. self.assertIn('<html>', message.get_payload(1).get_payload())
  181. def test_email_found_custom_from(self):
  182. "Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
  183. response = self.client.post('/password_reset_from_email/', {'email': 'staffmember@example.com'})
  184. self.assertEqual(response.status_code, 302)
  185. self.assertEqual(len(mail.outbox), 1)
  186. self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
  187. # Skip any 500 handler action (like sending more mail...)
  188. @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
  189. def test_poisoned_http_host(self):
  190. "Poisoned HTTP_HOST headers can't be used for reset emails"
  191. # This attack is based on the way browsers handle URLs. The colon
  192. # should be used to separate the port, but if the URL contains an @,
  193. # the colon is interpreted as part of a username for login purposes,
  194. # making 'evil.com' the request domain. Since HTTP_HOST is used to
  195. # produce a meaningful reset URL, we need to be certain that the
  196. # HTTP_HOST header isn't poisoned. This is done as a check when get_host()
  197. # is invoked, but we check here as a practical consequence.
  198. with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
  199. response = self.client.post(
  200. '/password_reset/',
  201. {'email': 'staffmember@example.com'},
  202. HTTP_HOST='www.example:dr.frankenstein@evil.tld'
  203. )
  204. self.assertEqual(response.status_code, 400)
  205. self.assertEqual(len(mail.outbox), 0)
  206. self.assertEqual(len(logger_calls), 1)
  207. # Skip any 500 handler action (like sending more mail...)
  208. @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
  209. def test_poisoned_http_host_admin_site(self):
  210. "Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
  211. with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
  212. response = self.client.post(
  213. '/admin_password_reset/',
  214. {'email': 'staffmember@example.com'},
  215. HTTP_HOST='www.example:dr.frankenstein@evil.tld'
  216. )
  217. self.assertEqual(response.status_code, 400)
  218. self.assertEqual(len(mail.outbox), 0)
  219. self.assertEqual(len(logger_calls), 1)
  220. def _test_confirm_start(self):
  221. # Start by creating the email
  222. self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
  223. self.assertEqual(len(mail.outbox), 1)
  224. return self._read_signup_email(mail.outbox[0])
  225. def _read_signup_email(self, email):
  226. urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
  227. self.assertIsNotNone(urlmatch, "No URL found in sent email")
  228. return urlmatch.group(), urlmatch.groups()[0]
  229. def test_confirm_valid(self):
  230. url, path = self._test_confirm_start()
  231. response = self.client.get(path)
  232. # redirect to a 'complete' page:
  233. self.assertContains(response, "Please enter your new password")
  234. def test_confirm_invalid(self):
  235. url, path = self._test_confirm_start()
  236. # Let's munge the token in the path, but keep the same length,
  237. # in case the URLconf will reject a different length.
  238. path = path[:-5] + ("0" * 4) + path[-1]
  239. response = self.client.get(path)
  240. self.assertContains(response, "The password reset link was invalid")
  241. def test_confirm_invalid_user(self):
  242. # Ensure that we get a 200 response for a non-existent user, not a 404
  243. response = self.client.get('/reset/123456/1-1/')
  244. self.assertContains(response, "The password reset link was invalid")
  245. def test_confirm_overflow_user(self):
  246. # Ensure that we get a 200 response for a base36 user id that overflows int
  247. response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
  248. self.assertContains(response, "The password reset link was invalid")
  249. def test_confirm_invalid_post(self):
  250. # Same as test_confirm_invalid, but trying
  251. # to do a POST instead.
  252. url, path = self._test_confirm_start()
  253. path = path[:-5] + ("0" * 4) + path[-1]
  254. self.client.post(path, {
  255. 'new_password1': 'anewpassword',
  256. 'new_password2': ' anewpassword',
  257. })
  258. # Check the password has not been changed
  259. u = User.objects.get(email='staffmember@example.com')
  260. self.assertTrue(not u.check_password("anewpassword"))
  261. def test_confirm_complete(self):
  262. url, path = self._test_confirm_start()
  263. response = self.client.post(path, {'new_password1': 'anewpassword',
  264. 'new_password2': 'anewpassword'})
  265. # Check the password has been changed
  266. u = User.objects.get(email='staffmember@example.com')
  267. self.assertTrue(u.check_password("anewpassword"))
  268. # Check we can't use the link again
  269. response = self.client.get(path)
  270. self.assertContains(response, "The password reset link was invalid")
  271. def test_confirm_different_passwords(self):
  272. url, path = self._test_confirm_start()
  273. response = self.client.post(path, {'new_password1': 'anewpassword',
  274. 'new_password2': 'x'})
  275. self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
  276. def test_reset_redirect_default(self):
  277. response = self.client.post('/password_reset/',
  278. {'email': 'staffmember@example.com'})
  279. self.assertEqual(response.status_code, 302)
  280. self.assertURLEqual(response.url, '/password_reset/done/')
  281. def test_reset_custom_redirect(self):
  282. response = self.client.post('/password_reset/custom_redirect/',
  283. {'email': 'staffmember@example.com'})
  284. self.assertEqual(response.status_code, 302)
  285. self.assertURLEqual(response.url, '/custom/')
  286. def test_reset_custom_redirect_named(self):
  287. response = self.client.post('/password_reset/custom_redirect/named/',
  288. {'email': 'staffmember@example.com'})
  289. self.assertEqual(response.status_code, 302)
  290. self.assertURLEqual(response.url, '/password_reset/')
  291. def test_confirm_redirect_default(self):
  292. url, path = self._test_confirm_start()
  293. response = self.client.post(path, {'new_password1': 'anewpassword',
  294. 'new_password2': 'anewpassword'})
  295. self.assertEqual(response.status_code, 302)
  296. self.assertURLEqual(response.url, '/reset/done/')
  297. def test_confirm_redirect_custom(self):
  298. url, path = self._test_confirm_start()
  299. path = path.replace('/reset/', '/reset/custom/')
  300. response = self.client.post(path, {'new_password1': 'anewpassword',
  301. 'new_password2': 'anewpassword'})
  302. self.assertEqual(response.status_code, 302)
  303. self.assertURLEqual(response.url, '/custom/')
  304. def test_confirm_redirect_custom_named(self):
  305. url, path = self._test_confirm_start()
  306. path = path.replace('/reset/', '/reset/custom/named/')
  307. response = self.client.post(path, {'new_password1': 'anewpassword',
  308. 'new_password2': 'anewpassword'})
  309. self.assertEqual(response.status_code, 302)
  310. self.assertURLEqual(response.url, '/password_reset/')
  311. def test_confirm_display_user_from_form(self):
  312. url, path = self._test_confirm_start()
  313. response = self.client.get(path)
  314. # #16919 -- The ``password_reset_confirm`` view should pass the user
  315. # object to the ``SetPasswordForm``, even on GET requests.
  316. # For this test, we render ``{{ form.user }}`` in the template
  317. # ``registration/password_reset_confirm.html`` so that we can test this.
  318. username = User.objects.get(email='staffmember@example.com').username
  319. self.assertContains(response, "Hello, %s." % username)
  320. # However, the view should NOT pass any user object on a form if the
  321. # password reset link was invalid.
  322. response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
  323. self.assertContains(response, "Hello, .")
  324. @override_settings(AUTH_USER_MODEL='auth.CustomUser')
  325. class CustomUserPasswordResetTest(AuthViewsTestCase):
  326. user_email = 'staffmember@example.com'
  327. @classmethod
  328. def setUpTestData(cls):
  329. cls.u1 = CustomUser.custom_objects.create(
  330. password='sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161',
  331. last_login=datetime.datetime(2006, 12, 17, 7, 3, 31), email='staffmember@example.com', is_active=True,
  332. is_admin=False, date_of_birth=datetime.date(1976, 11, 8)
  333. )
  334. def _test_confirm_start(self):
  335. # Start by creating the email
  336. response = self.client.post('/password_reset/', {'email': self.user_email})
  337. self.assertEqual(response.status_code, 302)
  338. self.assertEqual(len(mail.outbox), 1)
  339. return self._read_signup_email(mail.outbox[0])
  340. def _read_signup_email(self, email):
  341. urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
  342. self.assertIsNotNone(urlmatch, "No URL found in sent email")
  343. return urlmatch.group(), urlmatch.groups()[0]
  344. def test_confirm_valid_custom_user(self):
  345. url, path = self._test_confirm_start()
  346. response = self.client.get(path)
  347. # redirect to a 'complete' page:
  348. self.assertContains(response, "Please enter your new password")
  349. # then submit a new password
  350. response = self.client.post(path, {
  351. 'new_password1': 'anewpassword',
  352. 'new_password2': 'anewpassword',
  353. })
  354. self.assertRedirects(response, '/reset/done/')
  355. @override_settings(AUTH_USER_MODEL='auth.UUIDUser')
  356. class UUIDUserPasswordResetTest(CustomUserPasswordResetTest):
  357. def _test_confirm_start(self):
  358. # instead of fixture
  359. UUIDUser.objects.create_user(
  360. email=self.user_email,
  361. username='foo',
  362. password='foo',
  363. )
  364. return super(UUIDUserPasswordResetTest, self)._test_confirm_start()
  365. class ChangePasswordTest(AuthViewsTestCase):
  366. def fail_login(self, password='password'):
  367. response = self.client.post('/login/', {
  368. 'username': 'testclient',
  369. 'password': password,
  370. })
  371. self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % {
  372. 'username': User._meta.get_field('username').verbose_name
  373. })
  374. def logout(self):
  375. self.client.get('/logout/')
  376. def test_password_change_fails_with_invalid_old_password(self):
  377. self.login()
  378. response = self.client.post('/password_change/', {
  379. 'old_password': 'donuts',
  380. 'new_password1': 'password1',
  381. 'new_password2': 'password1',
  382. })
  383. self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect'])
  384. def test_password_change_fails_with_mismatched_passwords(self):
  385. self.login()
  386. response = self.client.post('/password_change/', {
  387. 'old_password': 'password',
  388. 'new_password1': 'password1',
  389. 'new_password2': 'donuts',
  390. })
  391. self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
  392. def test_password_change_succeeds(self):
  393. self.login()
  394. self.client.post('/password_change/', {
  395. 'old_password': 'password',
  396. 'new_password1': 'password1',
  397. 'new_password2': 'password1',
  398. })
  399. self.fail_login()
  400. self.login(password='password1')
  401. def test_password_change_done_succeeds(self):
  402. self.login()
  403. response = self.client.post('/password_change/', {
  404. 'old_password': 'password',
  405. 'new_password1': 'password1',
  406. 'new_password2': 'password1',
  407. })
  408. self.assertEqual(response.status_code, 302)
  409. self.assertURLEqual(response.url, '/password_change/done/')
  410. @override_settings(LOGIN_URL='/login/')
  411. def test_password_change_done_fails(self):
  412. response = self.client.get('/password_change/done/')
  413. self.assertEqual(response.status_code, 302)
  414. self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
  415. def test_password_change_redirect_default(self):
  416. self.login()
  417. response = self.client.post('/password_change/', {
  418. 'old_password': 'password',
  419. 'new_password1': 'password1',
  420. 'new_password2': 'password1',
  421. })
  422. self.assertEqual(response.status_code, 302)
  423. self.assertURLEqual(response.url, '/password_change/done/')
  424. def test_password_change_redirect_custom(self):
  425. self.login()
  426. response = self.client.post('/password_change/custom/', {
  427. 'old_password': 'password',
  428. 'new_password1': 'password1',
  429. 'new_password2': 'password1',
  430. })
  431. self.assertEqual(response.status_code, 302)
  432. self.assertURLEqual(response.url, '/custom/')
  433. def test_password_change_redirect_custom_named(self):
  434. self.login()
  435. response = self.client.post('/password_change/custom/named/', {
  436. 'old_password': 'password',
  437. 'new_password1': 'password1',
  438. 'new_password2': 'password1',
  439. })
  440. self.assertEqual(response.status_code, 302)
  441. self.assertURLEqual(response.url, '/password_reset/')
  442. class SessionAuthenticationTests(AuthViewsTestCase):
  443. def test_user_password_change_updates_session(self):
  444. """
  445. #21649 - Ensure contrib.auth.views.password_change updates the user's
  446. session auth hash after a password change so the session isn't logged out.
  447. """
  448. self.login()
  449. response = self.client.post('/password_change/', {
  450. 'old_password': 'password',
  451. 'new_password1': 'password1',
  452. 'new_password2': 'password1',
  453. })
  454. # if the hash isn't updated, retrieving the redirection page will fail.
  455. self.assertRedirects(response, '/password_change/done/')
  456. class LoginTest(AuthViewsTestCase):
  457. def test_current_site_in_context_after_login(self):
  458. response = self.client.get(reverse('login'))
  459. self.assertEqual(response.status_code, 200)
  460. if apps.is_installed('django.contrib.sites'):
  461. Site = apps.get_model('sites.Site')
  462. site = Site.objects.get_current()
  463. self.assertEqual(response.context['site'], site)
  464. self.assertEqual(response.context['site_name'], site.name)
  465. else:
  466. self.assertIsInstance(response.context['site'], RequestSite)
  467. self.assertIsInstance(response.context['form'], AuthenticationForm)
  468. def test_security_check(self, password='password'):
  469. login_url = reverse('login')
  470. # Those URLs should not pass the security check
  471. for bad_url in ('http://example.com',
  472. 'http:///example.com',
  473. 'https://example.com',
  474. 'ftp://exampel.com',
  475. '///example.com',
  476. '//example.com',
  477. 'javascript:alert("XSS")'):
  478. nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
  479. 'url': login_url,
  480. 'next': REDIRECT_FIELD_NAME,
  481. 'bad_url': urlquote(bad_url),
  482. }
  483. response = self.client.post(nasty_url, {
  484. 'username': 'testclient',
  485. 'password': password,
  486. })
  487. self.assertEqual(response.status_code, 302)
  488. self.assertNotIn(bad_url, response.url,
  489. "%s should be blocked" % bad_url)
  490. # These URLs *should* still pass the security check
  491. for good_url in ('/view/?param=http://example.com',
  492. '/view/?param=https://example.com',
  493. '/view?param=ftp://exampel.com',
  494. 'view/?param=//example.com',
  495. 'https://testserver/',
  496. 'HTTPS://testserver/',
  497. '//testserver/',
  498. '/url%20with%20spaces/'): # see ticket #12534
  499. safe_url = '%(url)s?%(next)s=%(good_url)s' % {
  500. 'url': login_url,
  501. 'next': REDIRECT_FIELD_NAME,
  502. 'good_url': urlquote(good_url),
  503. }
  504. response = self.client.post(safe_url, {
  505. 'username': 'testclient',
  506. 'password': password,
  507. })
  508. self.assertEqual(response.status_code, 302)
  509. self.assertIn(good_url, response.url, "%s should be allowed" % good_url)
  510. def test_login_form_contains_request(self):
  511. # 15198
  512. self.client.post('/custom_requestauth_login/', {
  513. 'username': 'testclient',
  514. 'password': 'password',
  515. }, follow=True)
  516. # the custom authentication form used by this login asserts
  517. # that a request is passed to the form successfully.
  518. def test_login_csrf_rotate(self, password='password'):
  519. """
  520. Makes sure that a login rotates the currently-used CSRF token.
  521. """
  522. # Do a GET to establish a CSRF token
  523. # TestClient isn't used here as we're testing middleware, essentially.
  524. req = HttpRequest()
  525. CsrfViewMiddleware().process_view(req, login_view, (), {})
  526. # get_token() triggers CSRF token inclusion in the response
  527. get_token(req)
  528. resp = login_view(req)
  529. resp2 = CsrfViewMiddleware().process_response(req, resp)
  530. csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None)
  531. token1 = csrf_cookie.coded_value
  532. # Prepare the POST request
  533. req = HttpRequest()
  534. req.COOKIES[settings.CSRF_COOKIE_NAME] = token1
  535. req.method = "POST"
  536. req.POST = {'username': 'testclient', 'password': password, 'csrfmiddlewaretoken': token1}
  537. # Use POST request to log in
  538. SessionMiddleware().process_request(req)
  539. CsrfViewMiddleware().process_view(req, login_view, (), {})
  540. req.META["SERVER_NAME"] = "testserver" # Required to have redirect work in login view
  541. req.META["SERVER_PORT"] = 80
  542. resp = login_view(req)
  543. resp2 = CsrfViewMiddleware().process_response(req, resp)
  544. csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None)
  545. token2 = csrf_cookie.coded_value
  546. # Check the CSRF token switched
  547. self.assertNotEqual(token1, token2)
  548. def test_session_key_flushed_on_login(self):
  549. """
  550. To avoid reusing another user's session, ensure a new, empty session is
  551. created if the existing session corresponds to a different authenticated
  552. user.
  553. """
  554. self.login()
  555. original_session_key = self.client.session.session_key
  556. self.login(username='staff')
  557. self.assertNotEqual(original_session_key, self.client.session.session_key)
  558. def test_session_key_flushed_on_login_after_password_change(self):
  559. """
  560. As above, but same user logging in after a password change.
  561. """
  562. self.login()
  563. original_session_key = self.client.session.session_key
  564. # If no password change, session key should not be flushed.
  565. self.login()
  566. self.assertEqual(original_session_key, self.client.session.session_key)
  567. user = User.objects.get(username='testclient')
  568. user.set_password('foobar')
  569. user.save()
  570. self.login(password='foobar')
  571. self.assertNotEqual(original_session_key, self.client.session.session_key)
  572. def test_login_session_without_hash_session_key(self):
  573. """
  574. Session without django.contrib.auth.HASH_SESSION_KEY should login
  575. without an exception.
  576. """
  577. user = User.objects.get(username='testclient')
  578. engine = import_module(settings.SESSION_ENGINE)
  579. session = engine.SessionStore()
  580. session[SESSION_KEY] = user.id
  581. session.save()
  582. original_session_key = session.session_key
  583. self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key
  584. self.login()
  585. self.assertNotEqual(original_session_key, self.client.session.session_key)
  586. class LoginURLSettings(AuthViewsTestCase):
  587. """Tests for settings.LOGIN_URL."""
  588. def assertLoginURLEquals(self, url, parse_qs=False):
  589. response = self.client.get('/login_required/')
  590. self.assertEqual(response.status_code, 302)
  591. self.assertURLEqual(response.url, url, parse_qs=parse_qs)
  592. @override_settings(LOGIN_URL='/login/')
  593. def test_standard_login_url(self):
  594. self.assertLoginURLEquals('/login/?next=/login_required/')
  595. @override_settings(LOGIN_URL='login')
  596. def test_named_login_url(self):
  597. self.assertLoginURLEquals('/login/?next=/login_required/')
  598. @override_settings(LOGIN_URL='http://remote.example.com/login')
  599. def test_remote_login_url(self):
  600. quoted_next = urlquote('http://testserver/login_required/')
  601. expected = 'http://remote.example.com/login?next=%s' % quoted_next
  602. self.assertLoginURLEquals(expected)
  603. @override_settings(LOGIN_URL='https:///login/')
  604. def test_https_login_url(self):
  605. quoted_next = urlquote('http://testserver/login_required/')
  606. expected = 'https:///login/?next=%s' % quoted_next
  607. self.assertLoginURLEquals(expected)
  608. @override_settings(LOGIN_URL='/login/?pretty=1')
  609. def test_login_url_with_querystring(self):
  610. self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/', parse_qs=True)
  611. @override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/')
  612. def test_remote_login_url_with_next_querystring(self):
  613. quoted_next = urlquote('http://testserver/login_required/')
  614. expected = 'http://remote.example.com/login/?next=%s' % quoted_next
  615. self.assertLoginURLEquals(expected)
  616. @override_settings(LOGIN_URL=reverse_lazy('login'))
  617. def test_lazy_login_url(self):
  618. self.assertLoginURLEquals('/login/?next=/login_required/')
  619. class LoginRedirectUrlTest(AuthViewsTestCase):
  620. """Tests for settings.LOGIN_REDIRECT_URL."""
  621. def assertLoginRedirectURLEqual(self, url):
  622. response = self.login()
  623. self.assertEqual(response.status_code, 302)
  624. self.assertURLEqual(response.url, url)
  625. def test_default(self):
  626. self.assertLoginRedirectURLEqual('/accounts/profile/')
  627. @override_settings(LOGIN_REDIRECT_URL='/custom/')
  628. def test_custom(self):
  629. self.assertLoginRedirectURLEqual('/custom/')
  630. @override_settings(LOGIN_REDIRECT_URL='password_reset')
  631. def test_named(self):
  632. self.assertLoginRedirectURLEqual('/password_reset/')
  633. @override_settings(LOGIN_REDIRECT_URL='http://remote.example.com/welcome/')
  634. def test_remote(self):
  635. self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/')
  636. class RedirectToLoginTests(AuthViewsTestCase):
  637. """Tests for the redirect_to_login view"""
  638. @override_settings(LOGIN_URL=reverse_lazy('login'))
  639. def test_redirect_to_login_with_lazy(self):
  640. login_redirect_response = redirect_to_login(next='/else/where/')
  641. expected = '/login/?next=/else/where/'
  642. self.assertEqual(expected, login_redirect_response.url)
  643. @override_settings(LOGIN_URL=reverse_lazy('login'))
  644. def test_redirect_to_login_with_lazy_and_unicode(self):
  645. login_redirect_response = redirect_to_login(next='/else/where/झ/')
  646. expected = '/login/?next=/else/where/%E0%A4%9D/'
  647. self.assertEqual(expected, login_redirect_response.url)
  648. class LogoutTest(AuthViewsTestCase):
  649. def confirm_logged_out(self):
  650. self.assertNotIn(SESSION_KEY, self.client.session)
  651. def test_logout_default(self):
  652. "Logout without next_page option renders the default template"
  653. self.login()
  654. response = self.client.get('/logout/')
  655. self.assertContains(response, 'Logged out')
  656. self.confirm_logged_out()
  657. def test_14377(self):
  658. # Bug 14377
  659. self.login()
  660. response = self.client.get('/logout/')
  661. self.assertIn('site', response.context)
  662. def test_logout_with_overridden_redirect_url(self):
  663. # Bug 11223
  664. self.login()
  665. response = self.client.get('/logout/next_page/')
  666. self.assertEqual(response.status_code, 302)
  667. self.assertURLEqual(response.url, '/somewhere/')
  668. response = self.client.get('/logout/next_page/?next=/login/')
  669. self.assertEqual(response.status_code, 302)
  670. self.assertURLEqual(response.url, '/login/')
  671. self.confirm_logged_out()
  672. def test_logout_with_next_page_specified(self):
  673. "Logout with next_page option given redirects to specified resource"
  674. self.login()
  675. response = self.client.get('/logout/next_page/')
  676. self.assertEqual(response.status_code, 302)
  677. self.assertURLEqual(response.url, '/somewhere/')
  678. self.confirm_logged_out()
  679. def test_logout_with_redirect_argument(self):
  680. "Logout with query string redirects to specified resource"
  681. self.login()
  682. response = self.client.get('/logout/?next=/login/')
  683. self.assertEqual(response.status_code, 302)
  684. self.assertURLEqual(response.url, '/login/')
  685. self.confirm_logged_out()
  686. def test_logout_with_custom_redirect_argument(self):
  687. "Logout with custom query string redirects to specified resource"
  688. self.login()
  689. response = self.client.get('/logout/custom_query/?follow=/somewhere/')
  690. self.assertEqual(response.status_code, 302)
  691. self.assertURLEqual(response.url, '/somewhere/')
  692. self.confirm_logged_out()
  693. def test_logout_with_named_redirect(self):
  694. "Logout resolves names or URLs passed as next_page."
  695. self.login()
  696. response = self.client.get('/logout/next_page/named/')
  697. self.assertEqual(response.status_code, 302)
  698. self.assertURLEqual(response.url, '/password_reset/')
  699. self.confirm_logged_out()
  700. def test_security_check(self, password='password'):
  701. logout_url = reverse('logout')
  702. # Those URLs should not pass the security check
  703. for bad_url in ('http://example.com',
  704. 'http:///example.com',
  705. 'https://example.com',
  706. 'ftp://exampel.com',
  707. '///example.com',
  708. '//example.com',
  709. 'javascript:alert("XSS")'):
  710. nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
  711. 'url': logout_url,
  712. 'next': REDIRECT_FIELD_NAME,
  713. 'bad_url': urlquote(bad_url),
  714. }
  715. self.login()
  716. response = self.client.get(nasty_url)
  717. self.assertEqual(response.status_code, 302)
  718. self.assertNotIn(bad_url, response.url,
  719. "%s should be blocked" % bad_url)
  720. self.confirm_logged_out()
  721. # These URLs *should* still pass the security check
  722. for good_url in ('/view/?param=http://example.com',
  723. '/view/?param=https://example.com',
  724. '/view?param=ftp://exampel.com',
  725. 'view/?param=//example.com',
  726. 'https://testserver/',
  727. 'HTTPS://testserver/',
  728. '//testserver/',
  729. '/url%20with%20spaces/'): # see ticket #12534
  730. safe_url = '%(url)s?%(next)s=%(good_url)s' % {
  731. 'url': logout_url,
  732. 'next': REDIRECT_FIELD_NAME,
  733. 'good_url': urlquote(good_url),
  734. }
  735. self.login()
  736. response = self.client.get(safe_url)
  737. self.assertEqual(response.status_code, 302)
  738. self.assertIn(good_url, response.url, "%s should be allowed" % good_url)
  739. self.confirm_logged_out()
  740. def test_logout_preserve_language(self):
  741. """Check that language stored in session is preserved after logout"""
  742. # Create a new session with language
  743. engine = import_module(settings.SESSION_ENGINE)
  744. session = engine.SessionStore()
  745. session[LANGUAGE_SESSION_KEY] = 'pl'
  746. session.save()
  747. self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
  748. self.client.get('/logout/')
  749. self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'pl')
  750. # Redirect in test_user_change_password will fail if session auth hash
  751. # isn't updated after password change (#21649)
  752. @override_settings(
  753. PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
  754. ROOT_URLCONF='auth_tests.urls_admin',
  755. )
  756. class ChangelistTests(AuthViewsTestCase):
  757. def setUp(self):
  758. # Make me a superuser before logging in.
  759. User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True)
  760. self.login()
  761. self.admin = User.objects.get(pk=self.u1.pk)
  762. def get_user_data(self, user):
  763. return {
  764. 'username': user.username,
  765. 'password': user.password,
  766. 'email': user.email,
  767. 'is_active': user.is_active,
  768. 'is_staff': user.is_staff,
  769. 'is_superuser': user.is_superuser,
  770. 'last_login_0': user.last_login.strftime('%Y-%m-%d'),
  771. 'last_login_1': user.last_login.strftime('%H:%M:%S'),
  772. 'initial-last_login_0': user.last_login.strftime('%Y-%m-%d'),
  773. 'initial-last_login_1': user.last_login.strftime('%H:%M:%S'),
  774. 'date_joined_0': user.date_joined.strftime('%Y-%m-%d'),
  775. 'date_joined_1': user.date_joined.strftime('%H:%M:%S'),
  776. 'initial-date_joined_0': user.date_joined.strftime('%Y-%m-%d'),
  777. 'initial-date_joined_1': user.date_joined.strftime('%H:%M:%S'),
  778. 'first_name': user.first_name,
  779. 'last_name': user.last_name,
  780. }
  781. # #20078 - users shouldn't be allowed to guess password hashes via
  782. # repeated password__startswith queries.
  783. def test_changelist_disallows_password_lookups(self):
  784. # A lookup that tries to filter on password isn't OK
  785. with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls:
  786. response = self.client.get(reverse('auth_test_admin:auth_user_changelist') + '?password__startswith=sha1$')
  787. self.assertEqual(response.status_code, 400)
  788. self.assertEqual(len(logger_calls), 1)
  789. def test_user_change_email(self):
  790. data = self.get_user_data(self.admin)
  791. data['email'] = 'new_' + data['email']
  792. response = self.client.post(
  793. reverse('auth_test_admin:auth_user_change', args=(self.admin.pk,)),
  794. data
  795. )
  796. self.assertRedirects(response, reverse('auth_test_admin:auth_user_changelist'))
  797. row = LogEntry.objects.latest('id')
  798. self.assertEqual(row.change_message, 'Changed email.')
  799. def test_user_not_change(self):
  800. response = self.client.post(
  801. reverse('auth_test_admin:auth_user_change', args=(self.admin.pk,)),
  802. self.get_user_data(self.admin)
  803. )
  804. self.assertRedirects(response, reverse('auth_test_admin:auth_user_changelist'))
  805. row = LogEntry.objects.latest('id')
  806. self.assertEqual(row.change_message, 'No fields changed.')
  807. def test_user_change_password(self):
  808. user_change_url = reverse('auth_test_admin:auth_user_change', args=(self.admin.pk,))
  809. password_change_url = reverse('auth_test_admin:auth_user_password_change', args=(self.admin.pk,))
  810. response = self.client.get(user_change_url)
  811. # Test the link inside password field help_text.
  812. rel_link = re.search(
  813. r'you can change the password using <a href="([^"]*)">this form</a>',
  814. force_text(response.content)
  815. ).groups()[0]
  816. self.assertEqual(
  817. os.path.normpath(user_change_url + rel_link),
  818. os.path.normpath(password_change_url)
  819. )
  820. response = self.client.post(
  821. password_change_url,
  822. {
  823. 'password1': 'password1',
  824. 'password2': 'password1',
  825. }
  826. )
  827. self.assertRedirects(response, user_change_url)
  828. row = LogEntry.objects.latest('id')
  829. self.assertEqual(row.change_message, 'Changed password.')
  830. self.logout()
  831. self.login(password='password1')
  832. def test_user_change_different_user_password(self):
  833. u = User.objects.get(email='staffmember@example.com')
  834. response = self.client.post(
  835. reverse('auth_test_admin:auth_user_password_change', args=(u.pk,)),
  836. {
  837. 'password1': 'password1',
  838. 'password2': 'password1',
  839. }
  840. )
  841. self.assertRedirects(response, reverse('auth_test_admin:auth_user_change', args=(u.pk,)))
  842. row = LogEntry.objects.latest('id')
  843. self.assertEqual(row.user_id, self.admin.pk)
  844. self.assertEqual(row.object_id, str(u.pk))
  845. self.assertEqual(row.change_message, 'Changed password.')
  846. def test_password_change_bad_url(self):
  847. response = self.client.get(reverse('auth_test_admin:auth_user_password_change', args=('foobar',)))
  848. self.assertEqual(response.status_code, 404)
  849. @override_settings(
  850. AUTH_USER_MODEL='auth.UUIDUser',
  851. ROOT_URLCONF='auth_tests.urls_custom_user_admin',
  852. )
  853. class UUIDUserTests(TestCase):
  854. def test_admin_password_change(self):
  855. u = UUIDUser.objects.create_superuser(username='uuid', email='foo@bar.com', password='test')
  856. self.assertTrue(self.client.login(username='uuid', password='test'))
  857. user_change_url = reverse('custom_user_admin:auth_uuiduser_change', args=(u.pk,))
  858. response = self.client.get(user_change_url)
  859. self.assertEqual(response.status_code, 200)
  860. password_change_url = reverse('custom_user_admin:auth_user_password_change', args=(u.pk,))
  861. response = self.client.get(password_change_url)
  862. self.assertEqual(response.status_code, 200)
  863. # A LogEntry is created with pk=1 which breaks a FK constraint on MySQL
  864. with connection.constraint_checks_disabled():
  865. response = self.client.post(password_change_url, {
  866. 'password1': 'password1',
  867. 'password2': 'password1',
  868. })
  869. self.assertRedirects(response, user_change_url)
  870. row = LogEntry.objects.latest('id')
  871. self.assertEqual(row.user_id, 1) # harcoded in CustomUserAdmin.log_change()
  872. self.assertEqual(row.object_id, str(u.pk))
  873. self.assertEqual(row.change_message, 'Changed password.')