Search code examples
djangoseleniumcookiescsrfdjango-csrf

Why is Selenium causing a CSRF 403?


I'm trying to create a simple login test using Django and Selenium, but getting a 403 due to a CSRF failure. I'm expecting the middleware to add the cookie on the GET request and then parse it back out on the POST.

Here's what I've checked so far:

1. Is the cookie being set on the GET request to /accounts/login/?

Yes, the cookie is being set in the process_response method

2. Is the cookie available on the Selenium driver?

Yes

ipdb> self.selenium.get_cookies()
[{u'domain': u'localhost', u'name': u'csrftoken', u'value': u'DzNbEn9kZw0WZQ4OsRLouriFN5MOIQos', u'expiry': 1470691410, u'path': u'/', u'httpOnly': False, u'secure': True}]

3. Is the cookie found during the POST request?

No, this try/except from django.middleware.CsrfViewMiddleware.process_view fails:

source

try:
    csrf_token = _sanitize_token(
        request.COOKIES[settings.CSRF_COOKIE_NAME])
    # Use same token next time
    request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
    csrf_token = None
    # Generate token and store it in the request, so it's
    # available to the view.
    request.META["CSRF_COOKIE"] = _get_new_csrf_key()

Code

class TestLogin(StaticLiveServerTestCase):

    @classmethod
    def setUpClass(cls):
        cls.selenium = getattr(webdriver, settings.SELENIUM_WEBDRIVER)()
        cls.selenium.maximize_window()
        cls.selenium.implicitly_wait(5)

        super(TestLogin, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()

        super(TestLogin, cls).tearDownClass()

    def test_login(self):

        self.selenium.get('{}{}'.format(self.live_server_url, '/accounts/login/?next=/'))
        assert "Django" in self.selenium.title
        un_el = self.selenium.find_element_by_id('id_username').send_keys('the_un')
        pw_el = self.selenium.find_element_by_id('id_password')
        pw_el.send_keys('the_pw')
        pw_el.send_keys(Keys.RETURN)

        try:
            WebDriverWait(self.selenium, 5).until(EC.title_contains("New Title"))
        except TimeoutException as e:
            msg = "Could not find 'New Title' in title. Current title: {}".format(self.selenium.title)
            raise TimeoutException(msg)
        finally:
            self.selenium.quit()

Question

What can I try next to debug this?


Solution

  • Oldish question, but after getting stuck with this for a few hours the answer was simple.

    From the docs:

    If a browser connects initially via HTTP, which is the default for most browsers, it is possible for existing cookies to be leaked. For this reason, you should set your SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE settings to True. This instructs the browser to only send these cookies over HTTPS connections. Note that this will mean that sessions will not work over HTTP, and the CSRF protection will prevent any POST data being accepted over HTTP (which will be fine if you are redirecting all HTTP traffic to HTTPS).

    Like me, you are probably using django_extensions + Werkzeug for the majority of your work, and are by default running all of your local work over SSL.

    If you're using unittest or Djangos version of it, I'd recommend that you modify these settings at test runtime, like so:

    ...
    from django.conf import settings
    
    class ProfilePagetest(LiveServerTestCase):
    
        def setUp(self):
    
            settings.CSRF_COOKIE_SECURE = False
            settings.SESSION_COOKIE_SECURE = False
    
            self.url = reverse('clientpage:profile')
            self.username = '[email protected]'
            self.password = 'strange decisions...'
            get_user_model().objects.create_user(self.username, self.username, self.password)
            self.browser = webdriver.Firefox()
    

    This should stop the CSRF validation issues.