355 lines
14 KiB
Python
355 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# authentic2 - authentic2 authentication for FranceConnect
|
|
# Copyright (C) 2020 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import datetime
|
|
import mock
|
|
|
|
import requests
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.urls import reverse
|
|
from django.utils.six.moves.urllib import parse as urlparse
|
|
from django.utils.timezone import now
|
|
|
|
from authentic2_auth_fc import models
|
|
from authentic2_auth_fc.utils import requests_retry_session
|
|
|
|
from ..utils import login, get_link_from_mail
|
|
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def path(url):
|
|
return urlparse.urlparse(url).path
|
|
|
|
|
|
def test_login_redirect(app, franceconnect):
|
|
url = reverse('fc-login-or-link')
|
|
response = app.get(url, status=302)
|
|
assert response['Location'].startswith('https://fcp.integ01')
|
|
|
|
|
|
def test_login_with_condition(settings, app, franceconnect):
|
|
# open the page first time so session cookie can be set
|
|
response = app.get('/login/')
|
|
assert 'fc-button' in response
|
|
|
|
# make sure FC block is first
|
|
assert response.text.index('div id="fc-button"') < response.text.index('name="login-password-submit"')
|
|
|
|
settings.AUTH_FRONTENDS_KWARGS = {'fc': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
|
|
response = app.get('/login/')
|
|
assert 'fc-button' not in response
|
|
|
|
|
|
def test_login_autorun(settings, app, franceconnect):
|
|
# hide password block
|
|
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
|
|
response = app.get('/login/')
|
|
assert response['Location'] == reverse('fc-login-or-link')
|
|
|
|
|
|
def test_no_create(app, franceconnect):
|
|
response = app.get('/login/?service=portail&next=/idp/')
|
|
response = response.click(href='callback')
|
|
franceconnect.handle_authorization(app, response.location, status=302)
|
|
assert User.objects.count() == 0
|
|
|
|
|
|
def test_create(settings, app, franceconnect, hooks):
|
|
# test direct creation
|
|
settings.A2_FC_CREATE = True
|
|
|
|
response = app.get('/login/?service=portail&next=/idp/')
|
|
response = response.click(href='callback')
|
|
|
|
assert User.objects.count() == 0
|
|
response = franceconnect.handle_authorization(app, response.location, status=302)
|
|
assert User.objects.count() == 1
|
|
|
|
user = User.objects.get()
|
|
assert user.verified_attributes.first_name == 'Ÿuñe'
|
|
assert user.verified_attributes.last_name == 'Frédérique'
|
|
assert path(response['Location']) == '/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 app.session.get_expire_at_browser_close()
|
|
assert models.FcAccount.objects.count() == 1
|
|
|
|
# test unlink cancel case
|
|
response = app.get('/accounts/')
|
|
response = response.click('Delete link')
|
|
assert len(response.pyquery('[name=cancel][formnovalidate]')) == 1
|
|
response = response.form.submit(name='cancel')
|
|
response = response.follow()
|
|
|
|
# test unlink submit case
|
|
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.text
|
|
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 path(response['Location']) == '/accounts/'
|
|
|
|
|
|
def test_create_expired(settings, app, franceconnect, hooks):
|
|
# test direct creation failure on an expired id_token
|
|
settings.A2_FC_CREATE = True
|
|
franceconnect.exp = now() - datetime.timedelta(seconds=30)
|
|
|
|
response = app.get('/login/?service=portail&next=/idp/')
|
|
response = response.click(href='callback')
|
|
|
|
assert User.objects.count() == 0
|
|
response = franceconnect.handle_authorization(app, response.location, status=302)
|
|
assert User.objects.count() == 0
|
|
|
|
|
|
def test_login_email_is_unique(settings, app, franceconnect, caplog):
|
|
settings.A2_EMAIL_IS_UNIQUE = True
|
|
user = User(
|
|
email='john.doe@example.com',
|
|
first_name='John',
|
|
last_name='Doe')
|
|
user.set_password('toto')
|
|
user.save()
|
|
franceconnect.user_info['email'] = user.email
|
|
|
|
assert User.objects.count() == 1
|
|
franceconnect.login_with_fc_fixed_params(app)
|
|
assert User.objects.count() == 1
|
|
assert app.session['_auth_user_id'] == str(user.pk)
|
|
|
|
|
|
def test_unlink_after_login_with_password(app, franceconnect, simple_user):
|
|
models.FcAccount.objects.create(user=simple_user, user_info='{}')
|
|
|
|
response = login(app, simple_user, path='/accounts/')
|
|
response = response.click('Delete link')
|
|
assert 'new_password1' not in response.form.fields
|
|
response = response.form.submit(name='unlink').follow()
|
|
assert 'The link with the FranceConnect account has been deleted' in response.text
|
|
# no logout from FC since we are not logged to it
|
|
assert response.request.path == '/accounts/'
|
|
|
|
|
|
def test_unlink_after_login_with_fc(app, franceconnect, simple_user):
|
|
models.FcAccount.objects.create(user=simple_user, sub=franceconnect.sub, user_info='{}')
|
|
|
|
response = franceconnect.login_with_fc(app, path='/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.text
|
|
assert models.FcAccount.objects.count() == 0
|
|
continue_url = response.pyquery('a#a2-continue').attr['href']
|
|
response = franceconnect.handle_logout(app, continue_url)
|
|
assert path(response.location) == '/accounts/'
|
|
|
|
|
|
def test_login_email_is_unique_and_already_linked(settings, app, franceconnect, caplog):
|
|
settings.A2_EMAIL_IS_UNIQUE = True
|
|
|
|
# setup an already linked user account
|
|
user = User.objects.create(email='john.doe@example.com', first_name='John', last_name='Doe')
|
|
models.FcAccount.objects.create(user=user, sub='4567', token='xxx', user_info='{}')
|
|
response = app.get('/login/?service=portail&next=/idp/')
|
|
response = response.click(href='callback')
|
|
response = franceconnect.handle_authorization(app, response.location, status=302)
|
|
assert models.FcAccount.objects.count() == 1
|
|
assert 'is already used' in app.cookies['messages']
|
|
assert '_auth_user_id' not in app.session
|
|
|
|
|
|
def test_requests_proxies_support(settings, app):
|
|
session = requests_retry_session()
|
|
assert session.proxies == {}
|
|
other_session = requests.Session()
|
|
other_session.proxies = {'http': 'http://example.net'}
|
|
session = requests_retry_session(session=other_session)
|
|
assert session is other_session
|
|
assert session.proxies == {'http': 'http://example.net'}
|
|
|
|
settings.REQUESTS_PROXIES = {'https': 'http://pubproxy.com/api/proxy'}
|
|
session = requests_retry_session()
|
|
assert session.proxies == {'https': 'http://pubproxy.com/api/proxy'}
|
|
|
|
with mock.patch('authentic2_auth_fc.utils.requests.Session.send') as mocked_send:
|
|
mocked_send.return_value = mock.Mock(status_code=200, content='whatever')
|
|
session.get('https://example.net/')
|
|
assert mocked_send.call_args[1]['proxies'] == {'https': 'http://pubproxy.com/api/proxy'}
|
|
|
|
|
|
def test_no_password_with_fc_account_can_reset_password(app, db, mailoutbox):
|
|
user = User.objects.create(email='john.doe@example.com')
|
|
# No FC account, forbidden to set a password
|
|
response = app.get('/login/')
|
|
response = response.click('Reset it!').maybe_follow()
|
|
response.form['email'] = user.email
|
|
assert len(mailoutbox) == 0
|
|
response = response.form.submit()
|
|
assert len(mailoutbox) == 1
|
|
url = get_link_from_mail(mailoutbox[0])
|
|
response = app.get(url).follow().follow()
|
|
assert '_auth_user_id' not in app.session
|
|
assert 'not possible to reset' in response
|
|
|
|
# With FC account, can set a password
|
|
models.FcAccount.objects.create(user=user, sub='xxx', token='aaa')
|
|
response = app.get('/login/')
|
|
response = response.click('Reset it!').maybe_follow()
|
|
response.form['email'] = user.email
|
|
assert len(mailoutbox) == 1
|
|
response = response.form.submit()
|
|
assert len(mailoutbox) == 2
|
|
url = get_link_from_mail(mailoutbox[1])
|
|
response = app.get(url, status=200)
|
|
response.form.set('new_password1', 'ikKL1234')
|
|
response.form.set('new_password2', 'ikKL1234')
|
|
response = response.form.submit().follow()
|
|
assert '_auth_user_id' in app.session
|
|
|
|
|
|
def test_registration1(settings, app, franceconnect, caplog, hooks):
|
|
response = franceconnect.login_with_fc_fixed_params(app)
|
|
assert User.objects.count() == 0
|
|
assert path(response.location) == '/login/'
|
|
response = response.follow()
|
|
response = response.click(href='/accounts/fc/register')
|
|
response.location.startswith('http://testserver/accounts/activate/')
|
|
assert User.objects.count() == 0
|
|
response = response.follow()
|
|
assert response.location.startswith('/fc/callback/')
|
|
# a new user has been created
|
|
assert User.objects.count() == 1
|
|
# but no FcAccount
|
|
assert models.FcAccount.objects.count() == 0
|
|
# we must be connected
|
|
assert app.session['_auth_user_id']
|
|
# hook must have been called
|
|
assert hooks.calls['event'][0]['kwargs']['service'] == 'portail'
|
|
|
|
response = response.follow()
|
|
# a new redirect to FC is done
|
|
response = franceconnect.handle_authorization(app, response.location)
|
|
|
|
# FcAccount now exists
|
|
assert models.FcAccount.objects.count() == 1
|
|
user = User.objects.get()
|
|
assert user.verified_attributes.first_name == 'Ÿuñe'
|
|
assert user.verified_attributes.last_name == 'Frédérique'
|
|
|
|
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.text
|
|
assert models.FcAccount.objects.count() == 0
|
|
continue_url = response.pyquery('a#a2-continue').attr['href']
|
|
response = franceconnect.handle_logout(app, continue_url)
|
|
assert path(response.location) == '/accounts/'
|
|
|
|
|
|
def test_registration2(settings, app, franceconnect, hooks):
|
|
response = app.get('/login/?service=portail&next=/idp/')
|
|
response = response.click("Register")
|
|
response = response.click(href='callback')
|
|
franceconnect.callback_params['registration'] = ''
|
|
response = franceconnect.handle_authorization(app, response.location)
|
|
|
|
assert User.objects.count() == 0
|
|
assert path(response.location) == '/accounts/fc/register/'
|
|
response = response.follow()
|
|
response.location.startswith('http://testserver/accounts/activate/')
|
|
response = response.follow()
|
|
assert User.objects.count() == 1
|
|
user = User.objects.get()
|
|
assert user.verified_attributes.first_name is None
|
|
assert user.verified_attributes.last_name is None
|
|
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']
|
|
response = response.follow()
|
|
|
|
del franceconnect.callback_params['registration']
|
|
response = franceconnect.handle_authorization(app, response.location)
|
|
user = User.objects.get()
|
|
assert user.verified_attributes.first_name == 'Ÿuñe'
|
|
assert user.verified_attributes.last_name == 'Frédérique'
|
|
|
|
|
|
def test_can_change_password(settings, app, franceconnect):
|
|
user = User.objects.create(email='john.doe@example.com')
|
|
models.FcAccount.objects.create(user=user, sub=franceconnect.sub)
|
|
|
|
response = franceconnect.login_with_fc(app, path='/accounts/')
|
|
assert len(response.pyquery('[href*="password/change"]')) == 0
|
|
response = response.click('Logout')
|
|
response = franceconnect.handle_logout(app, response.location).follow()
|
|
assert '_auth_user_id' not in app.session
|
|
|
|
# Login with password
|
|
user.username = 'test'
|
|
user.set_password('test')
|
|
user.save()
|
|
|
|
response = login(app, user, path='/accounts/')
|
|
assert len(response.pyquery('[href*="password/change"]')) > 0
|
|
response = response.click('Logout').follow()
|
|
|
|
# Relogin with FC
|
|
response = franceconnect.login_with_fc(app, path='/accounts/')
|
|
assert len(response.pyquery('[href*="password/change"]')) == 0
|
|
|
|
# Unlink
|
|
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.text
|
|
continue_url = response.pyquery('a#a2-continue').attr['href']
|
|
response = franceconnect.handle_logout(app, continue_url).follow()
|
|
assert len(response.pyquery('[href*="password/change"]')) > 0
|
|
|
|
|
|
def test_invalid_next_url(app, franceconnect):
|
|
assert app.get('/fc/callback/?code=coin&next=JJJ72QQQ').location == 'JJJ72QQQ'
|
|
|
|
|
|
def test_manager_user_sidebar(app, superuser, simple_user):
|
|
login(app, superuser, '/manage/')
|
|
response = app.get('/manage/users/%s/' % simple_user.id)
|
|
assert 'FranceConnect' not in response
|
|
|
|
fc_account = models.FcAccount(user=simple_user)
|
|
fc_account.save()
|
|
|
|
response = app.get('/manage/users/%s/' % simple_user.id)
|
|
assert 'FranceConnect' in response
|