Projet

Général

Profil

0003-add-FranceConnect-authentication-method-fixes-14510.patch

Benjamin Dauvergne, 07 janvier 2017 14:45

Télécharger (9,78 ko)

Voir les différences:

Subject: [PATCH 3/4] add FranceConnect authentication method (fixes #14510)

 tests/test_fc_auth.py   | 205 ++++++++++++++++++++++++++++++++++++++++++++++++
 wcs/admin/settings.py   |   2 +
 wcs/qommon/publisher.py |   2 +
 wcs/qommon/sessions.py  |  12 +++
 4 files changed, 221 insertions(+)
 create mode 100644 tests/test_fc_auth.py
tests/test_fc_auth.py
1
import urlparse
2
import base64
3
import json
4
import urllib
5

  
6
from quixote import cleanup, get_session_manager
7

  
8
from utilities import get_app, create_temporary_pub
9
import mock
10

  
11
PROFILE = {
12
    'fields': [
13
      {
14
        'kind': 'string',
15
        'description': '',
16
        'required': True,
17
        'user_visible': True,
18
        'label': u'Prenoms',
19
        'disabled': False,
20
        'user_editable': True,
21
        'asked_on_registration': True,
22
        'name': 'prenoms'
23
      },
24
      {
25
        'kind': 'string',
26
        'description': '',
27
        'required': True,
28
        'user_visible': True,
29
        'label': 'Nom',
30
        'disabled': False,
31
        'user_editable': True,
32
        'asked_on_registration': True,
33
        'name': 'nom'
34
      },
35
      {
36
        'kind': 'string',
37
        'description': '',
38
        'required': True,
39
        'user_visible': True,
40
        'label': 'Email',
41
        'disabled': False,
42
        'user_editable': True,
43
        'asked_on_registration': True,
44
        'name': 'email'
45
      },
46
    ]
47
}
48

  
49

  
50
def base64url_encode(v):
51
    return base64.urlsafe_b64encode(v).strip('=')
52

  
53

  
54
def setup_module(module):
55
    cleanup()
56
    global pub
57
    pub = create_temporary_pub()
58

  
59

  
60
def setup_profile_environement(pub):
61
    if not pub.cfg:
62
        pub.cfg = {}
63
    # create some roles
64
    from wcs.ctl.check_hobos import CmdCheckHobos
65

  
66
    # setup an hobo profile
67
    CmdCheckHobos().update_profile(PROFILE, pub)
68
    pub.cfg['users']['field_name'] = ['_prenoms', '_nom']
69
    pub.user_class.wipe()
70
    pub.write_cfg()
71

  
72
FC_CONFIG = {
73
    'client_id': '123',
74
    'client_secret': 'xyz',
75
    'platform': 'dev-particulier',
76
    'scopes': 'identite_pivot',
77
    'user_field_mappings': [
78
        {
79
            'field_varname': 'prenoms',
80
            'value': '[given_name]',
81
            'verified': 'always',
82
        },
83
        {
84
            'field_varname': 'nom',
85
            'value': '[family_name]',
86
            'verified': 'always',
87
        },
88
        {
89
            'field_varname': 'email',
90
            'value': '[email]',
91
            'verified': 'always',
92
        },
93
    ]
94

  
95
}
96

  
97

  
98
def setup_fc_environment(pub):
99
    if not pub.cfg:
100
        pub.cfg = {}
101
    pub.cfg['identification'] = {
102
        'methods': ['fc'],
103
    }
104
    pub.cfg['fc'] = FC_CONFIG
105
    pub.user_class.wipe()
106
    pub.write_cfg()
107

  
108

  
109
def test_fc_login_page():
110
    setup_profile_environement(pub)
111
    setup_fc_environment(pub)
112
    app = get_app(pub)
113
    resp = app.get('/')
114
    resp = app.get('/login/')
115
    assert resp.status_int == 302
116
    assert resp.location.startswith('https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize')
117
    qs = urlparse.parse_qs(resp.location.split('?')[1])
118
    nonce = qs['nonce'][0]
119
    state = qs['state'][0]
120

  
121
    id_token = {
122
        'nonce': nonce,
123
    }
124
    token_result = {
125
        'access_token': 'abcd',
126
        'id_token': '.%s.' % base64url_encode(json.dumps(id_token)),
127
    }
128
    user_info_result = {
129
        'sub': 'ymca',
130
        'given_name': 'John',
131
        'family_name': 'Doe',
132
        'email': 'john.doe@example.com',
133
    }
134

  
135
    assert pub.user_class.count() == 0
136
    with mock.patch('qommon.ident.franceconnect.http_post_request') as http_post_request, \
137
            mock.patch('qommon.ident.franceconnect.http_get_page') as http_get_page:
138
        http_post_request.return_value = (None, 200, json.dumps(token_result), None)
139
        http_get_page.return_value = (None, 200, json.dumps(user_info_result), None)
140
        resp = app.get('/ident/fc/callback?%s' % urllib.urlencode({
141
            'code': '1234', 'state': state,
142
        }))
143
    assert pub.user_class.count() == 1
144
    user = pub.user_class.select()[0]
145
    assert user.form_data == {'_email': 'john.doe@example.com', '_nom': 'Doe', '_prenoms': 'John'}
146
    assert set(user.verified_fields) == set(['_nom', '_prenoms', '_email'])
147
    assert user.email == 'john.doe@example.com'
148
    assert user.name_identifiers == ['ymca']
149
    assert user.name == 'John Doe'
150

  
151
    # Verify we are logged in
152
    session_id = app.cookies.values()[0].strip('"')
153
    session = get_session_manager().session_class.get(session_id)
154
    assert session.user == user.id
155
    assert session.extra_variables['fc_user_given_name'] == 'John'
156
    assert session.extra_variables['fc_user_family_name'] == 'Doe'
157
    assert session.extra_variables['fc_user_email'] == 'john.doe@example.com'
158
    assert session.extra_variables['fc_user_sub'] == 'ymca'
159

  
160
    # Login existing user
161
    resp = app.get('/logout')
162
    resp = app.get('/login/')
163
    with mock.patch('qommon.ident.franceconnect.http_post_request') as http_post_request, \
164
            mock.patch('qommon.ident.franceconnect.http_get_page') as http_get_page:
165
        http_post_request.return_value = (None, 200, json.dumps(token_result), None)
166
        http_get_page.return_value = (None, 200, json.dumps(user_info_result), None)
167
        resp = app.get('/ident/fc/callback?%s' % urllib.urlencode({
168
            'code': '1234', 'state': state,
169
        }))
170
    new_session_id = app.cookies.values()[0].strip('"')
171
    assert session_id != new_session_id, 'no new session created'
172
    session = get_session_manager().session_class.get(new_session_id)
173
    assert pub.user_class.count() == 1, 'existing user has not been used'
174

  
175

  
176
def test_fc_settings():
177
    setup_profile_environement(pub)
178
    app = get_app(pub)
179
    resp = app.get('/backoffice/settings/identification/')
180
    resp.forms[0]['methods$elementfc'].checked = True
181
    resp = resp.forms[0].submit().follow()
182

  
183
    assert 'FranceConnect' in resp.body
184
    resp = resp.click('FranceConnect')
185
    resp = resp.forms[0].submit('user_field_mappings$add_element')
186
    resp = resp.forms[0].submit('user_field_mappings$add_element')
187
    resp.forms[0]['client_id'].value = '123'
188
    resp.forms[0]['client_secret'].value = 'xyz'
189
    resp.forms[0]['platform'].value = 'Development citizens'
190
    resp.forms[0]['scopes'].value = 'identite_pivot'
191

  
192
    resp.forms[0]['user_field_mappings$element0$field_varname'] = 'prenoms'
193
    resp.forms[0]['user_field_mappings$element0$value'] = '[given_name]'
194
    resp.forms[0]['user_field_mappings$element0$verified'] = 'Always'
195

  
196
    resp.forms[0]['user_field_mappings$element1$field_varname'] = 'nom'
197
    resp.forms[0]['user_field_mappings$element1$value'] = '[family_name]'
198
    resp.forms[0]['user_field_mappings$element1$verified'] = 'Always'
199

  
200
    resp.forms[0]['user_field_mappings$element2$field_varname'] = 'email'
201
    resp.forms[0]['user_field_mappings$element2$value'] = '[email]'
202
    resp.forms[0]['user_field_mappings$element2$verified'] = 'Always'
203

  
204
    resp = resp.forms[0].submit('submit').follow()
205
    assert pub.cfg['fc'] == FC_CONFIG
wcs/admin/settings.py
71 71
        if lasso is not None:
72 72
            methods.insert(0,
73 73
                    ('idp', _('Delegated to SAML identity provider'), 'idp'))
74
            methods.insert(0,
75
                    ('fc', _('Delegated to France Connect'), 'fc'))
74 76
        form.add(CheckboxesWidget, 'methods', title = _('Methods'),
75 77
                value=identification_cfg.get('methods'),
76 78
                options=methods,
wcs/qommon/publisher.py
669 669
        if lasso:
670 670
            import qommon.ident.idp
671 671
            classes.append(qommon.ident.idp.IdPAuthMethod)
672
        import qommon.ident.franceconnect
673
        classes.append(qommon.ident.franceconnect.FCAuthMethod)
672 674
        import qommon.ident.password
673 675
        classes.append(qommon.ident.password.PasswordAuthMethod)
674 676
        self.ident_methods = {}
wcs/qommon/sessions.py
81 81
    jsonp_display_values = None
82 82
    extra_variables = None
83 83
    expire = None
84
    extra_attributes = None
84 85

  
85 86
    username = None # only set on password authentication
86 87

  
......
114 115
            self.extra_variables or \
115 116
            CaptchaSession.has_info(self) or \
116 117
            self.expire or \
118
            self.extra_attributes or \
117 119
            QuixoteSession.has_info(self)
118 120
    is_dirty = has_info
119 121

  
......
238 240
                d[prefix + k] = v
239 241
        return d
240 242

  
243
    def set_extra_attribute(self, key, value):
244
        self.extra_attributes = self.extra_attributes = {}
245
        self.extra_attributes[key] = value
246

  
247
    def get_extra_attribute(self, key, default=None):
248
        return (self.extra_attributes or {}).get(key, default)
249

  
250
    def pop_extra_attribute(self, key, default=None):
251
        return (self.extra_attributes or {}).pop(key, default)
252

  
241 253

  
242 254
class QommonSessionManager(QuixoteSessionManager):
243 255
    def start_request(self):
244
-