Projet

Général

Profil

0001-settings-set-secure-flag-on-cookies-71880.patch

Benjamin Dauvergne, 30 novembre 2022 20:37

Télécharger (25,4 ko)

Voir les différences:

Subject: [PATCH 1/2] settings: set secure flag on cookies (#71880)

Tests fixes :
* force https scheme in webtest HTTP client
* add secure=True to call with the django HTTP client
* replace http scheme by https in URLs assertions,
* properly use response.form in tests directly using app.post, as CSRF checks on secure connection also test the Referrer
* manually add Referer header in other cases,
 src/authentic2/settings.py        |  5 ++++
 tests/api/test_all.py             | 16 ++++++-------
 tests/auth_fc/conftest.py         |  4 ++--
 tests/auth_fc/test_auth_fc.py     |  2 +-
 tests/auth_fc/test_auth_fc_api.py |  4 ++--
 tests/conftest.py                 |  6 +++--
 tests/idp_oidc/test_misc.py       |  4 ++--
 tests/settings.py                 |  2 +-
 tests/test_attribute_kinds.py     |  2 +-
 tests/test_auth_oidc.py           |  2 +-
 tests/test_commands.py            |  2 +-
 tests/test_csv_import.py          |  2 +-
 tests/test_idp_saml2.py           | 16 +++++++------
 tests/test_manager.py             | 40 ++++++++++++++++++++++---------
 tests/test_password_reset.py      | 15 ++++--------
 tests/test_registration.py        | 11 +++------
 tests/test_role_manager.py        |  8 +++++--
 tests/test_user_manager.py        |  4 +++-
 18 files changed, 84 insertions(+), 61 deletions(-)
src/authentic2/settings.py
55 55
    }
56 56
}
57 57

  
58
# Cookies
59
SESSION_COOKIE_SECURE = True
60
CSRF_COOKIE_SECURE = True
61
LANGUAGE_COOKIE_SECURE = True
62

  
58 63
# Hey Entr'ouvert is in France !!
59 64
TIME_ZONE = 'Europe/Paris'
60 65
LANGUAGE_CODE = 'fr'
tests/api/test_all.py
100 100
    Role.objects.create(name='Role4', service=service)
101 101

  
102 102
    # test failure when unlogged
103
    response = client.get('/api/user/', HTTP_ORIGIN='http://testserver')
103
    response = client.get('/api/user/', HTTP_ORIGIN='https://testserver', secure=True)
104 104
    assert response.content == b'{}'
105 105

  
106 106
    # login
107 107
    client.login(request=None, username='john.doe', password='password')
108
    response = client.get('/api/user/', HTTP_ORIGIN='http://testserver')
108
    response = client.get('/api/user/', HTTP_ORIGIN='https://testserver', secure=True)
109 109
    data = json.loads(force_str(response.content))
110 110
    assert isinstance(data, dict)
