diff --git a/uffd/mfa/templates/auth.html b/uffd/mfa/templates/auth.html index 945c4357233cf1c261c1b85e900535874c78e78c..831bc87a74ed93418ee6ec6139765cf0ac656c7b 100644 --- a/uffd/mfa/templates/auth.html +++ b/uffd/mfa/templates/auth.html @@ -50,8 +50,16 @@ function begin_webauthn() { fetch({{ url_for('mfa.auth_webauthn_begin')|tojson }}, { method: 'POST', }).then(function(response) { - if(response.ok) return response.arrayBuffer(); - throw new Error('You have not registered any U2F/FIDO2 devices for your account'); + if (response.ok) { + return response.arrayBuffer(); + } else if (response.status == 403) { + window.location = {{ request.url|tojson }}; /* reload */ + throw new Error('Session timed out'); + } else if (response.status == 404) { + throw new Error('You have not registered any U2F/FIDO2 devices for your account'); + } else { + throw new Error('Server error'); + } }).then(CBOR.decode).then(function(options) { $('#webauthn-btn-text').text('Waiting for response from your device'); return navigator.credentials.get(options); @@ -72,10 +80,13 @@ function begin_webauthn() { $('#webauthn-spinner').addClass('d-none'); $('#webauthn-btn-text').text('Success, redirecting'); window.location = {{ (ref or url_for('index'))|tojson }}; + } else if (response.status == 403) { + window.location = {{ request.url|tojson }}; /* reload */ + throw new Error('Session timed out'); } else { throw new Error('Response from authenticator rejected'); } - }, function(err) { + }).catch(function(err) { console.log(err); /* various webauthn errors */ if (err.name == 'NotAllowedError') diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py index 62b0f4d027a9344cc9c81dffade811582eba3168..7255ae026ec9c3992d9fdc88b917b76cb5edd8d4 100644 --- a/uffd/mfa/views.py +++ b/uffd/mfa/views.py @@ -5,7 +5,7 @@ from flask import Blueprint, render_template, session, request, redirect, url_fo from uffd.database import db from uffd.mfa.models import MFAMethod, TOTPMethod, WebauthnMethod, RecoveryCodeMethod -from uffd.session.views import get_current_user, login_required +from uffd.session.views import get_current_user, login_required, pre_mfa_login_required from uffd.ldap import uid_to_dn from uffd.user.models import User from uffd.csrf import csrf_protect @@ -156,6 +156,7 @@ if WEBAUTHN_SUPPORTED: return cbor.dumps({"status": "OK"}) @bp.route("/auth/webauthn/begin", methods=["POST"]) + @pre_mfa_login_required(no_redirect=True) def auth_webauthn_begin(): user = get_current_user() server = get_webauthn_server() @@ -168,6 +169,7 @@ if WEBAUTHN_SUPPORTED: return cbor.dumps(auth_data) @bp.route("/auth/webauthn/complete", methods=["POST"]) + @pre_mfa_login_required(no_redirect=True) def auth_webauthn_complete(): user = get_current_user() server = get_webauthn_server() @@ -204,7 +206,7 @@ def delete_webauthn(id): #pylint: disable=redefined-builtin return redirect(url_for('mfa.setup')) @bp.route('/auth', methods=['GET']) -@login_required(skip_mfa=True) +@pre_mfa_login_required() def auth(): user = get_current_user() recovery_methods = RecoveryCodeMethod.query.filter_by(dn=user.dn).all() @@ -218,7 +220,7 @@ def auth(): webauthn_methods=webauthn_methods, recovery_methods=recovery_methods) @bp.route('/auth', methods=['POST']) -@login_required(skip_mfa=True) +@pre_mfa_login_required() def auth_finish(): user = get_current_user() recovery_methods = RecoveryCodeMethod.query.filter_by(dn=user.dn).all() diff --git a/uffd/session/views.py b/uffd/session/views.py index 561d190956579068833aab8028672e31ce98c227..310ceaf67fb3dabf0d3c53228bd4b9b54b44965c 100644 --- a/uffd/session/views.py +++ b/uffd/session/views.py @@ -2,7 +2,7 @@ import datetime import secrets import functools -from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session +from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session, abort from uffd.user.models import User from uffd.ldap import user_conn, uid_to_dn @@ -59,15 +59,28 @@ def is_valid_session(): return True bp.add_app_template_global(is_valid_session) -def login_required(group=None, skip_mfa=False): +def pre_mfa_login_required(no_redirect=False): + def wrapper(func): + @functools.wraps(func) + def decorator(*args, **kwargs): + if not login_valid() or datetime.datetime.now().timestamp() > session['logintime'] + 10*60: + session.clear() + if no_redirect: + abort(403) + flash('You need to login first') + return redirect(url_for('session.login', ref=request.url)) + return func(*args, **kwargs) + return decorator + return wrapper + +def login_required(group=None): def wrapper(func): @functools.wraps(func) def decorator(*args, **kwargs): if not login_valid(): flash('You need to login first') return redirect(url_for('session.login', ref=request.url)) - if not skip_mfa and not session.get('user_mfa'): - print('redirecting login_required', skip_mfa, session.get('user_mfa')) + if not session.get('user_mfa'): return redirect(url_for('mfa.auth', ref=request.url)) if not get_current_user().is_in_group(group): flash('Access denied')