Skip to content

Commit

Permalink
Swissvotes: 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 9adbe66 commit e0db0be
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-04-28 15:08+0200\n"
"POT-Creation-Date: 2024-08-08 12:56+0200\n"
"PO-Revision-Date: 2021-01-07 09:13+0100\n"
"Last-Translator: Marc Sommerhalder <[email protected]>\n"
"Language-Team: \n"
Expand Down Expand Up @@ -2029,6 +2029,21 @@ msgstr "Passwort geändert."
msgid "Wrong username or password reset link not valid any more."
msgstr "Ungültige E-Mail 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"

msgid "Access Denied"
msgstr "Zugriff verweigert"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-04-28 15:08+0200\n"
"POT-Creation-Date: 2024-08-08 12:56+0200\n"
"PO-Revision-Date: 2021-01-07 09:11+0100\n"
"Last-Translator: Marc Sommerhalder <[email protected]>\n"
"Language-Team: \n"
Expand Down Expand Up @@ -2073,6 +2073,18 @@ msgstr "Password changed."
msgid "Wrong username or password reset link not valid any more."
msgstr "Wrong username or password reset link has expired."

msgid "Failed to continue login, please ensure cookies are allowed."
msgstr "Failed to continue login, please ensure cookies are allowed."

msgid "Invalid or expired TOTP provided."
msgstr "Invalid or expired TOTP provided."

msgid "Please enter the six digit code from your authenticator app"
msgstr "Please enter the six digit code from your authenticator app"

msgid "Enter TOTP"
msgstr "Enter TOTP"

msgid "Access Denied"
msgstr "Access denied"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-04-28 15:08+0200\n"
"POT-Creation-Date: 2024-08-08 12:56+0200\n"
"PO-Revision-Date: 2021-01-07 09:13+0100\n"
"Last-Translator: Marc Sommerhalder <[email protected]>\n"
"Language-Team: \n"
Expand Down Expand Up @@ -2033,6 +2033,22 @@ msgstr "Le mot de passe a été changé."
msgid "Wrong username or password reset link not valid any more."
msgstr "E-Mail non valide ou expiration de la durée de validité du lien."

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"

msgid "Access Denied"
msgstr "Accès refusé"

Expand Down
62 changes: 62 additions & 0 deletions src/onegov/swissvotes/views/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from morepath import redirect
from onegov.core.security import Personal
from onegov.core.security import Public
from onegov.core.templates import render_template
Expand All @@ -9,10 +10,13 @@
from onegov.swissvotes.layouts import MailLayout
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 @@ -171,3 +175,61 @@ def handle_password_reset(
'form': form,
'button_text': _("Submit"),
}


@SwissvotesApp.form(
model=Auth,
name='totp',
template='form.pt',
permission=Public,
form=TOTPForm
)
def handle_totp_second_factor(
self: Auth,
request: 'SwissvotesRequest',
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'
}
37 changes: 37 additions & 0 deletions tests/onegov/swissvotes/test_views_auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import onegov
import os
import pyotp
import transaction

from lxml.html import document_fromstring
from onegov.user import UserCollection
from sqlalchemy.orm.session import close_all_sessions
from tests.shared import Client, utils


Expand Down Expand Up @@ -87,3 +92,35 @@ def test_view_reset_password(swissvotes_app):
login_page.form['password'] = 'password2'
login_page = login_page.form.submit().maybe_follow()
assert "Abmelden" in login_page.maybe_follow()


def test_login_totp(swissvotes_app):
swissvotes_app.totp_enabled = True
client = Client(swissvotes_app)

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('/auth/login')
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

page = client.get('/').maybe_follow().click('Abmelden').maybe_follow()
assert 'Abmelden' not in page

0 comments on commit e0db0be

Please sign in to comment.