mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Merge pull request #1390 from PhilippMundhenk/master
implemented ProxyAuth
This commit is contained in:
commit
e4f3df3bb9
20
plugins/proxy-auth/LICENSE
Normal file
20
plugins/proxy-auth/LICENSE
Normal 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.
|
66
plugins/proxy-auth/README.md
Normal file
66
plugins/proxy-auth/README.md
Normal 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.
|
184
plugins/proxy-auth/index.php
Normal file
184
plugins/proxy-auth/index.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue