Projet

Général

Profil

0001-keep-authentication-context-fixes-21908.patch

Benjamin Dauvergne, 18 septembre 2018 17:54

Télécharger (32,1 ko)

Voir les différences:

Subject: [PATCH 1/2] keep authentication context (fixes #21908)

- simplify and reorganize login templates,
- URL are not built inside templates anymore,
- we have now 3 different templates:
  - login.html for the login page
  - registration.html for the registration page
  - linking.html for the account page
- using feature from #25623, authentication_method is kept by the
  registration view.
- the service slug is correctly threaded between every views.
- explanations about FranceConnect are now done in a common template
  "explanation.html".
- restore popup mode, use it through setting A2_FC_POPUP=True, it works
  for:
  - login and login with registration (workflow for login with
    registration is a bit complicated),
  - registration,
  - and linking (linking your existing to FC through the "My account"
   page)
  unlinking is not handled with a popup.
 src/authentic2_auth_fc/app_settings.py        |   4 +
 src/authentic2_auth_fc/auth_frontends.py      |  61 +++++-
 .../static/authentic2_auth_fc/js/fc.js        |  30 +++
 .../authentic2_auth_fc/connecting.html        |  23 ---
 .../authentic2_auth_fc/explanation.html       |  10 +
 .../templates/authentic2_auth_fc/linking.html |   5 +-
 .../templates/authentic2_auth_fc/login.html   |  17 +-
 .../login_registration.html                   |  28 +++
 .../authentic2_auth_fc/registration.html      |  16 ++
 src/authentic2_auth_fc/views.py               |  53 ++++--
 tests/conftest.py                             |  29 +++
 tests/test_auth_fc.py                         | 175 +++++++++++++++++-
 12 files changed, 391 insertions(+), 60 deletions(-)
 create mode 100644 src/authentic2_auth_fc/static/authentic2_auth_fc/js/fc.js
 delete mode 100644 src/authentic2_auth_fc/templates/authentic2_auth_fc/connecting.html
 create mode 100644 src/authentic2_auth_fc/templates/authentic2_auth_fc/explanation.html
 create mode 100644 src/authentic2_auth_fc/templates/authentic2_auth_fc/login_registration.html
 create mode 100644 src/authentic2_auth_fc/templates/authentic2_auth_fc/registration.html
src/authentic2_auth_fc/app_settings.py
108 108
    def scopes(self):
109 109
        return self._setting('SCOPES', [])
110 110

  
111
    @property
112
    def popup(self):
113
        return self._setting('POPUP', False)
114

  
111 115

  
112 116
import sys
113 117

  
src/authentic2_auth_fc/auth_frontends.py
2 2
from django.template.loader import render_to_string
3 3
from django.shortcuts import render
4 4

  
5
from authentic2 import app_settings as a2_app_settings
5
from authentic2 import app_settings as a2_app_settings, utils as a2_utils
6 6

  
7 7
from . import app_settings
8 8

  
......
17 17
    def id(self):
18 18
        return 'fc'
19 19

  
20
    @property
21
    def popup(self):
22
        return app_settings.popup
23

  
20 24
    def login(self, request, *args, **kwargs):
21 25
        if 'nofc' in request.GET:
22 26
            return
27
        fc_user_info = request.session.get('fc_user_info')
23 28
        context = kwargs.pop('context', {}).copy()
24
        context['about_url'] = app_settings.about_url
25
        if 'fc_user_info' in request.session:
26
            context['fc_user_info'] = request.session['fc_user_info']
27
        return render(request, 'authentic2_auth_fc/login.html', context)
29
        params = {}
30
        if self.popup:
31
            params['popup'] = ''
32
        context.update({
33
            'popup': self.popup,
34
            'about_url': app_settings.about_url,
35
            'fc_user_info': fc_user_info,
36
        })
37
        if fc_user_info:
38
            context.update({
39
                'registration_url': a2_utils.make_url('fc-registration',
40
                                                      keep_params=True,
41
                                                      params=params,
42
                                                      request=request),
43
                'fc_user_info': fc_user_info,
44
            })
45
            template = 'authentic2_auth_fc/login_registration.html'
46
        else:
47
            context['login_url'] = a2_utils.make_url('fc-login-or-link',
48
                                                     keep_params=True,
49
                                                     params=params,
50
                                                     request=request)
51
            template = 'authentic2_auth_fc/login.html'
52
        return render(request, template, context)
28 53

  
29 54
    def profile(self, request, *args, **kwargs):
30 55
        # We prevent unlinking if the user has no usable password and can't change it
......
32 57
        # and unlinking would make the account unreachable.
33 58
        unlink = request.user.has_usable_password() or a2_app_settings.A2_REGISTRATION_CAN_CHANGE_PASSWORD
34 59

  
60
        account_path = a2_utils.reverse('account_management')
61
        params = {
62
            'next': account_path,
63
        }
64
        if self.popup:
65
            params['popup'] = ''
66
        link_url = a2_utils.make_url('fc-login-or-link',
67
                                     params=params)
68

  
35 69
        context = kwargs.pop('context', {}).copy()
36 70
        context.update({
37
            'popup': True,
71
            'popup': self.popup,
38 72
            'unlink': unlink,
39
            'about_url': app_settings.about_url
73
            'about_url': app_settings.about_url,
74
            'link_url': link_url,
40 75
        })
41 76
        return render_to_string('authentic2_auth_fc/linking.html', context, request=request)
42 77

  
......
45 80
            return []
46 81

  
47 82
        context = kwargs.get('context', {}).copy()
83
        params = {
84
            'registration': '',
85
        }
86
        if self.popup:
87
            params['popup'] = ''
48 88
        context.update({
89
            'login_url': a2_utils.make_url('fc-login-or-link',
90
                                           keep_params=True, params=params,
91
                                           request=request),
92
            'popup': self.popup,
49 93
            'about_url': app_settings.about_url,
50
            'registration': True,
51 94
        })
52
        return render(request, 'authentic2_auth_fc/login.html', context)
95
        return render(request, 'authentic2_auth_fc/registration.html', context)
src/authentic2_auth_fc/static/authentic2_auth_fc/js/fc.js
1
/* Open FranceConnect in popup */
2

  
3

  
4
(function(undef) {
5
  function PopupCenter(url, title, w, h) {
6
        // Fixes dual-screen position                         Most browsers      Firefox
7
        var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX;
8
        var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY;
9

  
10
        var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
11
        var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
12

  
13
        var left = ((width / 2) - (w / 2)) + dualScreenLeft;
14
        var top = ((height / 2) - (h / 2)) + dualScreenTop;
15
        var newWindow = window.open(url, title, 'location=0,status=0,menubar=0,toolbar=0,scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
16

  
17
        // Puts focus on the newWindow
18
        if (window.focus) {
19
            newWindow.focus();
20
        }
21
  }
22
  var tags = document.getElementsByClassName('js-fc-popup');
23
  for (var i = 0; i < tags.length; i++) {
24
    var tag = tags[i];
25
    tag.onclick = function (ev) {
26
      PopupCenter(this.href, 'Authentification FranceConnect', 700, 500);
27
      return false;
28
    };
29
  }
30
})();
src/authentic2_auth_fc/templates/authentic2_auth_fc/connecting.html
1
{% load staticfiles %}
2
{% load i18n %}
3

  
4
{% if 'nofc' not in request.GET %}
5
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
6
<div id="fc-button-wrapper">
7
    <div id="fc-button">
8
        {% if not fc_user_info %}
9
        <a href="{% url 'fc-login-or-link' %}{% if request.GET.next or popup or registration %}?{% endif %}{% if next %}{{ next }}{% else %}{% if request.GET.next %}{{ request.GET.urlencode }}{% endif %}{% endif %}{% if popup %}&popup=1{% endif %}{% if registration %}&registration{% endif %}" title="{% trans 'Log in with FranceConnect' %}" class="button connexion{% if popup %} js-oauth-popup{% endif %}"><div><img src="{% if registration %}{% static "authentic2_auth_fc/img/FC-register-button.svg" %}{% else %}{% static "authentic2_auth_fc/img/FC-connect-button.svg" %}{% endif %}"></img></div></a>
10
        {% else %}
11
            <a class="button" href="{% url 'fc-registration' %}{% if request.GET.next or popup %}?{% endif %}{% if request.GET.next %}{{ request.GET.urlencode }}{% endif %}{% if popup %}&popup=1{% endif %}" title="{% trans 'Create your account with FranceConnect' %}" class="connexion{% if popup %} js-oauth-popup{% endif %}">
12
        <div>{% trans "Create your account with FranceConnect" %}<br/><br/><span class="certified">{{ fc_user_info.given_name }} {{ fc_user_info.family_name }}{% if fc_user_info.email %}<br/>{{ fc_user_info.email }}{% endif %}</span><br/><br/><img src="{% static 'authentic2_auth_fc/img/FC-register-button.svg' %}"></img></div></a>
13
        {% endif %}
14
    </div>
15
    {% block fc-explanation %}
16
    <p><a href="{{ about_url }}" target="_blank">{% trans "What is FranceConnect?" %}</a></p>
17
    <p>{% blocktrans %}
18
    FranceConnect is the solution proposed by the French state to streamline
19
    logging in online services. You can use to connect to your account.
20
    {% endblocktrans %}</p>
21
    {% endblock %}
22
</div>
23
{% endif %}
src/authentic2_auth_fc/templates/authentic2_auth_fc/explanation.html
1
{% load i18n %}
2
{% block fc-explanation %}
3
    <p>
4
	    <a href="{{ about_url }}" target="_blank">{% trans "What is FranceConnect?" %}</a>
5
    </p>
6
    <p>{% blocktrans %}
7
    FranceConnect is the solution proposed by the French state to streamline
8
    logging in online services. You can use to connect to your account.
9
    {% endblocktrans %}</p>
10
{% endblock %}
src/authentic2_auth_fc/templates/authentic2_auth_fc/linking.html
11 11
          {% trans "Linked FranceConnect accounts" %}
12 12
        </p>
13 13
        <ul class="fond">
14
        <li class="picto utilisateur"><p class="lien">{{ user.fc_accounts.all.0 }}{% if unlink %} <a href="{% url 'fc-unlink' %}">{% trans 'Delete link'%}</a>{% endif %}</p></li>
14
		<li class="picto utilisateur"><p class="lien">{{ user.fc_accounts.all.0 }}{% if unlink %} <a href="{% url 'fc-unlink' %}">{% trans 'Delete link'%}</a>{% endif %}</p></li>
15 15
        </ul>
16 16
      {% else %}
17 17
        <p>
18 18
          <div id="fc-button-wrapper">
19 19
              <div id="fc-button">
20
                  <a href="{% url 'fc-login-or-link' %}?next={% url 'account_management' %}" title="{% trans 'Link with a FranceConnect account' %}" class="button connexion"><div>{% trans "Link with a FranceConnect account" %}<img src="{% static 'authentic2_auth_fc/img/FCboutons-10.svg' %}"></img></div></a>
20
                <a href="{{ link_url }}" title="{% trans 'Link with a FranceConnect account' %}" class="button connexion{% if popup %} js-fc-popup{% endif %}"><div>{% trans "Link with a FranceConnect account" %}<img src="{% static 'authentic2_auth_fc/img/FCboutons-10.svg' %}"></img></div></a>
21 21
              </div>
22 22
          </div>
23 23
        </p>
......
26 26
  </div>
27 27
  <p><a href="{{ about_url }}" target="_blank">{% trans "What is FranceConnect?" %}</a></p>
28 28
</div>
29
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
src/authentic2_auth_fc/templates/authentic2_auth_fc/login.html
1
{% include "authentic2_auth_fc/connecting.html" %}
1
{% load staticfiles %}
2
{% load i18n %}
3

  
4
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
5
<div id="fc-button-wrapper">
6
<div id="fc-button">
7
    <a href="{{ login_url }}"
8
       title="{% trans 'Log in with FranceConnect' %}"
9
       class="button connexion{% if popup %} js-fc-popup{% endif %}">
10
        <div>
11
            <img src="{% static "authentic2_auth_fc/img/FC-connect-button.svg" %}"></img>
12
        </div>
13
    </a>
14
</div>
15
{% include "authentic2_auth_fc/explanation.html" %}
16
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
src/authentic2_auth_fc/templates/authentic2_auth_fc/login_registration.html
1
{% load staticfiles %}
2
{% load i18n %}
3

  
4
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
5
<div id="fc-button-wrapper">
6
<div id="fc-button">
7
    <a href="{{ registration_url }}"
8
       title="{% trans 'Create your account with FranceConnect' %}"
9
       class="button connexion{% if popup %} js-fc-popup{% endif %}">
10
        <div>
11
        {% trans "Create your account with FranceConnect" %}
12
        <br/>
13
        <br/>
14
        <span class="certified">
15
            {{ fc_user_info.given_name }} {{ fc_user_info.family_name }}
16
            {% if fc_user_info.email %}
17
            <br/>
18
            {{ fc_user_info.email }}
19
            {% endif %}
20
        </span>
21
        <br/>
22
        <br/>
23
        <img src="{% static 'authentic2_auth_fc/img/FC-register-button.svg' %}"></img>
24
        </div>
25
    </a>
26
</div>
27
{% include "authentic2_auth_fc/explanation.html" %}
28
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
src/authentic2_auth_fc/templates/authentic2_auth_fc/registration.html
1
{% load staticfiles %}
2
{% load i18n %}
3

  
4
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
5
<div id="fc-button-wrapper">
6
<div id="fc-button">
7
    <a href="{{ login_url }}"
8
       title="{% trans 'Register with FranceConnect' %}"
9
       class="button connexion{% if popup %} js-fc-popup{% endif %}">
10
        <div>
11
            <img src="{% static "authentic2_auth_fc/img/FC-register-button.svg" %}"></img>
12
        </div>
13
    </a>
14
</div>
15
{% include "authentic2_auth_fc/explanation.html" %}
16
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
src/authentic2_auth_fc/views.py
143 143
    def get_in_popup(self):
144 144
        return self.in_popup
145 145

  
146
    def redirect_to(self, request, *args, **kwargs):
146
    def redirect_to(self, request):
147 147
        if request.method == 'POST':
148 148
            redirect_to = request.POST.get(self.redirect_field_name,
149 149
                                           request.GET.get(self.redirect_field_name, ''))
......
162 162
                      {'redirect_to': next_url})
163 163

  
164 164
    def simple_redirect(self, request, next_url, *args, **kwargs):
165
        return HttpResponseRedirect(next_url)
165
        return a2_utils.redirect(request, next_url, *args, **kwargs)
166 166

  
167 167
    def redirect(self, request, *args, **kwargs):
168 168
        next_url = kwargs.pop('next_url', None)
......
175 175
            return self.simple_redirect(request, next_url, *args, **kwargs)
176 176

  
177 177
    def redirect_and_come_back(self, request, next_url, *args, **kwargs):
178
        old_next_url = self.redirect_to(request, *args, **kwargs)
179
        here = '{0}?{1}'.format(
180
            request.path, urlencode({REDIRECT_FIELD_NAME: old_next_url}))
181
        there = '{0}{2}{1}'.format(
182
            next_url, urlencode({REDIRECT_FIELD_NAME: here}),
183
            '&' if '?' in next_url else '?')
178
        old_next_url = self.redirect_to(request)
179
        here = a2_utils.make_url(request.path, params={REDIRECT_FIELD_NAME: old_next_url})
180
        here = a2_utils.make_url(here, **kwargs)
181
        there = a2_utils.make_url(next_url, params={REDIRECT_FIELD_NAME: here})
184 182
        return self.redirect(request, next_url=there, *args, **kwargs)
185 183

  
186 184
    def get_scopes(self):
......
421 419
            self.logger.info('logged in using fc sub %s', self.sub)
422 420
            return self.redirect(request)
423 421
        else:
422
            params = {}
423
            if self.service_slug:
424
                params[constants.SERVICE_FIELD_NAME] = self.service_slug
424 425
            if registration:
425
                return self.redirect_and_come_back(request, reverse('fc-registration'))
426
                return self.redirect_and_come_back(request,
427
                                                   a2_utils.make_url('fc-registration',
428
                                                                     params=params),
429
                                                   params=params)
426 430
            else:
427 431
                messages.info(request, _('If you already have an account, please log in, else '
428 432
                                         'create your account.'))
429
                if app_settings.show_button_quick_account_creation:
430
                    return self.redirect_and_come_back(request, settings.LOGIN_URL)
431
                else:
432
                    return self.redirect_and_come_back(request,
433
                                                       '{0}?nofc=1'.format(settings.LOGIN_URL))
434 433

  
434
                login_params = params.copy()
435
                if not app_settings.show_button_quick_account_creation:
436
                    login_params['nofc'] = 1
437

  
438
                login_url = a2_utils.make_url(settings.LOGIN_URL, params=login_params)
439
                return self.redirect_and_come_back(request, login_url, params=params)
435 440

  
436
class RegistrationView(LoggerMixin, View):
441

  
442
class RegistrationView(PopupViewMixin, LoggerMixin, View):
437 443
    def get(self, request, *args, **kwargs):
438 444
        data = utils.get_mapped_attributes_flat(request)
439 445
        data['no_password'] = True
......
447 453

  
448 454
        # Prevent errors when redirect_to does not contain fc-login-or-link view
449 455
        parsed_redirect_to = urlparse.urlparse(redirect_to)
450
        if parsed_redirect_to.path != reverse('fc-login-or-link'):
451
            redirect_to = '%s?%s=%s' % (
452
                reverse('fc-login-or-link'),
453
                REDIRECT_FIELD_NAME,
454
                urllib.quote(redirect_to))
456
        if parsed_redirect_to.path == reverse('fc-login-or-link'):
457
            redirect_to = urlparse.parse_qs(parsed_redirect_to.query) \
458
                .get(REDIRECT_FIELD_NAME, [a2_utils.make_url('auth_homepage')])[0]
459
        params = {
460
            REDIRECT_FIELD_NAME: redirect_to,
461
        }
462
        if self.get_in_popup():
463
            params['popup'] = ''
464
        redirect_to = a2_utils.make_url('fc-login-or-link', params=params)
455 465
        if not 'email' in data:
456 466
            data[REDIRECT_FIELD_NAME] = redirect_to
457 467
            messages.warning(request,
......
460 470
                                                             signing.dumps(data)))
