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.
This commit is contained in:
parent
c21a16108a
commit
2e5ac496b8
|
@ -108,6 +108,10 @@ class AppSettings(object):
|
|||
def scopes(self):
|
||||
return self._setting('SCOPES', [])
|
||||
|
||||
@property
|
||||
def popup(self):
|
||||
return self._setting('POPUP', False)
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.utils.translation import gettext_noop
|
|||
from django.template.loader import render_to_string
|
||||
from django.shortcuts import render
|
||||
|
||||
from authentic2 import app_settings as a2_app_settings
|
||||
from authentic2 import app_settings as a2_app_settings, utils as a2_utils
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
@ -17,14 +17,39 @@ class FcFrontend(object):
|
|||
def id(self):
|
||||
return 'fc'
|
||||
|
||||
@property
|
||||
def popup(self):
|
||||
return app_settings.popup
|
||||
|
||||
def login(self, request, *args, **kwargs):
|
||||
if 'nofc' in request.GET:
|
||||
return
|
||||
fc_user_info = request.session.get('fc_user_info')
|
||||
context = kwargs.pop('context', {}).copy()
|
||||
context['about_url'] = app_settings.about_url
|
||||
if 'fc_user_info' in request.session:
|
||||
context['fc_user_info'] = request.session['fc_user_info']
|
||||
return render(request, 'authentic2_auth_fc/login.html', context)
|
||||
params = {}
|
||||
if self.popup:
|
||||
params['popup'] = ''
|
||||
context.update({
|
||||
'popup': self.popup,
|
||||
'about_url': app_settings.about_url,
|
||||
'fc_user_info': fc_user_info,
|
||||
})
|
||||
if fc_user_info:
|
||||
context.update({
|
||||
'registration_url': a2_utils.make_url('fc-registration',
|
||||
keep_params=True,
|
||||
params=params,
|
||||
request=request),
|
||||
'fc_user_info': fc_user_info,
|
||||
})
|
||||
template = 'authentic2_auth_fc/login_registration.html'
|
||||
else:
|
||||
context['login_url'] = a2_utils.make_url('fc-login-or-link',
|
||||
keep_params=True,
|
||||
params=params,
|
||||
request=request)
|
||||
template = 'authentic2_auth_fc/login.html'
|
||||
return render(request, template, context)
|
||||
|
||||
def profile(self, request, *args, **kwargs):
|
||||
# We prevent unlinking if the user has no usable password and can't change it
|
||||
|
@ -32,11 +57,21 @@ class FcFrontend(object):
|
|||
# and unlinking would make the account unreachable.
|
||||
unlink = request.user.has_usable_password() or a2_app_settings.A2_REGISTRATION_CAN_CHANGE_PASSWORD
|
||||
|
||||
account_path = a2_utils.reverse('account_management')
|
||||
params = {
|
||||
'next': account_path,
|
||||
}
|
||||
if self.popup:
|
||||
params['popup'] = ''
|
||||
link_url = a2_utils.make_url('fc-login-or-link',
|
||||
params=params)
|
||||
|
||||
context = kwargs.pop('context', {}).copy()
|
||||
context.update({
|
||||
'popup': True,
|
||||
'popup': self.popup,
|
||||
'unlink': unlink,
|
||||
'about_url': app_settings.about_url
|
||||
'about_url': app_settings.about_url,
|
||||
'link_url': link_url,
|
||||
})
|
||||
return render_to_string('authentic2_auth_fc/linking.html', context, request=request)
|
||||
|
||||
|
@ -45,8 +80,16 @@ class FcFrontend(object):
|
|||
return []
|
||||
|
||||
context = kwargs.get('context', {}).copy()
|
||||
params = {
|
||||
'registration': '',
|
||||
}
|
||||
if self.popup:
|
||||
params['popup'] = ''
|
||||
context.update({
|
||||
'login_url': a2_utils.make_url('fc-login-or-link',
|
||||
keep_params=True, params=params,
|
||||
request=request),
|
||||
'popup': self.popup,
|
||||
'about_url': app_settings.about_url,
|
||||
'registration': True,
|
||||
})
|
||||
return render(request, 'authentic2_auth_fc/login.html', context)
|
||||
return render(request, 'authentic2_auth_fc/registration.html', context)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/* Open FranceConnect in popup */
|
||||
|
||||
|
||||
(function(undef) {
|
||||
function PopupCenter(url, title, w, h) {
|
||||
// Fixes dual-screen position Most browsers Firefox
|
||||
var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX;
|
||||
var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY;
|
||||
|
||||
var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
||||
var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
|
||||
|
||||
var left = ((width / 2) - (w / 2)) + dualScreenLeft;
|
||||
var top = ((height / 2) - (h / 2)) + dualScreenTop;
|
||||
var newWindow = window.open(url, title, 'location=0,status=0,menubar=0,toolbar=0,scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
|
||||
|
||||
// Puts focus on the newWindow
|
||||
if (window.focus) {
|
||||
newWindow.focus();
|
||||
}
|
||||
}
|
||||
var tags = document.getElementsByClassName('js-fc-popup');
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
var tag = tags[i];
|
||||
tag.onclick = function (ev) {
|
||||
PopupCenter(this.href, 'Authentification FranceConnect', 700, 500);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -1,23 +0,0 @@
|
|||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if 'nofc' not in request.GET %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
|
||||
<div id="fc-button-wrapper">
|
||||
<div id="fc-button">
|
||||
{% if not fc_user_info %}
|
||||
<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 %}®istration{% 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>
|
||||
{% else %}
|
||||
<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 %}">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% block fc-explanation %}
|
||||
<p><a href="{{ about_url }}" target="_blank">{% trans "What is FranceConnect?" %}</a></p>
|
||||
<p>{% blocktrans %}
|
||||
FranceConnect is the solution proposed by the French state to streamline
|
||||
logging in online services. You can use to connect to your account.
|
||||
{% endblocktrans %}</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
|
@ -0,0 +1,10 @@
|
|||
{% load i18n %}
|
||||
{% block fc-explanation %}
|
||||
<p>
|
||||
<a href="{{ about_url }}" target="_blank">{% trans "What is FranceConnect?" %}</a>
|
||||
</p>
|
||||
<p>{% blocktrans %}
|
||||
FranceConnect is the solution proposed by the French state to streamline
|
||||
logging in online services. You can use to connect to your account.
|
||||
{% endblocktrans %}</p>
|
||||
{% endblock %}
|
|
@ -11,13 +11,13 @@
|
|||
{% trans "Linked FranceConnect accounts" %}
|
||||
</p>
|
||||
<ul class="fond">
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>
|
||||
<div id="fc-button-wrapper">
|
||||
<div id="fc-button">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
@ -26,3 +26,4 @@
|
|||
</div>
|
||||
<p><a href="{{ about_url }}" target="_blank">{% trans "What is FranceConnect?" %}</a></p>
|
||||
</div>
|
||||
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
|
||||
|
|
|
@ -1 +1,16 @@
|
|||
{% include "authentic2_auth_fc/connecting.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
|
||||
<div id="fc-button-wrapper">
|
||||
<div id="fc-button">
|
||||
<a href="{{ login_url }}"
|
||||
title="{% trans 'Log in with FranceConnect' %}"
|
||||
class="button connexion{% if popup %} js-fc-popup{% endif %}">
|
||||
<div>
|
||||
<img src="{% static "authentic2_auth_fc/img/FC-connect-button.svg" %}"></img>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% include "authentic2_auth_fc/explanation.html" %}
|
||||
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
|
||||
<div id="fc-button-wrapper">
|
||||
<div id="fc-button">
|
||||
<a href="{{ registration_url }}"
|
||||
title="{% trans 'Create your account with FranceConnect' %}"
|
||||
class="button connexion{% if popup %} js-fc-popup{% endif %}">
|
||||
<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>
|
||||
</div>
|
||||
{% include "authentic2_auth_fc/explanation.html" %}
|
||||
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
|
|
@ -0,0 +1,16 @@
|
|||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'authentic2_auth_fc/css/fc.css' %}">
|
||||
<div id="fc-button-wrapper">
|
||||
<div id="fc-button">
|
||||
<a href="{{ login_url }}"
|
||||
title="{% trans 'Register with FranceConnect' %}"
|
||||
class="button connexion{% if popup %} js-fc-popup{% endif %}">
|
||||
<div>
|
||||
<img src="{% static "authentic2_auth_fc/img/FC-register-button.svg" %}"></img>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% include "authentic2_auth_fc/explanation.html" %}
|
||||
{% if popup %}<script src="{% static 'authentic2_auth_fc/js/fc.js' %}" type="text/javascript"></script>{% endif %}
|
|
@ -143,7 +143,7 @@ class FcOAuthSessionViewMixin(LoggerMixin):
|
|||
def get_in_popup(self):
|
||||
return self.in_popup
|
||||
|
||||
def redirect_to(self, request, *args, **kwargs):
|
||||
def redirect_to(self, request):
|
||||
if request.method == 'POST':
|
||||
redirect_to = request.POST.get(self.redirect_field_name,
|
||||
request.GET.get(self.redirect_field_name, ''))
|
||||
|
@ -162,7 +162,7 @@ class FcOAuthSessionViewMixin(LoggerMixin):
|
|||
{'redirect_to': next_url})
|
||||
|
||||
def simple_redirect(self, request, next_url, *args, **kwargs):
|
||||
return HttpResponseRedirect(next_url)
|
||||
return a2_utils.redirect(request, next_url, *args, **kwargs)
|
||||
|
||||
def redirect(self, request, *args, **kwargs):
|
||||
next_url = kwargs.pop('next_url', None)
|
||||
|
@ -175,12 +175,10 @@ class FcOAuthSessionViewMixin(LoggerMixin):
|
|||
return self.simple_redirect(request, next_url, *args, **kwargs)
|
||||
|
||||
def redirect_and_come_back(self, request, next_url, *args, **kwargs):
|
||||
old_next_url = self.redirect_to(request, *args, **kwargs)
|
||||
here = '{0}?{1}'.format(
|
||||
request.path, urlencode({REDIRECT_FIELD_NAME: old_next_url}))
|
||||
there = '{0}{2}{1}'.format(
|
||||
next_url, urlencode({REDIRECT_FIELD_NAME: here}),
|
||||
'&' if '?' in next_url else '?')
|
||||
old_next_url = self.redirect_to(request)
|
||||
here = a2_utils.make_url(request.path, params={REDIRECT_FIELD_NAME: old_next_url})
|
||||
here = a2_utils.make_url(here, **kwargs)
|
||||
there = a2_utils.make_url(next_url, params={REDIRECT_FIELD_NAME: here})
|
||||
return self.redirect(request, next_url=there, *args, **kwargs)
|
||||
|
||||
def get_scopes(self):
|
||||
|
@ -421,19 +419,27 @@ class LoginOrLinkView(PopupViewMixin, FcOAuthSessionViewMixin, View):
|
|||
self.logger.info('logged in using fc sub %s', self.sub)
|
||||
return self.redirect(request)
|
||||
else:
|
||||
params = {}
|
||||
if self.service_slug:
|
||||
params[constants.SERVICE_FIELD_NAME] = self.service_slug
|
||||
if registration:
|
||||
return self.redirect_and_come_back(request, reverse('fc-registration'))
|
||||
return self.redirect_and_come_back(request,
|
||||
a2_utils.make_url('fc-registration',
|
||||
params=params),
|
||||
params=params)
|
||||
else:
|
||||
messages.info(request, _('If you already have an account, please log in, else '
|
||||
'create your account.'))
|
||||
if app_settings.show_button_quick_account_creation:
|
||||
return self.redirect_and_come_back(request, settings.LOGIN_URL)
|
||||
else:
|
||||
return self.redirect_and_come_back(request,
|
||||
'{0}?nofc=1'.format(settings.LOGIN_URL))
|
||||
|
||||
login_params = params.copy()
|
||||
if not app_settings.show_button_quick_account_creation:
|
||||
login_params['nofc'] = 1
|
||||
|
||||
login_url = a2_utils.make_url(settings.LOGIN_URL, params=login_params)
|
||||
return self.redirect_and_come_back(request, login_url, params=params)
|
||||
|
||||
|
||||
class RegistrationView(LoggerMixin, View):
|
||||
class RegistrationView(PopupViewMixin, LoggerMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
data = utils.get_mapped_attributes_flat(request)
|
||||
data['no_password'] = True
|
||||
|
@ -447,11 +453,15 @@ class RegistrationView(LoggerMixin, View):
|
|||
|
||||
# Prevent errors when redirect_to does not contain fc-login-or-link view
|
||||
parsed_redirect_to = urlparse.urlparse(redirect_to)
|
||||
if parsed_redirect_to.path != reverse('fc-login-or-link'):
|
||||
redirect_to = '%s?%s=%s' % (
|
||||
reverse('fc-login-or-link'),
|
||||
REDIRECT_FIELD_NAME,
|
||||
urllib.quote(redirect_to))
|
||||
if parsed_redirect_to.path == reverse('fc-login-or-link'):
|
||||
redirect_to = urlparse.parse_qs(parsed_redirect_to.query) \
|
||||
.get(REDIRECT_FIELD_NAME, [a2_utils.make_url('auth_homepage')])[0]
|
||||
params = {
|
||||
REDIRECT_FIELD_NAME: redirect_to,
|
||||
}
|
||||
if self.get_in_popup():
|
||||
params['popup'] = ''
|
||||
redirect_to = a2_utils.make_url('fc-login-or-link', params=params)
|
||||
if not 'email' in data:
|
||||
data[REDIRECT_FIELD_NAME] = redirect_to
|
||||
messages.warning(request,
|
||||
|
@ -460,6 +470,9 @@ class RegistrationView(LoggerMixin, View):
|
|||
signing.dumps(data)))
|
||||
data['valid_email'] = False
|
||||
data['franceconnect'] = True
|
||||
data['authentication_method'] = 'france-connect'
|
||||
if constants.SERVICE_FIELD_NAME in request.GET:
|
||||
data[constants.SERVICE_FIELD_NAME] = request.GET[constants.SERVICE_FIELD_NAME]
|
||||
activation_url = a2_utils.build_activation_url(request,
|
||||
next_url=redirect_to,
|
||||
**data)
|
||||
|
|
|
@ -84,3 +84,32 @@ def admin(db):
|
|||
def user_cartman(db, ou_southpark):
|
||||
return create_user(username='ecartman', first_name='eric', last_name='cartman',
|
||||
email='ecartman@southpark.org', ou=ou_southpark, federation=CARTMAN_FC_INFO)
|
||||
|
||||
|
||||
class AllHook(object):
|
||||
def __init__(self):
|
||||
self.calls = {}
|
||||
from authentic2 import hooks
|
||||
hooks.get_hooks.cache.clear()
|
||||
|
||||
def __call__(self, hook_name, *args, **kwargs):
|
||||
calls = self.calls.setdefault(hook_name, [])
|
||||
calls.append({'args': args, 'kwargs': kwargs})
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.calls.get(name, [])
|
||||
|
||||
def clear(self):
|
||||
self.calls = {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hooks(settings):
|
||||
if hasattr(settings, 'A2_HOOKS'):
|
||||
hooks = settings.A2_HOOKS
|
||||
else:
|
||||
hooks = settings.A2_HOOKS = {}
|
||||
hook = hooks['__all__'] = AllHook()
|
||||
yield hook
|
||||
hook.clear()
|
||||
del settings.A2_HOOKS['__all__']
|
||||
|
|
|
@ -64,9 +64,9 @@ def check_authorization_url(url):
|
|||
|
||||
@pytest.mark.parametrize('exp', [timestamp_from_datetime(now() + datetime.timedelta(seconds=1000)),
|
||||
timestamp_from_datetime(now() - datetime.timedelta(seconds=1000))])
|
||||
def test_login(app, fc_settings, caplog, exp):
|
||||
callback = reverse('fc-login-or-link')
|
||||
response = app.get(callback, status=302)
|
||||
def test_login_simple(app, fc_settings, caplog, hooks, exp):
|
||||
response = app.get('/login/?service=portail&next=/idp/')
|
||||
response = response.click(href='callback')
|
||||
location = response['Location']
|
||||
state = check_authorization_url(location)
|
||||
|
||||
|
@ -101,17 +101,21 @@ def test_login(app, fc_settings, caplog, exp):
|
|||
'given_name': u'Ÿuñe',
|
||||
})
|
||||
|
||||
callback = reverse('fc-login-or-link')
|
||||
with httmock.HTTMock(access_token_response, user_info_response):
|
||||
response = app.get(callback + '?code=zzz&state=%s' % state, status=302)
|
||||
response = app.get(callback + '?service=portail&next=/idp/&code=zzz&state=%s' % state, status=302)
|
||||
assert User.objects.count() == 0
|
||||
fc_settings.A2_FC_CREATE = True
|
||||
with httmock.HTTMock(access_token_response, user_info_response):
|
||||
response = app.get(callback + '?code=zzz&state=%s' % state, status=302)
|
||||
response = app.get(callback + '?service=portail&next=/idp/&code=zzz&state=%s' % state, status=302)
|
||||
if exp < timestamp_from_datetime(now()):
|
||||
assert User.objects.count() == 0
|
||||
else:
|
||||
assert User.objects.count() == 1
|
||||
if User.objects.count():
|
||||
assert response['Location'] == 'http://testserver/idp/'
|
||||
assert hooks.event[1]['kwargs']['name'] == 'login'
|
||||
assert hooks.event[1]['kwargs']['service'] == 'portail'
|
||||
# we must be connected
|
||||
assert app.session['_auth_user_id']
|
||||
assert models.FcAccount.objects.count() == 1
|
||||
|
@ -273,3 +277,164 @@ def test_password_reset(app, mailoutbox):
|
|||
models.FcAccount.objects.create(user=user, sub='xxx', token='aaa')
|
||||
response = app.get(url)
|
||||
assert 'new_password1' in response.form.fields
|
||||
|
||||
|
||||
def test_registration1(app, fc_settings, caplog, hooks):
|
||||
exp = timestamp_from_datetime(now() + datetime.timedelta(seconds=1000))
|
||||
response = app.get('/login/?service=portail&next=/idp/')
|
||||
response = response.click(href="callback")
|
||||
# 1. Try a login
|
||||
# 2. Verify we come back to login page
|
||||
# 3. Check presence of registration link
|
||||
# 4. Follow it
|
||||
location = response['Location']
|
||||
state = check_authorization_url(location)
|
||||
|
||||
@httmock.urlmatch(path=r'.*/token$')
|
||||
def access_token_response(url, request):
|
||||
parsed = {x: y[0] for x, y in urlparse.parse_qs(request.body).items()}
|
||||
assert set(parsed.keys()) == set(['code', 'client_id', 'client_secret', 'redirect_uri',
|
||||
'grant_type'])
|
||||
assert parsed['code'] == 'zzz'
|
||||
assert parsed['client_id'] == 'xxx'
|
||||
assert parsed['client_secret'] == 'yyy'
|
||||
assert parsed['grant_type'] == 'authorization_code'
|
||||
assert callback in parsed['redirect_uri']
|
||||
id_token = {
|
||||
'sub': '1234',
|
||||
'aud': 'xxx',
|
||||
'nonce': state,
|
||||
'exp': exp,
|
||||
'iss': 'https://fcp.integ01.dev-franceconnect.fr/',
|
||||
'email': 'john.doe@example.com',
|
||||
}
|
||||
return json.dumps({
|
||||
'access_token': 'uuu',
|
||||
'id_token': hmac_jwt(id_token, 'yyy')
|
||||
})
|
||||
|
||||
@httmock.urlmatch(path=r'.*userinfo$')
|
||||
def user_info_response(url, request):
|
||||
assert request.headers['Authorization'] == 'Bearer uuu'
|
||||
return json.dumps({
|
||||
'sub': '1234',
|
||||
'family_name': u'Frédérique',
|
||||
'given_name': u'Ÿuñe',
|
||||
'email': 'john.doe@example.com',
|
||||
})
|
||||
|
||||
callback = urlparse.parse_qs(urlparse.urlparse(location).query)['redirect_uri'][0]
|
||||
with httmock.HTTMock(access_token_response, user_info_response):
|
||||
response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
|
||||
assert User.objects.count() == 0
|
||||
assert response['Location'].startswith('http://testserver/login/')
|
||||
response = response.follow()
|
||||
response = response.click('Create your account with FranceConnect')
|
||||
location = response['Location']
|
||||
location.startswith('http://testserver/accounts/activate/')
|
||||
response = response.follow()
|
||||
assert hooks.calls['event'][0]['kwargs']['service'] == 'portail'
|
||||
# we must be connected
|
||||
assert app.session['_auth_user_id']
|
||||
assert response['Location'].startswith(callback)
|
||||
response = response.follow()
|
||||
location = response['Location']
|
||||
state = check_authorization_url(location)
|
||||
with httmock.HTTMock(access_token_response, user_info_response):
|
||||
response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
|
||||
assert models.FcAccount.objects.count() == 1
|
||||
response = app.get('/accounts/')
|
||||
response = response.click('Delete link')
|
||||
response.form.set('new_password1', 'ikKL1234')
|
||||
response.form.set('new_password2', 'ikKL1234')
|
||||
response = response.form.submit(name='unlink')
|
||||
assert 'The link with the FranceConnect account has been deleted' in response.content
|
||||
assert models.FcAccount.objects.count() == 0
|
||||
continue_url = response.pyquery('a#a2-continue').attr['href']
|
||||
state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
|
||||
assert app.session['fc_states'][state]['next'] == '/accounts/'
|
||||
response = app.get(reverse('fc-logout') + '?state=' + state)
|
||||
assert response['Location'] == 'http://testserver/accounts/'
|
||||
|
||||
|
||||
def test_registration2(app, fc_settings, caplog, hooks):
|
||||
exp = timestamp_from_datetime(now() + datetime.timedelta(seconds=1000))
|
||||
response = app.get('/login/?service=portail&next=/idp/')
|
||||
response = response.click("Register")
|
||||
response = response.click(href='callback')
|
||||
# 1. Try a login
|
||||
# 2. Verify we come back to login page
|
||||
# 3. Check presence of registration link
|
||||
# 4. Follow it
|
||||
location = response['Location']
|
||||
state = check_authorization_url(location)
|
||||
|
||||
@httmock.urlmatch(path=r'.*/token$')
|
||||
def access_token_response(url, request):
|
||||
parsed = {x: y[0] for x, y in urlparse.parse_qs(request.body).items()}
|
||||
assert set(parsed.keys()) == set(['code', 'client_id', 'client_secret', 'redirect_uri',
|
||||
'grant_type'])
|
||||
assert parsed['code'] == 'zzz'
|
||||
assert parsed['client_id'] == 'xxx'
|
||||
assert parsed['client_secret'] == 'yyy'
|
||||
assert parsed['grant_type'] == 'authorization_code'
|
||||
assert callback in parsed['redirect_uri']
|
||||
id_token = {
|
||||
'sub': '1234',
|
||||
'aud': 'xxx',
|
||||
'nonce': state,
|
||||
'exp': exp,
|
||||
'iss': 'https://fcp.integ01.dev-franceconnect.fr/',
|
||||
'email': 'john.doe@example.com',
|
||||
}
|
||||
return json.dumps({
|
||||
'access_token': 'uuu',
|
||||
'id_token': hmac_jwt(id_token, 'yyy')
|
||||
})
|
||||
|
||||
@httmock.urlmatch(path=r'.*userinfo$')
|
||||
def user_info_response(url, request):
|
||||
assert request.headers['Authorization'] == 'Bearer uuu'
|
||||
return json.dumps({
|
||||
'sub': '1234',
|
||||
'family_name': u'Frédérique',
|
||||
'given_name': u'Ÿuñe',
|
||||
'email': 'john.doe@example.com',
|
||||
})
|
||||
|
||||
callback = urlparse.parse_qs(urlparse.urlparse(location).query)['redirect_uri'][0]
|
||||
with httmock.HTTMock(access_token_response, user_info_response):
|
||||
response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
|
||||
assert User.objects.count() == 0
|
||||
assert response['Location'].startswith('http://testserver/accounts/fc/register/')
|
||||
response = response.follow()
|
||||
location = response['Location']
|
||||
location.startswith('http://testserver/accounts/activate/')
|
||||
response = response.follow()
|
||||
assert hooks.calls['event'][0]['kwargs']['service'] == 'portail'
|
||||
assert hooks.calls['event'][1]['kwargs']['service'] == 'portail'
|
||||
# we must be connected
|
||||
assert app.session['_auth_user_id']
|
||||
# remove the registration parameter
|
||||
callback = callback.replace('®istration=', '')
|
||||
callback = callback.replace('?registration=', '?')
|
||||
callback = callback.replace('?&', '?')
|
||||
assert response['Location'].startswith(callback)
|
||||
response = response.follow()
|
||||
location = response['Location']
|
||||
state = check_authorization_url(location)
|
||||
with httmock.HTTMock(access_token_response, user_info_response):
|
||||
response = app.get(callback + '&code=zzz&state=%s' % state, status=302)
|
||||
assert models.FcAccount.objects.count() == 1
|
||||
response = app.get('/accounts/')
|
||||
response = response.click('Delete link')
|
||||
response.form.set('new_password1', 'ikKL1234')
|
||||
response.form.set('new_password2', 'ikKL1234')
|
||||
response = response.form.submit(name='unlink')
|
||||
assert 'The link with the FranceConnect account has been deleted' in response.content
|
||||
assert models.FcAccount.objects.count() == 0
|
||||
continue_url = response.pyquery('a#a2-continue').attr['href']
|
||||
state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
|
||||
assert app.session['fc_states'][state]['next'] == '/accounts/'
|
||||
response = app.get(reverse('fc-logout') + '?state=' + state)
|
||||
assert response['Location'] == 'http://testserver/accounts/'
|
||||
|
|
Reference in New Issue