diff --git a/core/admin/mailu/sso/views/base.py b/core/admin/mailu/sso/views/base.py index ff710add..55ed079e 100644 --- a/core/admin/mailu/sso/views/base.py +++ b/core/admin/mailu/sso/views/base.py @@ -13,6 +13,9 @@ from werkzeug.urls import url_unquote @sso.route('/login', methods=['GET', 'POST']) def login(): + if flask.request.headers.get(app.config['PROXY_AUTH_HEADER']) and not 'noproxyauth' in flask.request.url: + return _proxy() + client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) form = forms.LoginForm() @@ -30,15 +33,11 @@ def login(): fields = [fields] if form.validate_on_submit(): - if form.submitAdmin.data: - destination = app.config['WEB_ADMIN'] - elif form.submitWebmail.data: - destination = app.config['WEB_WEBMAIL'] - if url := flask.request.args.get('url'): - url = url_unquote(url) - target = urlparse(urljoin(flask.request.url, url)) - if target.netloc == urlparse(flask.request.url).netloc: - destination = target.geturl() + if not destination := _has_usable_redirect(): + if form.submitAdmin.data: + destination = app.config['WEB_ADMIN'] + elif form.submitWebmail.data: + destination = app.config['WEB_WEBMAIL'] device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit')) username = form.email.data if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip): @@ -73,10 +72,22 @@ def logout(): response.set_cookie(cookie, 'empty', expires=0) return response +""" +Redirect to the url passed in parameter if any; Ensure that this is not an open-redirect too... +https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html +""" +def _has_usable_redirect(): + if url := flask.request.args.get('url'): + url = url_unquote(url) + target = urlparse(urljoin(flask.request.url, url)) + if target.netloc == urlparse(flask.request.url).netloc: + return target.geturl() + return None -@sso.route('/proxy', methods=['GET']) -@sso.route('/proxy/', methods=['GET']) -def proxy(target='webmail'): +""" +https://mailu.io/master/configuration.html#header-authentication-using-an-external-proxy +""" +def _proxy(): ip = ipaddress.ip_address(flask.request.remote_addr) if not any(ip in cidr for cidr in app.config['PROXY_AUTH_WHITELIST']): return flask.abort(500, '%s is not on PROXY_AUTH_WHITELIST' % flask.request.remote_addr) @@ -85,11 +96,13 @@ def proxy(target='webmail'): if not email: return flask.abort(500, 'No %s header' % app.config['PROXY_AUTH_HEADER']) + url = _has_usable_redirect or app.config['WEB_ADMIN'] + user = models.User.get(email) if user: flask.session.regenerate() flask_login.login_user(user) - return flask.redirect(app.config['WEB_ADMIN'] if target=='admin' else app.config['WEB_WEBMAIL']) + return flask.redirect(url) if not app.config['PROXY_AUTH_CREATE']: return flask.abort(500, 'You don\'t exist. Go away! (%s)' % email) @@ -108,6 +121,8 @@ def proxy(target='webmail'): user.set_password(secrets.token_urlsafe()) models.db.session.add(user) models.db.session.commit() + flask.session.regenerate() + flask_login.login_user(user) user.send_welcome() flask.current_app.logger.info(f'Login succeeded by proxy created user: {user} from {client_ip} through {flask.request.remote_addr}.') - return flask.redirect(app.config['WEB_ADMIN'] if target=='admin' else app.config['WEB_WEBMAIL']) + return flask.redirect(url) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index bbf0a829..ced16b7f 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -33,7 +33,6 @@ from flask.sessions import SessionMixin, SessionInterface from itsdangerous.encoding import want_bytes from werkzeug.datastructures import CallbackDict from werkzeug.middleware.proxy_fix import ProxyFix -from werkzeug.urls import url_quote # Login configuration login = flask_login.LoginManager() @@ -43,7 +42,7 @@ login.login_view = "sso.login" def handle_needs_login(): """ redirect unauthorized requests to login page """ return flask.redirect( - flask.url_for('sso.login')+f'?url={url_quote(flask.request.url)}' + flask.url_for('sso.login', url=flask.request.url) ) # DNS stub configured to do DNSSEC enabled queries diff --git a/docs/configuration.rst b/docs/configuration.rst index d1175f3f..aec62a8f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -375,8 +375,9 @@ The ``PROXY_AUTH_WHITELIST`` (default: unset/disabled) option allows you to conf Use ``PROXY_AUTH_HEADER`` (default: 'X-Auth-Email') to customize which HTTP header the email address of the user to authenticate as should be and ``PROXY_AUTH_CREATE`` (default: False) to control whether non-existing accounts should be auto-created. Please note that Mailu doesn't currently support creating new users for non-existing domains; you do need to create all the domains that may be used manually. -Once configured, any request to /sso/proxy will be redirected to the webmail and /sso/proxy/admin to the admin panel. Please check issue `1972` for more details. +Once configured, any request to /sso/login with the correct headers will be authenticated unless the "noproxyauth" parameter is passed, in which case the "standard" login form will be displayed. Please check issues `1972` and `2692` for more details. Use ``PROXY_AUTH_LOGOUT_URL`` (default: unset) to redirect users to a specific URL after they have been logged out. .. _`1972`: https://github.com/Mailu/Mailu/issues/1972 +.. _`2692`: https://github.com/Mailu/Mailu/issues/2692 diff --git a/towncrier/newsfragments/2692.misc b/towncrier/newsfragments/2692.misc index c8f6f8fc..30d73843 100644 --- a/towncrier/newsfragments/2692.misc +++ b/towncrier/newsfragments/2692.misc @@ -1,2 +1,3 @@ Make the login page "guess" where the user wants to land Introduce AUTH_PROXY_LOGOUT_URL to redirect users to a specific URL after they have been logged-out +Retire /sso/proxy and merge it in /sso/login