461 471
        data['valid_email'] = False
462 472
        data['franceconnect'] = True
473
        data['authentication_method'] = 'france-connect'
474
        if constants.SERVICE_FIELD_NAME in request.GET:
475
            data[constants.SERVICE_FIELD_NAME] = request.GET[constants.SERVICE_FIELD_NAME]
463 476
        activation_url = a2_utils.build_activation_url(request,
464 477
                                                       next_url=redirect_to,
465 478
                                                       **data)
tests/conftest.py
84 84
def user_cartman(db, ou_southpark):
85 85
    return create_user(username='ecartman', first_name='eric', last_name='cartman',
86 86
                       email='ecartman@southpark.org', ou=ou_southpark, federation=CARTMAN_FC_INFO)
87

  
88

  
89
class AllHook(object):
90
    def __init__(self):
91
        self.calls = {}
92
        from authentic2 import hooks
93
        hooks.get_hooks.cache.clear()
94

  
95
    def __call__(self, hook_name, *args, **kwargs):
96
        calls = self.calls.setdefault(hook_name, [])
97
        calls.append({'args': args, 'kwargs': kwargs})
98

  
99
    def __getattr__(self, name):
100
        return self.calls.get(name, [])
101

  
102
    def clear(self):
103
        self.calls = {}
104

  
105

  
106
@pytest.fixture
107
def hooks(settings):
108
    if hasattr(settings, 'A2_HOOKS'):
