Merge pull request #1390 from PhilippMundhenk/master

implemented ProxyAuth
This commit is contained in:
the-djmaze 2024-01-16 01:36:45 +01:00 committed by GitHub
commit e4f3df3bb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 270 additions and 0 deletions

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2023 Philipp Mundhenk
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,66 @@
# SnappyMail Proxy Auth
This plugin allows to authenticate a user through the remote user header, effectively allowing single-sign on.
This is achieved through "master user"-like functionality.
## Example Configuration
The exact setup depends on your mailserver, reverse proxy, authentication solution, etc.
The following example is for Traefik with Authelia and Dovecot as mailserver.
### SnappyMail
The following steps are require in SnappyMail:
- To open SnappyMail through a reverse proxy server, make sure to enable the correct secfetch policies: ```mode=navigate,dest=document,site=cross-site,user=true``` in the admin panel -> Config -> Security -> secfetch_allow.
- Activate plugin in admin panel -> Extensions
- Configure the plugin with the required data:
- Master User Separator is dependent on Dovecot config (see below)
- Master User is dependent on Dovecot config (see below)
- Master User Password is dependent on Dovecot config (see below)
- Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User".
- Check Proxy: Since this plugin partially bypasses authentication, it is important to only allow this access from well-defined hosts. It is highly recommended to activate this option!
- When checking for reverse proxy, it is required to set the IP filter to either an IP address or a subnet.
This concludes the setup of SnappyMail.
### Dovecot
In Dovecot, you need to enable Master User.
Enable ```!include auth-master.conf.ext``` in /etc/dovecot/conf.d/10-auth.conf.
The file /etc/dovecot/conf.d/auth-master.conf.ext should contain:
```
# Authentication for master users. Included from auth.conf.
# By adding master=yes setting inside a passdb you make the passdb a list
# of "master users", who can log in as anyone else.
# <doc/wiki/Authentication.MasterUsers.txt>
# Example master user passdb using passwd-file. You can use any passdb though.
passdb {
driver = passwd-file
master = yes
args = /etc/dovecot/master-users
# Unless you're using PAM, you probably still want the destination user to
# be looked up from passdb that it really exists. pass=yes does that.
pass = yes
}
```
You then need to create a master user in /etc/dovecot/master-users:
```
admin:PASSWORD
```
where the encrypted password ```PASSWORD``` can be created from a cleartext password with ```doveadm pw -s CRYPT```.
It should start with ```{CRYPT}```.
Username and password need to configured in the SnappyMail ProxyAuth plugin (see above).
Additionally, you need to set the master user separator in /etc/dovecot/conf.d/10-auth.conf, e.g., ```auth_master_user_separator = *```.
The separator needs to be configured in the SnappyMail ProxyAuth plugin (see above).
## Test
Once configured correctly, you should be able to access SnappyMail through your reverse proxy at ```https://snappymail.tld/?ProxyAuth```.
If your reverse proxy provides the username in the configured header (e.g., Remote-User), you will automatically be logged in to your account.
If not, you will be redirected to the login page.

View file

