From ce56e15d0bc98d480f456bc8a7b633327160608d Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 3 May 2019 10:11:47 +0200 Subject: [PATCH 1/2] apps: hide password management link when FC authenticated (#27083) It's done by implementing a2_hook_user_can_change_password on the AppConfig object. --- src/authentic2_auth_fc/__init__.py | 8 +++ tests/test_auth_fc.py | 103 +++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/src/authentic2_auth_fc/__init__.py b/src/authentic2_auth_fc/__init__.py index 615d939..9d88e4c 100644 --- a/src/authentic2_auth_fc/__init__.py +++ b/src/authentic2_auth_fc/__init__.py @@ -1,6 +1,8 @@ from . import utils from . import app_settings +from authentic2.utils import get_authentication_events + import django.apps @@ -71,5 +73,11 @@ class AppConfig(django.apps.AppConfig): return True return None + def a2_hook_user_can_change_password(self, user, request, **kwargs): + for authentication_event in get_authentication_events(request=request): + if authentication_event['how'] == 'france-connect': + return False + return True + default_app_config = '%s.%s' % (__name__, 'AppConfig') diff --git a/tests/test_auth_fc.py b/tests/test_auth_fc.py index f77cf8d..7f0520b 100644 --- a/tests/test_auth_fc.py +++ b/tests/test_auth_fc.py @@ -456,3 +456,106 @@ def test_registration2(app, fc_settings, caplog, hooks): assert app.session['fc_states'][state]['next'] == '/accounts/' response = app.get(reverse('fc-logout') + '?state=' + state) assert path(response['Location']) == '/accounts/' + + +def test_can_change_password(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 path(response['Location']) == '/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 path_and_query(response['Location']) == path_and_query(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 + user = User.objects.get() + assert user.verified_attributes.first_name == u'Ÿuñe' + assert user.verified_attributes.last_name == u'Frédérique' + response = app.get('/accounts/') + assert len(response.pyquery('[href*="password/change"]')) == 0 + + # Login with password + user = User.objects.get() + user.set_password('test') + user.save() + app.session.flush() + response = app.get('/login/') + response.form.set('username', User.objects.get().email) + response.form.set('password', 'test') + response = response.form.submit(name='login-password-submit').follow() + response = app.get('/accounts/') + assert len(response.pyquery('[href*="password/change"]')) > 0 + + # Relogin with FC + app.session.flush() + response = app.get('/login/?service=portail&next=/accounts/') + response = response.click(href='callback') + location = response['Location'] + state = check_authorization_url(location) + 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) + # we must be connected + assert app.session['_auth_user_id'] + assert path(response['Location']) == '/accounts/' + response = response.follow() + assert len(response.pyquery('[href*="password/change"]')) == 0 -- 2.20.1