109
        hooks = settings.A2_HOOKS
110
    else:
111
        hooks = settings.A2_HOOKS = {}
112
    hook = hooks['__all__'] = AllHook()
113
    yield hook
114
    hook.clear()
115
    del settings.A2_HOOKS['__all__']
tests/test_auth_fc.py
64 64

  
65 65
@pytest.mark.parametrize('exp', [timestamp_from_datetime(now() + datetime.timedelta(seconds=1000)),
66 66
                                 timestamp_from_datetime(now() - datetime.timedelta(seconds=1000))])
67
def test_login(app, fc_settings, caplog, exp):
68
    callback = reverse('fc-login-or-link')
69
    response = app.get(callback, status=302)
67
def test_login_simple(app, fc_settings, caplog, hooks, exp):
68
    response = app.get('/login/?service=portail&next=/idp/')
69
    response = response.click(href='callback')
70 70
    location = response['Location']
71 71
    state = check_authorization_url(location)
72 72

  
......
101 101
            'given_name': u'Ÿuñe',
102 102
        })
103 103

  
104
    callback = reverse('fc-login-or-link')
104 105
    with httmock.HTTMock(access_token_response, user_info_response):
105
        response = app.get(callback + '?code=zzz&state=%s' % state, status=302)
106
        response = app.get(callback + '?service=portail&next=/idp/&code=zzz&state=%s' % state, status=302)
