Skip to content

Commit

Permalink
ElectionDay: Adds Auth view for TOTP second factor
Browse files Browse the repository at this point in the history
TYPE: Feature
LINK: SEA-1413
  • Loading branch information
Daverball authored Aug 8, 2024
1 parent 91a94ce commit 9adbe66
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language-Team: German\n"
Expand Down Expand Up @@ -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}."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language-Team: \n"
Expand Down Expand Up @@ -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}."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language-Team: \n"
Expand Down Expand Up @@ -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}."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language-Team: \n"
Expand Down Expand Up @@ -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}."
Expand Down
63 changes: 62 additions & 1 deletion src/onegov/election_day/views/auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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'
}
39 changes: 39 additions & 0 deletions tests/onegov/election_day/views/test_views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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('[email protected]')
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'] = '[email protected]'
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()
Expand Down

0 comments on commit 9adbe66

Please sign in to comment.