diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index b958537c..4f3f8e84 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -86,6 +86,7 @@ DEFAULT_CONFIG = { 'PROXY_AUTH_WHITELIST': '', 'PROXY_AUTH_HEADER': 'X-Auth-Email', 'PROXY_AUTH_CREATE': False, + 'PROXY_AUTH_LOGOUT_URL': None, 'SUBNET': '192.168.203.0/24', 'SUBNET6': None, } diff --git a/core/admin/mailu/sso/views/base.py b/core/admin/mailu/sso/views/base.py index 5ca4a52d..6535a98c 100644 --- a/core/admin/mailu/sso/views/base.py +++ b/core/admin/mailu/sso/views/base.py @@ -8,26 +8,38 @@ import flask import flask_login import secrets import ipaddress +from urllib.parse import urlparse, urljoin +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() - form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin' - form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail' fields = [] - if str(app.config["WEBMAIL"]).upper() != "NONE": - fields.append(form.submitWebmail) - if str(app.config["ADMIN"]).upper() != "FALSE": + + if 'url' in flask.request.args and not 'homepage' in flask.request.url: fields.append(form.submitAdmin) + else: + form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin' + form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail' + if str(app.config["WEBMAIL"]).upper() != "NONE": + fields.append(form.submitWebmail) + if str(app.config["ADMIN"]).upper() != "FALSE": + fields.append(form.submitAdmin) 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 destination := _has_usable_redirect(): + pass + else: + 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): @@ -57,14 +69,29 @@ def login(): def logout(): flask_login.logout_user() flask.session.destroy() - response = flask.redirect(flask.url_for('.login')) + response = flask.redirect(app.config['PROXY_AUTH_LOGOUT_URL'] or flask.url_for('.login')) for cookie in ['roundcube_sessauth', 'roundcube_sessid', 'smsession']: response.set_cookie(cookie, 'empty', expires=0) return response -@sso.route('/proxy', methods=['GET']) -@sso.route('/proxy/', methods=['GET']) -def proxy(target='webmail'): +""" +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 'homepage' in flask.request.url: + return None + 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 + +""" +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) @@ -73,11 +100,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) @@ -100,4 +129,4 @@ def proxy(target='webmail'): 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 b432192d..ced16b7f 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -42,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') + flask.url_for('sso.login', url=flask.request.url) ) # DNS stub configured to do DNSSEC enabled queries diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index 89eeb4bf..85f22e45 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -173,11 +173,15 @@ http { } {% endif %} + location @sso_login { + return 302 /sso/login?url=$request_uri; + } + {% if WEB_WEBMAIL != '/' and WEBROOT_REDIRECT != 'none' %} location / { expires $expires; {% if WEBROOT_REDIRECT %} - try_files $uri {{ WEBROOT_REDIRECT }}; + try_files $uri {{ WEBROOT_REDIRECT }}?homepage; {% else %} try_files $uri =404; {% endif %} @@ -192,7 +196,7 @@ http { {% endif %} include /etc/nginx/proxy.conf; auth_request /internal/auth/user; - error_page 403 @webmail_login; + error_page 403 @sso_login; proxy_pass http://$webmail; } @@ -211,13 +215,9 @@ http { auth_request_set $token $upstream_http_x_user_token; proxy_set_header X-Remote-User $user; proxy_set_header X-Remote-User-Token $token; - error_page 403 @webmail_login; + error_page 403 @sso_login; proxy_pass http://$webmail; } - - location @webmail_login { - return 302 /sso/login; - } {% endif %} {% if ADMIN %} location {{ WEB_ADMIN }} { @@ -232,6 +232,7 @@ http { proxy_set_header X-Real-IP ""; proxy_set_header X-Forwarded-For ""; proxy_pass http://$antispam; + error_page 403 @sso_login; } {% endif %} diff --git a/docs/configuration.rst b/docs/configuration.rst index abb0860d..645cd85d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -375,6 +375,15 @@ 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. + +Requests to: + +- "/sso/login" results the user being redirected to the web administration interface after authentication. +- "/admin" (``WEB_ADMIN=/admin``) results the user being redirected to the web administration interface after authentication. +- "/webmail" (``WEB_WEBMAIL=/webmail``) results the user being redirected to the web administration interface after authentication. + +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 new file mode 100644 index 00000000..9342c06b --- /dev/null +++ b/towncrier/newsfragments/2692.misc @@ -0,0 +1,7 @@ +Make the login page "guess" where the user wants to land. +This means that requests for /admin result in a login page that always redirects to admin. +Requests for /webmail results in a login page that redirects the user being logged in to webmail. +You can still access / (https://mydomain/) or /sso/login, to access the login page with both login buttons. + +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