106 107
    assert User.objects.count() == 0
107 108
    fc_settings.A2_FC_CREATE = True
108 109
    with httmock.HTTMock(access_token_response, user_info_response):
109
        response = app.get(callback + '?code=zzz&state=%s' % state, status=302)
110
        response = app.get(callback + '?service=portail&next=/idp/&code=zzz&state=%s' % state, status=302)
110 111
    if exp < timestamp_from_datetime(now()):
111 112
        assert User.objects.count() == 0
112 113
    else:
113 114
        assert User.objects.count() == 1
114 115
    if User.objects.count():
116
        assert response['Location'] == 'http://testserver/idp/'
117
        assert hooks.event[1]['kwargs']['name'] == 'login'
118
        assert hooks.event[1]['kwargs']['service'] == 'portail'
115 119
        # we must be connected
116 120
        assert app.session['_auth_user_id']
117 121
        assert models.FcAccount.objects.count() == 1
......
273 277
    models.FcAccount.objects.create(user=user, sub='xxx', token='aaa')
274 278
    response = app.get(url)
275 279
    assert 'new_password1' in response.form.fields
280

  
281

  
282
def test_registration1(app, fc_settings, caplog, hooks):
283
    exp = timestamp_from_datetime(now() + datetime.timedelta(seconds=1000))
