test_views.py 38 KB

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