From 906a051925766058feec60c0b768e274ccd7c862 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 6 Feb 2021 17:23:05 +0100 Subject: [PATCH] Make rainloop use internal auth --- core/admin/mailu/internal/nginx.py | 19 ++++++++++----- core/admin/mailu/internal/views/auth.py | 12 ++++++++++ core/admin/mailu/models.py | 10 ++++++++ core/admin/mailu/ui/views/base.py | 3 +++ core/nginx/conf/nginx.conf | 20 ++++++++++++++++ webmails/rainloop/Dockerfile | 1 + webmails/rainloop/application.ini | 2 ++ webmails/rainloop/sso.php | 31 +++++++++++++++++++++++++ webmails/rainloop/start.py | 1 + 9 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 webmails/rainloop/sso.php diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 1e0b16c2..a41543d3 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -7,7 +7,6 @@ import ipaddress import socket import tenacity - SUPPORTED_AUTH_METHODS = ["none", "plain"] @@ -49,11 +48,19 @@ def handle_authentication(headers): user = models.User.query.get(user_email) status = False if user: - for token in user.tokens: - if (token.check_password(password) and - (not token.ip or token.ip == ip)): - status = True - if user.check_password(password): + # webmails + if len(password) == 64 and ip == app.config['WEBMAIL_ADDRESS']: + if user.verify_temp_token(password): + status = True + + # All tokens are 32 characters hex lowercase + if len(password) == 32: + for token in user.tokens: + if (token.check_password(password) and + (not token.ip or token.ip == ip)): + status = True + break + if not status and user.check_password(password): status = True if status: if protocol == "imap" and not user.enable_imap: diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 825dba56..338141d8 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -43,6 +43,18 @@ def admin_authentication(): return "" return flask.abort(403) +@internal.route("/auth/user") +def user_authentication(): + """ Fails if the user is not authenticated. + """ + if (not flask_login.current_user.is_anonymous + and flask_login.current_user.enabled): + response = flask.Response() + response.headers["X-User"] = flask_login.current_user.get_id() + response.headers["X-User-Token"] = models.User.get_temp_token(flask_login.current_user.get_id()) + return response + return flask.abort(403) + @internal.route("/auth/basic") def basic_authentication(): diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index bbc00f2d..6230e252 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -12,6 +12,7 @@ import re import time import os import glob +import hmac import smtplib import idna import dns @@ -425,6 +426,15 @@ class User(Base, Email): user = cls.query.get(email) return user if (user and user.enabled and user.check_password(password)) else None + @classmethod + def get_temp_token(cls, email): + user = cls.query.get(email) + return hmac.new(bytearray(app.secret_key,'utf-8'), bytearray("{}|{}".format(datetime.utcnow().strftime("%Y%m%d"), email), 'utf-8'), 'sha256').hexdigest() if (user and user.enabled) else None + + def verify_temp_token(self, token): + return hmac.compare_digest(b''.fromhex(self.get_temp_token(self.email)), b''.fromhex(token)) + + class Alias(Base, Email): """ An alias is an email address that redirects to some destination. diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index 7501a883..1aff7db1 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -47,6 +47,9 @@ def announcement(): flask.flash('Your announcement was sent', 'success') return flask.render_template('announcement.html', form=form) +@ui.route('/webmail', methods=['GET']) +def webmail(): + return flask.redirect(app.config['WEB_WEBMAIL']) @ui.route('/client', methods=['GET']) def client(): diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index df598c94..a7a9e134 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -133,7 +133,27 @@ http { {% endif %} include /etc/nginx/proxy.conf; client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }}; + auth_request /internal/auth/user; proxy_pass http://$webmail; + error_page 403 @webmail_login; + } + location {{ WEB_WEBMAIL }}/sso.php { + {% if WEB_WEBMAIL != '/' %} + rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent; + rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break; + {% endif %} + include /etc/nginx/proxy.conf; + client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }}; + auth_request /internal/auth/user; + auth_request_set $user $upstream_http_x_user; + auth_request_set $token $upstream_http_x_user_token; + proxy_set_header X-Remote-User $user; + proxy_set_header X-Remote-User-Token $token; + proxy_pass http://$webmail; + error_page 403 @webmail_login; + } + location @webmail_login { + return 302 {{ WEB_ADMIN }}/ui/login?next=ui.webmail; } {% endif %} diff --git a/webmails/rainloop/Dockerfile b/webmails/rainloop/Dockerfile index c67c7496..640d7176 100644 --- a/webmails/rainloop/Dockerfile +++ b/webmails/rainloop/Dockerfile @@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists COPY include.php /var/www/html/include.php +COPY sso.php /var/www/html/sso.php COPY php.ini /php.ini COPY application.ini /application.ini diff --git a/webmails/rainloop/application.ini b/webmails/rainloop/application.ini index 5bd9943d..bc953af1 100644 --- a/webmails/rainloop/application.ini +++ b/webmails/rainloop/application.ini @@ -7,6 +7,8 @@ attachment_size_limit = {{ MAX_FILESIZE }} allow_admin_panel = Off [labs] +custom_login_link='sso.php' +custom_logout_link='{{ WEB_ADMIN }}/ui/logout' allow_gravatar = Off [contacts] diff --git a/webmails/rainloop/sso.php b/webmails/rainloop/sso.php new file mode 100644 index 00000000..2415f45c --- /dev/null +++ b/webmails/rainloop/sso.php @@ -0,0 +1,31 @@ +