284
    response = app.get('/login/?service=portail&next=/idp/')
285
    response = response.click(href="callback")
286
    # 1. Try a login
287
    # 2. Verify we come back to login page
288
    # 3. Check presence of registration link
289
    # 4. Follow it
290
    location = response['Location']
291
    state = check_authorization_url(location)
292

  
293
    @httmock.urlmatch(path=r'.*/token$')
294
    def access_token_response(url, request):
295
        parsed = {x: y[0] for x, y in urlparse.parse_qs(request.body).items()}
296
        assert set(parsed.keys()) == set(['code', 'client_id', 'client_secret', 'redirect_uri',
297
                                          'grant_type'])
298
        assert parsed['code'] == 'zzz'
299
        assert parsed['client_id'] == 'xxx'
300
        assert parsed['client_secret'] == 'yyy'
301
        assert parsed['grant_type'] == 'authorization_code'
302
        assert callback in parsed['redirect_uri']
303
        id_token = {
304
            'sub': '1234',
305
            'aud': 'xxx',
306
            'nonce': state,
307
            'exp': exp,
308
            'iss': 'https://fcp.integ01.dev-franceconnect.fr/',
309
            'email': 'john.doe@example.com',
310
        }
311
        return json.dumps({
312
            'access_token': 'uuu',
313
            'id_token': hmac_jwt(id_token, 'yyy')
314
        })
