diff --git a/src/onegov/election_day/locale/de_CH/LC_MESSAGES/onegov.election_day.po b/src/onegov/election_day/locale/de_CH/LC_MESSAGES/onegov.election_day.po index 375501b9eb..0b6506c54d 100644 --- a/src/onegov/election_day/locale/de_CH/LC_MESSAGES/onegov.election_day.po +++ b/src/onegov/election_day/locale/de_CH/LC_MESSAGES/onegov.election_day.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-08 09:34+0200\n" +"POT-Creation-Date: 2024-08-08 12:33+0200\n" "PO-Revision-Date: 2022-03-24 15:35+0100\n" "Last-Translator: Marc Sommerhalder \n" "Language-Team: German\n" @@ -1532,6 +1532,21 @@ msgstr "Passwort geändert." msgid "Wrong username or password reset link not valid any more." msgstr "Ungültige Adresse oder abgelaufener Link." +msgid "Failed to continue login, please ensure cookies are allowed." +msgstr "" +"Das Fortsetzen des Logins ist fehlgeschlagen, bitte stellen Sie sicher, dass " +"Sie Cookies erlauben." + +msgid "Invalid or expired TOTP provided." +msgstr "Ungültige oder abgelaufenes TOTP eingegeben." + +msgid "Please enter the six digit code from your authenticator app" +msgstr "" +"Bitte geben Sie den sechsstelligen Code aus ihrer Authenticator App ein" + +msgid "Enter TOTP" +msgstr "TOTP eingeben" + #, python-format msgid "" "The map shows the percentage of votes for the selected candidate by ${by}." diff --git a/src/onegov/election_day/locale/fr_CH/LC_MESSAGES/onegov.election_day.po b/src/onegov/election_day/locale/fr_CH/LC_MESSAGES/onegov.election_day.po index 864e2278f7..3b8fb25a38 100644 --- a/src/onegov/election_day/locale/fr_CH/LC_MESSAGES/onegov.election_day.po +++ b/src/onegov/election_day/locale/fr_CH/LC_MESSAGES/onegov.election_day.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-08 09:34+0200\n" +"POT-Creation-Date: 2024-08-08 12:33+0200\n" "PO-Revision-Date: 2022-03-22 07:59+0100\n" "Last-Translator: Marc Sommerhalder \n" "Language-Team: \n" @@ -1541,6 +1541,22 @@ msgstr "" "Identifiant ou mot de passe erroné. Le lien de réinitialisation du mot de " "passe a expiré." +msgid "Failed to continue login, please ensure cookies are allowed." +msgstr "" +"Échec de la poursuite de la connexion, veuillez vous assurer que les cookies " +"sont autorisés." + +msgid "Invalid or expired TOTP provided." +msgstr "TOTP fourni non valide ou expiré." + +msgid "Please enter the six digit code from your authenticator app" +msgstr "" +"Veuillez saisir le code à six chiffres de votre application " +"d'authentification" + +msgid "Enter TOTP" +msgstr "Entrer TOTP" + #, python-format msgid "" "The map shows the percentage of votes for the selected candidate by ${by}." diff --git a/src/onegov/election_day/locale/it_CH/LC_MESSAGES/onegov.election_day.po b/src/onegov/election_day/locale/it_CH/LC_MESSAGES/onegov.election_day.po index 4d7a7b26ee..ca555598e0 100644 --- a/src/onegov/election_day/locale/it_CH/LC_MESSAGES/onegov.election_day.po +++ b/src/onegov/election_day/locale/it_CH/LC_MESSAGES/onegov.election_day.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-08 09:34+0200\n" +"POT-Creation-Date: 2024-08-08 12:33+0200\n" "PO-Revision-Date: 2022-03-22 08:00+0100\n" "Last-Translator: Marc Sommerhalder \n" "Language-Team: \n" @@ -1528,6 +1528,18 @@ msgstr "" "Il collegamento per reimpostare il nuome utente o la password errati non è " "più valido." +msgid "Failed to continue login, please ensure cookies are allowed." +msgstr "Accesso fallito, assicurarsi che i cookie siano consentiti." + +msgid "Invalid or expired TOTP provided." +msgstr "È stato fornito un TOTP non valido o scaduto." + +msgid "Please enter the six digit code from your authenticator app" +msgstr "Immettere il codice a sei cifre dall'app Autenticatore" + +msgid "Enter TOTP" +msgstr "Immettre TOTP" + #, python-format msgid "" "The map shows the percentage of votes for the selected candidate by ${by}." diff --git a/src/onegov/election_day/locale/rm_CH/LC_MESSAGES/onegov.election_day.po b/src/onegov/election_day/locale/rm_CH/LC_MESSAGES/onegov.election_day.po index bc8e94d14a..f950978970 100644 --- a/src/onegov/election_day/locale/rm_CH/LC_MESSAGES/onegov.election_day.po +++ b/src/onegov/election_day/locale/rm_CH/LC_MESSAGES/onegov.election_day.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-08 09:34+0200\n" +"POT-Creation-Date: 2024-08-08 12:33+0200\n" "PO-Revision-Date: 2022-03-22 08:01+0100\n" "Last-Translator: Marc Sommerhalder \n" "Language-Team: \n" @@ -1544,6 +1544,19 @@ msgstr "Midà il pled-clav." msgid "Wrong username or password reset link not valid any more." msgstr "L'adressa è nunvalaivla u il link è nunvalaivel." +msgid "Failed to continue login, please ensure cookies are allowed." +msgstr "" +"Sch'ins na cuntinuescha betg cun login, alura èn las cuschinas lubidas." + +msgid "Invalid or expired TOTP provided." +msgstr "Invalida u ch'è scrudada TOTP tenor la disposiziun." + +msgid "Please enter the six digit code from your authenticator app" +msgstr "As inditgai en il sis digital da Vossa app per autenticatorica" + +msgid "Enter TOTP" +msgstr "Endatar TOTP" + #, python-format msgid "" "The map shows the percentage of votes for the selected candidate by ${by}." diff --git a/src/onegov/election_day/views/auth.py b/src/onegov/election_day/views/auth.py index 02cf7cc626..0e9b67f77e 100644 --- a/src/onegov/election_day/views/auth.py +++ b/src/onegov/election_day/views/auth.py @@ -1,5 +1,5 @@ """ The authentication views. """ - +from morepath import redirect from onegov.core.security import Private from onegov.core.security import Public from onegov.core.templates import render_template @@ -11,10 +11,13 @@ from onegov.election_day.models import Principal from onegov.user import Auth from onegov.user import UserCollection +from onegov.user.auth.second_factor import TOTPFactor from onegov.user.forms import LoginForm from onegov.user.forms import PasswordResetForm from onegov.user.forms import RequestPasswordResetForm +from onegov.user.forms import TOTPForm from onegov.user.utils import password_reset_url +from webob import exc from typing import TYPE_CHECKING @@ -179,3 +182,61 @@ def handle_password_reset( 'show_form': show_form, 'callout': callout } + + +@ElectionDayApp.form( + model=Auth, + name='totp', + template='form.pt', + permission=Public, + form=TOTPForm +) +def handle_totp_second_factor( + self: Auth, + request: 'ElectionDayRequest', + form: TOTPForm +) -> 'RenderData | Response': + + if not request.app.totp_enabled: + raise exc.HTTPNotFound() + + @request.after + def respond_with_no_index(response: 'Response') -> None: + response.headers['X-Robots-Tag'] = 'noindex' + + users = UserCollection(request.session) + username = request.browser_session.get('pending_username') + user = users.by_username(username) if username else None + if user is None: + if request.is_logged_in: + # redirect already logged in users to the redirect_to + return self.redirect(request, self.to) + + request.alert( + _("Failed to continue login, please ensure cookies are allowed.") + ) + return redirect(request.link(self, name='login')) + + if form.submitted(request): + assert form.totp.data is not None + factor = self.factors['totp'] + assert isinstance(factor, TOTPFactor) + + if factor.is_valid(request, user, form.totp.data): + del request.browser_session['pending_username'] + return self.complete_login(user, request) + else: + request.alert(_('Invalid or expired TOTP provided.')) + client = request.client_addr or 'unknown' + log.info(f'Failed login by {client} (TOTP)') + else: + request.info( + _('Please enter the six digit code from your authenticator app') + ) + + return { + 'layout': DefaultLayout(self, request), + 'title': _('Enter TOTP'), + 'form': form, + 'form_width': 'small' + } diff --git a/tests/onegov/election_day/views/test_views.py b/tests/onegov/election_day/views/test_views.py index c05dfc569c..7e67c65636 100644 --- a/tests/onegov/election_day/views/test_views.py +++ b/tests/onegov/election_day/views/test_views.py @@ -1,8 +1,13 @@ +import pyotp +import transaction + from freezegun import freeze_time from onegov import election_day from onegov.election_day import ElectionDayApp from onegov.election_day.models import Ballot from onegov.election_day.models import Vote +from onegov.user import UserCollection +from sqlalchemy.orm.session import close_all_sessions from tests.onegov.election_day.common import login from tests.onegov.election_day.common import upload_election_compound from tests.onegov.election_day.common import upload_majorz_election @@ -36,6 +41,40 @@ def test_view_private(election_day_app_zg): login(client) +def test_login_totp(election_day_app_zg): + election_day_app_zg.totp_enabled = True + client = Client(election_day_app_zg) + + totp_secret = pyotp.random_base32() + totp = pyotp.TOTP(totp_secret) + + # configure TOTP for admin user + users = UserCollection(client.app.session()) + admin = users.by_username('admin@example.org') + admin.second_factor = {'type': 'totp', 'data': totp_secret} + transaction.commit() + close_all_sessions() + + login_page = client.get('/').maybe_follow().click('Anmelden') + login_page.form['username'] = 'admin@example.org' + login_page.form['password'] = 'hunter2' + + totp_page = login_page.form.submit().maybe_follow() + assert "Bitte geben Sie den sechsstelligen Code" in totp_page.text + totp_page.form['totp'] = 'bogus' + totp_page = totp_page.form.submit() + assert "Ungültige oder abgelaufenes TOTP eingegeben." in totp_page.text + + totp_page.form['totp'] = totp.now() + page = totp_page.form.submit().maybe_follow() + assert 'Abmelden' in page + assert 'Anmelden' not in page + + page = client.get('/').maybe_follow().click('Abmelden').maybe_follow() + assert 'Abmelden' not in page + assert 'Anmelden' in page + + def test_i18n(election_day_app_zg): client = Client(election_day_app_zg) client.get('/locale/de_CH').follow()