Skip to content

Commit

Permalink
Gazette: 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 560b44e commit ce13e5b
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
17 changes: 16 additions & 1 deletion src/onegov/gazette/locale/de_CH/LC_MESSAGES/onegov.gazette.po
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-10 17:10+0100\n"
"POT-Creation-Date: 2024-08-08 10:53+0200\n"
"PO-Revision-Date: 2019-04-01 07:57+0200\n"
"Last-Translator: Marc Sommerhalder <[email protected]>\n"
"Language-Team: German\n"
Expand Down Expand Up @@ -495,6 +495,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"

msgid "Category added."
msgstr "Rubrik hinzugefügt."

Expand Down
62 changes: 62 additions & 0 deletions src/onegov/gazette/views/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
from onegov.gazette.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 @@ -163,3 +166,62 @@ def handle_password_reset(
'show_form': show_form,
'callout': callout
}


@GazetteApp.form(
model=Auth,
name='totp',
template='form.pt',
permission=Public,
form=TOTPForm
)
def handle_totp_second_factor(
self: Auth,
request: 'GazetteRequest',
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': Layout(self, request),
'title': _('Enter TOTP'),
'form': form,
'form_width': 'small'
}
45 changes: 43 additions & 2 deletions tests/onegov/gazette/test_views_auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import onegov.gazette
import os
import pyotp
import transaction

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


@mark.skip('Will mess up tests in the CI')
Expand Down Expand Up @@ -37,7 +41,7 @@ def test_view_login_logout(gazette_app):
login.form['password'] = 'hunter2'
page = login.form.submit().maybe_follow()

assert 'Angemeldet als {}'.format(realname or username) in page
assert f'Angemeldet als {realname or username}' in page
assert 'Abmelden' in page
assert 'Anmelden' not in page

Expand Down Expand Up @@ -99,3 +103,40 @@ def test_view_reset_password(gazette_app):
login_page.form['password'] = 'new_password'
login_page = login_page.form.submit().maybe_follow()
assert "Angemeldet als [email protected]" in login_page.maybe_follow()


def test_login_totp(gazette_app):
gazette_app.totp_enabled = True
client = Client(gazette_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('/').maybe_follow().click('Anmelden')
login_page.form['username'] = '[email protected]'
login_page.form['password'] = 'hunter2'

totp_page = login_page.form.submit().maybe_follow()
assert "TOTP eingeben" 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 'Angemeldet als [email protected]' in page
assert 'Abmelden' in page
assert 'Anmelden' not in page

page = client.get('/').maybe_follow().click('Abmelden').maybe_follow()
assert 'Sie sind angemeldet' not in page
assert 'Abmelden' not in page
assert 'Anmelden' in page

0 comments on commit ce13e5b

Please sign in to comment.