315

  
316
    @httmock.urlmatch(path=r'.*userinfo$')
317
    def user_info_response(url, request):
318
        assert request.headers['Authorization'] == 'Bearer uuu'
319
        return json.dumps({
320
            'sub': '1234',
321
            'family_name': u'Frédérique',
322
            'given_name': u'Ÿuñe',
323
            'email': 'john.doe@example.com',
324
        })
325

  
326
    callback = urlparse.parse_qs(urlparse.urlparse(location).query)['redirect_uri'][0]
327
    with httmock.HTTMock(access_token_response, user_info_response):
328
        response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
329
    assert User.objects.count() == 0
330
    assert response['Location'].startswith('http://testserver/login/')
331
    response = response.follow()
332
    response = response.click('Create your account with FranceConnect')
333
    location = response['Location']
334
    location.startswith('http://testserver/accounts/activate/')
335
    response = response.follow()
336
    assert hooks.calls['event'][0]['kwargs']['service'] == 'portail'
337
    # we must be connected
338
    assert app.session['_auth_user_id']
339
    assert response['Location'].startswith(callback)
340
    response = response.follow()
341
    location = response['Location']
342
    state = check_authorization_url(location)
343
    with httmock.HTTMock(access_token_response, user_info_response):
344
        response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
345
    assert models.FcAccount.objects.count() == 1
346
    response = app.get('/accounts/')
347
    response = response.click('Delete link')
348
    response.form.set('new_password1', 'ikKL1234')
349
    response.form.set('new_password2', 'ikKL1234')