@ -0,0 +1,184 @@
<?php
class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Proxy Auth',
AUTHOR = 'Philipp',
URL = 'https://www.mundhenk.org/',
VERSION = '0.1',
RELEASE = '2023-01-14',
REQUIRED = '2.27.0',
CATEGORY = 'Login',
LICENSE = 'MIT',
DESCRIPTION = 'Uses HTTP Remote-User and (Dovecot) master user for login';
public function Init() : void
{
$this->addPartHook('ProxyAuth', 'ServiceProxyAuth');
$this->addHook('login.credentials', 'MapEmailAddress');
}
/* by https://gist.github.com/tott/7684443 */
/**
* Check if a given ip is in a network
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
*/
private function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) == false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
public function MapEmailAddress(string &$sEmail, string &$sLogin, string &$sPassword)
{
$oActions = \RainLoop\Api::Actions();
$oLogger = $oActions->Logger();
$sPrefix = "ProxyAuth";
$sLevel = LOG_DEBUG;
$sMsg = "sEmail= " . $sEmail;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sMasterUser = \trim($this->Config()->getDecrypted('plugin', 'master_user', ''));
$sMasterSeparator = \trim($this->Config()->getDecrypted('plugin', 'master_separator', ''));
/* remove superuser from email for proper UI */
if (static::$login) {
$sEmail = str_replace($sMasterUser, "", $sEmail);
$sEmail = str_replace($sMasterSeparator, "", $sEmail);
}
}
private static bool $login = false;
public function ServiceProxyAuth() : bool
{
$oActions = \RainLoop\Api::Actions();
$oException = null;
$oAccount = null;
$oLogger = $oActions->Logger();
$sLevel = LOG_DEBUG;
$sPrefix = "ProxyAuth";
$sMasterUser = \trim($this->Config()->getDecrypted('plugin', 'master_user', ''));
$sMasterSeparator = \trim($this->Config()->getDecrypted('plugin', 'master_separator', ''));
$sHeaderName = \trim($this->Config()->getDecrypted('plugin', 'header_name', ''));
$sRemoteUser = $this->Manager()->Actions()->Http()->GetHeader($sHeaderName);
$sMsg = "Remote User: " . $sRemoteUser;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sProxyIP = $this->Config()->getDecrypted('plugin', 'proxy_ip', '');
$sMsg = "ProxyIP: " . $sProxyIP;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sProxyCheck = $this->Config()->getDecrypted('plugin', 'proxy_check', '');
$sClientIPs = $this->Manager()->Actions()->Http()->GetClientIP(true);
/* make sure that remote user is only set by authorized proxy to avoid security risks */
if ($sProxyCheck) {
$sProxyRequest = false;
$sMsg = "checking client IPs: " . $sClientIPs;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sClientIPs = explode(", ", $sClientIPs);
if (is_array($sClientIPs)) {
foreach ($sClientIPs as &$sIP) {
$sMsg = "checking client IP: " . $sIP;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
if ($this->ip_in_range($sIP, $sProxyIP)) {
$sProxyRequest = true;
}
}
} else {
$sMsg = "checking client IP: " . $sClientIPs;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
if ($this->ip_in_range($sClientIPs, $sProxyIP)) {
$sProxyRequest = true;
}
}
} else {
$sProxyRequest = true;
}
if ($sProxyRequest) {
/* create master user login from remote user header and settings */
$sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser;
$sPassword = \trim($this->Config()->getDecrypted('plugin', 'master_password', ''));
try
{
static::$login = true;
$oAccount = $oActions->LoginProcess($sEmail, $sPassword);
if ($oAccount instanceof \RainLoop\Model\MainAccount) {
$oActions->SetAuthToken($oAccount);
}
}
catch (\Throwable $oException)
{
$oLogger = $oActions->Logger();
$oLogger && $oLogger->WriteException($oException);
}
\MailSo\Base\Http::Location('./');
return true;
}
\MailSo\Base\Http::Location('./');
return true;
}
protected function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('master_separator')
->SetLabel('Master User separator')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Sets the master user separator (format: <username><separator><master username>)')
->SetDefaultValue('*')
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('master_user')
->SetLabel('Master User')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Username of master user')
->SetDefaultValue('admin')
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('master_password')
->SetLabel('Master Password')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Password for master user')
->SetDefaultValue('adminpassword')
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('header_name')
->SetLabel('Header Name')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Name of header containing username')
->SetDefaultValue('Remote-User')
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('check_proxy')
->SetLabel('Check Proxy')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('Activates check if proxy is connecting')
->SetDefaultValue(true)
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('proxy_ip')
->SetLabel('Proxy IPNet')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('IP or Subnet of proxy, auth header will only be accepted from this address')
->SetDefaultValue('10.1.0.0/24')
->SetEncrypted()
);
}
}