111 111
    assert set(data.keys()) == {
......
1055 1055
    assert response.json['user']['last_name'] == last_name
1056 1056
    assert check_password(password, response.json['user']['password'])
1057 1057
    assert response.json['token']
1058
    assert response.json['validation_url'].startswith('http://testserver/accounts/activate/')
1058
    assert response.json['validation_url'].startswith('https://testserver/accounts/activate/')
1059 1059
    assert User.objects.count() == 2
1060 1060
    user = User.objects.latest('id')
1061 1061
    assert user.ou == get_default_ou()
......
1114 1114
    assert response.json['user']['last_name'] == last_name
1115 1115
    assert check_password(password, response.json['user']['password'])
1116 1116
    assert response.json['token']
1117
    assert response.json['validation_url'].startswith('http://testserver/accounts/activate/')
1117
    assert response.json['validation_url'].startswith('https://testserver/accounts/activate/')
1118 1118
    assert User.objects.count() == 2
1119 1119
    user = User.objects.latest('id')
1120 1120
    assert user.username == username
......
1232 1232
    assert len(mailoutbox) == 1
1233 1233
    mail = mailoutbox[0]
1234 1234
    assert mail.to[0] == email
1235
    assert 'http://testserver/password/reset/confirm/' in mail.body
1235
    assert 'https://testserver/password/reset/confirm/' in mail.body
1236 1236
    assert_event('manager.user.password.reset.request', user=admin, api=True)
1237 1237

  
1238 1238

  
......
1265 1265
    mail = mailoutbox[0]
1266 1266

  
1267 1267
    assert mail.to[0] == new_email
1268
    assert 'http://testserver/accounts/change-email/verify/' in mail.body
1268
    assert 'https://testserver/accounts/change-email/verify/' in mail.body
1269 1269

  
1270 1270

  
1271 1271
def test_api_delete_role(app, admin_ou1, role_ou1):
......
2391 2391
    assert len(resp.json['data']) == 6
2392 2392
    login_stats = {
2393 2393
        'name': 'Login count by authentication type',
2394
        'url': 'http://testserver/api/statistics/login/',
2394
        'url': 'https://testserver/api/statistics/login/',
2395 2395
        'id': 'login',
2396 2396
        'filters': [
2397 2397
            {
......
2411 2411
    assert login_stats in resp.json['data']
2412 2412
    assert {
2413 2413
        'name': 'Login count by service',
2414
        'url': 'http://testserver/api/statistics/service_login/',
2414
        'url': 'https://testserver/api/statistics/service_login/',
2415 2415
        'id': 'service-login',
2416 2416
        'filters': [
2417 2417
            {
tests/auth_fc/conftest.py
80 80

  
81 81
    @property
82 82
    def callback_url(self):
83
        return 'http://testserver' + reverse('fc-login-or-link')
83
        return 'https://testserver' + reverse('fc-login-or-link')
84 84

  
85 85
    def login_with_fc_fixed_params(self, app):
86 86
        if app.session:
......
154 154
        assert url.startswith('https://fcp.integ01.dev-franceconnect.fr/api/v1/logout')
155 155
        parsed_url = urllib.parse.urlparse(url)
156 156
        query = QueryDict(parsed_url.query)
157
        assert_equals_url(query['post_logout_redirect_uri'], 'http://testserver' + reverse('fc-logout'))
157
        assert_equals_url(query['post_logout_redirect_uri'], 'https://testserver' + reverse('fc-logout'))
158 158
        assert query['state']
159 159
        self.state = query['state']
160 160
        return app.get(reverse('fc-logout') + '?state=' + self.state)
tests/auth_fc/test_auth_fc.py
128 128
    for body in (mailoutbox[0].body, mailoutbox[0].alternatives[0][0]):
129 129
        assert 'Hi AnonymousUser,' in body
130 130
        assert 'You have just created an account using FranceConnect.' in body
131
        assert 'http://testserver/login/' in body
131
        assert 'https://testserver/login/' in body
132 132

  
133 133
    assert user.verified_attributes.first_name == 'Ÿuñe'
134 134
    assert user.verified_attributes.last_name == 'Frédérique'
tests/auth_fc/test_auth_fc_api.py
43 43
    content = response.json['franceconnect']
44 44
    assert isinstance(content, dict), 'franceconnect field is not a dict'
45 45
    assert content.get('linked') is True
46
    assert content.get('link_url').startswith('http://')
46
    assert content.get('link_url').startswith('https://')
47 47
    assert content.get('link_url').endswith('/callback/')
48
    assert content.get('unlink_url').startswith('http://')
48
    assert content.get('unlink_url').startswith('https://')
49 49
    assert content.get('unlink_url').endswith('/unlink/')
50 50

  
51 51
    unlink_url = '/api/users/%s/fc-unlink/' % simple_user.uuid
tests/conftest.py
63 63
    try:
64 64

  
65 65
        def factory(hostname='testserver'):
66
            return django_webtest.DjangoTestApp(extra_environ={'HTTP_HOST': hostname})
66
            return django_webtest.DjangoTestApp(
67
                extra_environ={'HTTP_HOST': hostname, 'wsgi.url_scheme': 'https'}
68
            )
67 69

  
68 70
        yield factory
69 71
    finally:
......
420 422
    else:
421 423

  
422 424
        def check_location(response, default_return):
423
            assert urllib.parse.urljoin('http://testserver/', default_return).endswith(response['Location'])
425
            assert urllib.parse.urljoin('https://testserver/', default_return).endswith(response['Location'])
424 426

  
425 427
    return check_location
426 428

  
tests/idp_oidc/test_misc.py
353 353
    with open('tests/200x200.jpg', 'rb') as fd:
354 354
        simple_user.attributes.cityscape_image = File(fd)
355 355
    response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
356
    assert response.json['cityscape_image'].startswith('http://testserver/media/profile-image/')
356
    assert response.json['cityscape_image'].startswith('https://testserver/media/profile-image/')
357 357

  
358 358
    # check against a user without username
359 359
    simple_user.username = None
......
381 381
            src = iframes.attr('src')
382 382
            assert '?' in src
383 383
            src_qd = QueryDict(src.split('?', 1)[1])
384
            assert 'iss' in src_qd and src_qd['iss'] == 'http://testserver/'
384
            assert 'iss' in src_qd and src_qd['iss'] == 'https://testserver/'
385 385
            assert 'sid' in src_qd and src_qd['sid'] == get_session_id(
386 386
                mock.Mock(session=app.session), oidc_client
387 387
            )
tests/settings.py
55 55
TEMPLATES[0]['DIRS'].append('tests/templates')  # pylint: disable=undefined-variable
56 56
TEMPLATES[0]['OPTIONS']['debug'] = True  # pylint: disable=undefined-variable
57 57

  
58
SITE_BASE_URL = 'http://testserver'
58
SITE_BASE_URL = 'https://testserver'
59 59

  
60 60
A2_MAX_EMAILS_PER_IP = None
61 61
A2_MAX_EMAILS_FOR_ADDRESS = None
tests/test_attribute_kinds.py
541 541
    response = app.get('/api/users/%s/' % john().uuid)
542 542
    assert (
543 543
        response.json['cityscape_image']
544
        == 'http://testserver/media/%s' % john().attributes.cityscape_image.name
544
        == 'https://testserver/media/%s' % john().attributes.cityscape_image.name
545 545
    )
546 546
    app.authorization = None
547 547

  
tests/test_auth_oidc.py
511 511
    assert query['response_type'] == 'code'
512 512
    assert query['client_id'] == str(oidc_provider.client_id)
513 513
    assert query['scope'] == 'openid'
514
    assert query['redirect_uri'] == 'http://testserver' + reverse('oidc-login-callback')
514
    assert query['redirect_uri'] == 'https://testserver' + reverse('oidc-login-callback')
515 515
    nonce = query['nonce']
516 516

  
517 517
    if oidc_provider.claims_parameter_supported:
tests/test_commands.py
256 256
    simple_user.save()
257 257
    call_command('clean-unused-accounts')
258 258
    mail = mailoutbox[0]
259
    assert 'href="http://testserver/login/"' in mail.message().as_string()
259
    assert 'href="https://testserver/login/"' in mail.message().as_string()
260 260

  
261 261

  
262 262
def test_clean_unused_account_with_no_email(simple_user, mailoutbox, caplog):
tests/test_csv_import.py
559 559
    assert importer.run()
560 560
    thomas = User.objects.get(email='tnoel@entrouvert.com')
561 561
    assert len(mail.outbox) == 1
562
    assert 'http://testserver/password/reset/confirm/' in mail.outbox[0].body
562
    assert 'https://testserver/password/reset/confirm/' in mail.outbox[0].body
563 563

  
564 564
    password = thomas.password
565 565
    del mail.outbox[0]
tests/test_idp_saml2.py
149 149
        self.base_url = 'https://sp.example.com'
150 150
        self.name = 'Test SP'
151 151
        self.slug = 'test-sp'
152
        self.idp_entity_idp = ('http://testserver/idp/saml2/metadata',)
152
        self.idp_entity_idp = ('https://testserver/idp/saml2/metadata',)
153 153
        self.default_name_id_format = 'email'
154 154
        self.accepted_name_id_format = ['email', 'persistent', 'transient', 'username']
155 155
        self.ou = OrganizationalUnit.objects.get()
......
504 504
            ),
505 505
            (
506 506
                "/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/saml:AttributeValue",
507
                re.compile('^http://testserver/media/profile-image/.*$'),
507
                re.compile('^https://testserver/media/profile-image/.*$'),
508 508
            ),
509 509
            (
510 510
                "/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='verified_attributes']/@NameFormat",
......
662 662
        with mock.patch(
663 663
            'authentic2.idp.saml.saml2_endpoints.get_attributes', wraps=saml2_endpoints.get_attributes
664 664
        ) as get_attributes:
665
            request = rf.get('/')
665
            request = rf.get('/', secure=True)
666 666
            request.user = None
667 667
            assertion = lasso.Saml2Assertion()
668 668
            provider = Service(ou=None)
......
815 815
        == '_' + hashlib.sha256(b'b' + b'https://sp.com/' + b'a').hexdigest().upper()
816 816
    )
817 817

  
818
    edpt = saml2_endpoints.make_edu_person_targeted_id('http://testserver/idp/saml2/metadata', provider, user)
818
    edpt = saml2_endpoints.make_edu_person_targeted_id(
819
        'https://testserver/idp/saml2/metadata', provider, user
820
    )
819 821
    assert edpt is not None
820 822
    node = lasso.Node.newFromXmlNode(force_str(ET.tostring(edpt)))
821 823
    assert isinstance(node, lasso.Saml2NameID)
822 824
    assert force_str(node.content) == '_A485C0ACEEF43A6D39145F5CFE25D9D3B6F15DC6443F412263C76D81C72DA8D5'
823 825
    assert node.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
824 826

  
825
    assert node.nameQualifier == 'http://testserver/idp/saml2/metadata'
827
    assert node.nameQualifier == 'https://testserver/idp/saml2/metadata'
826 828
    assert node.spNameQualifier == 'https://sp.com/'
827 829

  
828 830

  
......
854 856
    assert isinstance(node, lasso.Saml2NameID)
855 857
    assert force_str(node.content) == '_A485C0ACEEF43A6D39145F5CFE25D9D3B6F15DC6443F412263C76D81C72DA8D5'
856 858
    assert node.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
857
    assert node.nameQualifier == 'http://testserver/idp/saml2/metadata'
859
    assert node.nameQualifier == 'https://testserver/idp/saml2/metadata'
858 860
    assert node.spNameQualifier == 'https://sp.com/'
859 861

  
860 862

  
......
886 888
    assert isinstance(node, lasso.Saml2NameID)
887 889
    assert force_str(node.content) == '_A485C0ACEEF43A6D39145F5CFE25D9D3B6F15DC6443F412263C76D81C72DA8D5'
888 890
    assert node.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
889
    assert node.nameQualifier == 'http://testserver/idp/saml2/metadata'
891
    assert node.nameQualifier == 'https://testserver/idp/saml2/metadata'
890 892
    assert node.spNameQualifier == 'https://sp.com/'
891 893

  
892 894

  
tests/test_manager.py
1034 1034
    assert q('table tbody tr td .icon-remove-sign')
1035 1035
    token = str(response.context['csrf_token'])
1036 1036
    params = {'action': 'remove', 'user_or_role': 'user-%s' % admin.pk, 'csrfmiddlewaretoken': token}
1037
    app.post('/manage/roles/%s/' % simple_role.pk, params=params)
1037
    app.post('/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'})
1038 1038
    assert simple_role not in admin.roles.all()
1039 1039

  
1040 1040
    # user can act on role inheritance
......
1049 1049
    response = app.get('/manage/roles/%s/children/' % simple_role.pk)
1050 1050
    token = str(response.context['csrf_token'])
1051 1051
    params = {'action': 'add', 'role': role.pk, 'csrfmiddlewaretoken': token}
1052
    response = app.post('/manage/roles/%s/children/' % simple_role.pk, params=params)
1052
    response = app.post(
1053
        '/manage/roles/%s/children/' % simple_role.pk,
1054
        params=params,
1055
        headers={'Referer': 'https://testserver/'},
1056
    )
1053 1057
    assert role in simple_role.children()
1054 1058

  
1055 1059
    params = {'action': 'remove', 'role': role.pk, 'csrfmiddlewaretoken': token}
1056
    response = app.post('/manage/roles/%s/children/' % simple_role.pk, params=params)
1060
    response = app.post(
1061
        '/manage/roles/%s/children/' % simple_role.pk,
1062
        params=params,
1063
        headers={'Referer': 'https://testserver/'},
1064
    )
1057 1065
    assert role not in simple_role.children()
1058 1066

  
1059 1067
    response = app.get('/manage/roles/%s/parents/' % role.pk)
1060 1068
    token = str(response.context['csrf_token'])
1061 1069
    params = {'action': 'add', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
1062
    response = app.post('/manage/roles/%s/parents/' % role.pk, params=params)
1070
    response = app.post(
1071
        '/manage/roles/%s/parents/' % role.pk, params=params, headers={'Referer': 'https://testserver/'}
1072
    )
1063 1073
    assert simple_role in role.parents()
1064 1074

  
1065 1075
    params = {'action': 'remove', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
1066
    response = app.post('/manage/roles/%s/parents/' % role.pk, params=params)
1076
    response = app.post(
1077
        '/manage/roles/%s/parents/' % role.pk, params=params, headers={'Referer': 'https://testserver/'}
1078
    )
1067 1079
    assert simple_role not in role.parents()
1068 1080

  
1069 1081
    # user can add role as a member through role members form
......
1078 1090
    assert q('table tbody tr td .icon-remove-sign')
1079 1091
    token = str(response.context['csrf_token'])
1080 1092
    params = {'action': 'remove', 'user_or_role': 'role-%s' % role.pk, 'csrfmiddlewaretoken': token}
1081
    app.post('/manage/roles/%s/' % simple_role.pk, params=params)
1093
    app.post('/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'})
1082 1094
    assert role not in simple_role.children()
1083 1095

  
1084 1096
    # try to add arbitrary role
......
1086 1098
    response = app.get('/manage/roles/%s/parents/' % role.pk)
1087 1099
    token = str(response.context['csrf_token'])
1088 1100
    params = {'action': 'add', 'role': admin_role.pk, 'csrfmiddlewaretoken': token}
1089
    response = app.post('/manage/roles/%s/parents/' % simple_role.pk, params=params)
1101
    response = app.post(
1102
        '/manage/roles/%s/parents/' % simple_role.pk,
1103
        params=params,
1104
        headers={'Referer': 'https://testserver/'},
1105
    )
1090 1106
    assert admin_role not in role.parents()
1091 1107

  
1092 1108
    # user roles view works
......
1097 1113

  
1098 1114
    token = str(response.context['csrf_token'])
1099 1115
    params = {'action': 'add', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
1100
    response = app.post('/manage/users/%s/roles/' % admin.pk, params=params)
1116
    response = app.post(
1117
        '/manage/users/%s/roles/' % admin.pk, params=params, headers={'Referer': 'https://testserver/'}
1118
    )
1101 1119
    assert simple_role in admin.roles.all()
1102 1120

  
1103 1121
    app.get('/manage/roles/add/', status=403)
......
1306 1324
        {
1307 1325
            'label': 'Identity management',
1308 1326
            'slug': 'identity-management',
1309
            'url': 'http://testserver/manage/',
1327
            'url': 'https://testserver/manage/',
1310 1328
            'sub': False,
1311 1329
        },
1312
        {'label': 'Users', 'slug': 'users', 'url': 'http://testserver/manage/users/', 'sub': True},
1313
        {'label': 'Roles', 'slug': 'roles', 'url': 'http://testserver/manage/roles/', 'sub': True},
1330
        {'label': 'Users', 'slug': 'users', 'url': 'https://testserver/manage/users/', 'sub': True},
1331
        {'label': 'Roles', 'slug': 'roles', 'url': 'https://testserver/manage/roles/', 'sub': True},
1314 1332
    ]
1315 1333

  
1316 1334
    response = login(app, admin)
tests/test_password_reset.py
176 176
    for body in (mail.body, mail.alternatives[0][0]):
177 177
        assert 'no account was found associated with this address' in body
178 178
        if settings.REGISTRATION_OPEN:
179
            assert 'http://testserver/register/' in body
179
            assert 'https://testserver/register/' in body
180 180
            # check next_url was preserved
181 181
            assert 'next=/whatever/' in body
182 182
        else:
183
            assert 'http://testserver/register/' not in body
183
            assert 'https://testserver/register/' not in body
184 184

  
185 185

  
186 186
def test_send_password_reset_email_disabled_account(app, simple_user, mailoutbox):
......
209 209

  
210 210
    url = reverse('password_reset')
211 211
    response = app.get(url, status=200)
212
    response = app.post(
213
        url,
214
        params={
215
            'email': 'testbot@entrouvert.com',
216
            'csrfmiddlewaretoken': response.context['csrf_token'],
217
            'robotcheck': 'a',
218
        },
219
    )
212
    response.form.set('email', 'testbot@entrouvert.com')
213
    response.form.set('robotcheck', True)
214
    response = response.form.submit()
220 215
    response = response.follow()
221 216
    assert len(mailoutbox) == 0
222 217
    assert 'Your password reset request has been refused' in response
tests/test_registration.py
812 812
    settings.DEFAULT_FROM_EMAIL = 'show only addr <noreply@example.net>'
813 813

  
814 814
    response = app.get(utils_misc.make_url('registration_register'))
815
    response = app.post(
816
        utils_misc.make_url('registration_register'),
817
        params={
818
            'email': 'testbot@entrouvert.com',
819
            'csrfmiddlewaretoken': response.context['csrf_token'],
820
            'robotcheck': 'a',
821
        },
822
    )
815
    response.form.set('email', 'testbot@entrouvert.com')
816
    response.form.set('robotcheck', True)
817
    response = response.form.submit()
823 818
    response = response.follow()
824 819
    assert len(mailoutbox) == 0
825 820
    assert 'Your registration request has been refused' in response
tests/test_role_manager.py
622 622
    # simulate click on Jôhn Dôe delete icon
623 623
    token = str(resp.context['csrf_token'])
624 624
    params = {'action': 'remove', 'user_or_role': 'user-%s' % simple_user.pk, 'csrfmiddlewaretoken': token}
625
    resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow()
625
    resp = app.post(
626
        '/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'}
627
    ).follow()
626 628
    assert 'Jôhn Dôe' not in resp.text
627 629

  
628 630
    # simulate click on role_ou1 delete icon
629 631
    token = str(resp.context['csrf_token'])
630 632
    params = {'action': 'remove', 'user_or_role': 'role-%s' % role_ou1.pk, 'csrfmiddlewaretoken': token}
631
    resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow()
633
    resp = app.post(
634
        '/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'}
635
    ).follow()
632 636
    assert 'role_ou1' not in resp.text
633 637

  
634 638
    # invalid choices are ignored
tests/test_user_manager.py
1174 1174
    # cannot click it's JS :/
1175 1175
    token = str(resp.context['csrf_token'])
1176 1176
    params = {'authorization': auth.pk, 'csrfmiddlewaretoken': token}
1177
    resp = app.post(user_authorizations_url, params=params, status=302)
1177
    resp = app.post(
1178
        user_authorizations_url, params=params, status=302, headers={'Referer': 'https://testserver/'}
1179
    )
1178 1180
    assert OIDCAuthorization.objects.count() == 0
1179 1181
    resp = resp.follow()
1180 1182
    assert resp.html.find('td').text == 'This user has not granted profile data access to any service yet.'
1181
-