350
    response = response.form.submit(name='unlink')
351
    assert 'The link with the FranceConnect account has been deleted' in response.content
352
    assert models.FcAccount.objects.count() == 0
353
    continue_url = response.pyquery('a#a2-continue').attr['href']
354
    state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
355
    assert app.session['fc_states'][state]['next'] == '/accounts/'
356
    response = app.get(reverse('fc-logout') + '?state=' + state)
357
    assert response['Location'] == 'http://testserver/accounts/'
358

  
359

  
360
def test_registration2(app, fc_settings, caplog, hooks):
361
    exp = timestamp_from_datetime(now() + datetime.timedelta(seconds=1000))
362
    response = app.get('/login/?service=portail&next=/idp/')
363
    response = response.click("Register")
364
    response = response.click(href='callback')
365
    # 1. Try a login
366
    # 2. Verify we come back to login page
367
    # 3. Check presence of registration link
368
    # 4. Follow it
369
    location = response['Location']
370
    state = check_authorization_url(location)
371

  
372
    @httmock.urlmatch(path=r'.*/token$')
373
    def access_token_response(url, request):
374
        parsed = {x: y[0] for x, y in urlparse.parse_qs(request.body).items()}
375
        assert set(parsed.keys()) == set(['code', 'client_id', 'client_secret', 'redirect_uri',
376
                                          'grant_type'])
377
        assert parsed['code'] == 'zzz'
378
        assert parsed['client_id'] == 'xxx'
379
        assert parsed['client_secret'] == 'yyy'
380
        assert parsed['grant_type'] == 'authorization_code'
381
        assert callback in parsed['redirect_uri']
382
        id_token = {
383
            'sub': '1234',
384
            'aud': 'xxx',
385
            'nonce': state,
386
            'exp': exp,
387
            'iss': 'https://fcp.integ01.dev-franceconnect.fr/',
388
            'email': 'john.doe@example.com',
389
        }
390
        return json.dumps({
391
            'access_token': 'uuu',
392
            'id_token': hmac_jwt(id_token, 'yyy')
393
        })
394

  
395
    @httmock.urlmatch(path=r'.*userinfo$')
396
    def user_info_response(url, request):
397
        assert request.headers['Authorization'] == 'Bearer uuu'
398
        return json.dumps({
399
            'sub': '1234',
400
            'family_name': u'Frédérique',
401
            'given_name': u'Ÿuñe',
402
            'email': 'john.doe@example.com',
403
        })
404

  
405
    callback = urlparse.parse_qs(urlparse.urlparse(location).query)['redirect_uri'][0]
406
    with httmock.HTTMock(access_token_response, user_info_response):
407
        response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
408
    assert User.objects.count() == 0
409
    assert response['Location'].startswith('http://testserver/accounts/fc/register/')
410
    response = response.follow()
411
    location = response['Location']
412
    location.startswith('http://testserver/accounts/activate/')
413
    response = response.follow()
414
    assert hooks.calls['event'][0]['kwargs']['service'] == 'portail'
415
    assert hooks.calls['event'][1]['kwargs']['service'] == 'portail'
416
    # we must be connected
417
    assert app.session['_auth_user_id']
418
    # remove the registration parameter
419
    callback = callback.replace('&registration=', '')
420
    callback = callback.replace('?registration=', '?')
421
    callback = callback.replace('?&', '?')
422
    assert response['Location'].startswith(callback)
423
    response = response.follow()
424
    location = response['Location']
425
    state = check_authorization_url(location)
426
    with httmock.HTTMock(access_token_response, user_info_response):
427
        response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
428
    assert models.FcAccount.objects.count() == 1
429
    response = app.get('/accounts/')
430
    response = response.click('Delete link')
431
    response.form.set('new_password1', 'ikKL1234')
432
    response.form.set('new_password2', 'ikKL1234')
433
    response = response.form.submit(name='unlink')
434
    assert 'The link with the FranceConnect account has been deleted' in response.content
435
    assert models.FcAccount.objects.count() == 0
436
    continue_url = response.pyquery('a#a2-continue').attr['href']
437
    state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
438
    assert app.session['fc_states'][state]['next'] == '/accounts/'
439
    response = app.get(reverse('fc-logout') + '?state=' + state)
440
    assert response['Location'] == 'http://testserver/accounts/'
276
-