Skip to content

Commit 16c3339

Browse files
committed
refactor(users): orchestrate authentication providers
1 parent a326a18 commit 16c3339

21 files changed

Lines changed: 863 additions & 147 deletions

mailscanner/checklogin.php

Lines changed: 40 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -56,86 +56,38 @@
5656
$mypassword = $_POST['mypassword'];
5757
}
5858

59-
$_SESSION['user_ldap'] = false;
60-
$_SESSION['user_imap'] = false;
61-
if (defined('USE_LDAP')
62-
&& (USE_LDAP === true)
63-
&& (($result = ldap_authenticate($myusername, $mypassword)) !== null)
64-
) {
65-
$_SESSION['user_ldap'] = true;
66-
$myusername = safe_value($result);
67-
$mypassword = safe_value($mypassword);
68-
} elseif (
69-
defined('USE_IMAP')
70-
&& (USE_IMAP === true)
71-
&& (($result = imap_authenticate($myusername, $mypassword)) !== null)
72-
) {
73-
$_SESSION['user_imap'] = true;
74-
$myusername = safe_value($myusername);
75-
$mypassword = safe_value($mypassword);
76-
} else {
77-
if ('' !== $mypassword) {
78-
$myusername = safe_value($myusername);
79-
$mypassword = safe_value($mypassword);
80-
} else {
81-
header('Location: /login.php?error=emptypassword');
82-
logFailedLogin($myusername);
83-
exit;
84-
}
85-
}
86-
87-
$sql = "SELECT * FROM users WHERE username='$myusername'";
88-
$result = dbquery($sql);
89-
90-
// mysql_num_row is counting table row
91-
$usercount = $result->num_rows;
92-
if (0 === $usercount) {
93-
// no user found, redirect to login
94-
dbclose();
59+
try {
60+
$authenticated = \MailWatch\ApplicationFactory::userAuthentication()->authenticate(
61+
(string)$myusername,
62+
(string)$mypassword,
63+
time(),
64+
);
65+
} catch (\MailWatch\Users\Application\EmptyPassword) {
66+
header('Location: /login.php?error=emptypassword');
67+
logFailedLogin($myusername);
68+
exit;
69+
} catch (\MailWatch\Users\Application\InvalidCredentials) {
9570
header('Location: /login.php?error=baduser');
9671
logFailedLogin($myusername);
9772
exit;
73+
} catch (\MailWatch\Users\Application\AuthenticationProviderUnavailable $exception) {
74+
exit($exception->getMessage());
9875
}
9976

100-
if (
101-
(false === $_SESSION['user_ldap'])
102-
&& (false === $_SESSION['user_imap'])
103-
) {
104-
$passwordInDb = Database::mysqli_result($result, 0, 'password');
105-
if (!is_string($passwordInDb)) {
106-
header('Location: /login.php?error=baduser');
107-
logFailedLogin($myusername);
108-
exit;
109-
}
110-
111-
if (!password_verify($mypassword, $passwordInDb)) {
112-
if (!hash_equals(md5($mypassword), $passwordInDb)) {
113-
header('Location: /login.php?error=baduser');
114-
logFailedLogin($myusername);
115-
exit;
116-
}
117-
118-
$newPasswordHash = password_hash($mypassword, PASSWORD_DEFAULT);
119-
updateUserPasswordHash($myusername, $newPasswordHash);
120-
} else {
121-
// upgraded password is valid, continue as normal
122-
if (password_needs_rehash($passwordInDb, PASSWORD_DEFAULT)) {
123-
$newPasswordHash = password_hash($mypassword, PASSWORD_DEFAULT);
124-
updateUserPasswordHash($myusername, $newPasswordHash);
125-
}
126-
}
77+
$account = $authenticated->account;
78+
$myusername = $account->username;
79+
$fullname = $account->fullName;
80+
$usertype = $account->role;
81+
$_SESSION['user_ldap'] = \MailWatch\Users\Domain\AuthenticationSource::Ldap === $authenticated->source;
82+
$_SESSION['user_imap'] = \MailWatch\Users\Domain\AuthenticationSource::Imap === $authenticated->source;
83+
if ($authenticated->passwordHashUpgraded) {
84+
audit_log(__('auditlogupdateuser03', true) . ' ' . $myusername);
12785
}
12886

129-
$fullname = Database::mysqli_result($result, 0, 'fullname');
130-
$usertype = Database::mysqli_result($result, 0, 'type');
131-
132-
$sql_userfilter = "SELECT filter FROM user_filters WHERE username='$myusername' AND active='Y'";
133-
$result_userfilter = dbquery($sql_userfilter);
134-
135-
$filter[] = $myusername;
136-
while ($row = $result_userfilter->fetch_array()) {
137-
$filter[] = $row['filter'];
138-
}
87+
$filter = array_map(
88+
static fn(string $value): string => safe_value($value),
89+
[$myusername, ...$account->activeFilters],
90+
);
13991

14092
$global_filter = address_filter_sql($filter, $usertype);
14193

@@ -173,31 +125,22 @@
173125
break;
174126
}
175127

176-
// If result matched $myusername and $mypassword, table row must be 1 row
177-
if (1 === $usercount) {
178-
session_regenerate_id(true);
179-
// Register $myusername, $mypassword and redirect to file "login_success.php"
180-
$_SESSION['myusername'] = $myusername;
181-
$_SESSION['fullname'] = $fullname;
182-
$_SESSION['user_type'] = ($usertype ?? '');
183-
$_SESSION['domain'] = ($domainname ?? '');
184-
$_SESSION['global_filter'] = '(' . $global_filter . ')';
185-
$_SESSION['global_list'] = ($global_list ?? '');
186-
$_SESSION['global_array'] = $filter;
187-
$_SESSION['token'] = generateToken();
188-
$_SESSION['formtoken'] = generateToken();
189-
// Initialize login expiry in users table for newly logged in user
190-
updateLoginExpiry($myusername);
191-
$redirect_url = 'index.php';
192-
if (isset($_SESSION['REQUEST_URI'])) {
193-
$redirect_url = $_SESSION['REQUEST_URI'];
194-
unset($_SESSION['REQUEST_URI']);
195-
}
196-
header('Location: ' . str_replace('&', '&', sanitizeInput($redirect_url)));
197-
} else {
198-
header('Location: /login.php?error=baduser');
199-
logFailedLogin($myusername);
128+
session_regenerate_id(true);
129+
$_SESSION['myusername'] = $myusername;
130+
$_SESSION['fullname'] = $fullname;
131+
$_SESSION['user_type'] = $usertype;
132+
$_SESSION['domain'] = ($domainname ?? '');
133+
$_SESSION['global_filter'] = '(' . $global_filter . ')';
134+
$_SESSION['global_list'] = ($global_list ?? '');
135+
$_SESSION['global_array'] = $filter;
136+
$_SESSION['token'] = generateToken();
137+
$_SESSION['formtoken'] = generateToken();
138+
$redirect_url = 'index.php';
139+
if (isset($_SESSION['REQUEST_URI'])) {
140+
$redirect_url = $_SESSION['REQUEST_URI'];
141+
unset($_SESSION['REQUEST_URI']);
200142
}
143+
header('Location: ' . str_replace('&', '&', sanitizeInput($redirect_url)));
201144

202145
// close any DB connections
203146
dbclose();

mailscanner/functions.php

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2918,14 +2918,19 @@ function ldap_build_uri($host, $port)
29182918
* @param string $username
29192919
* @param string $password
29202920
*
2921-
* @return string|null
2921+
* @return \MailWatch\Users\Domain\ExternalIdentity|null
29222922
*/
29232923
function ldap_authenticate($username, $password)
29242924
{
29252925
$username = ldap_escape(strtolower($username), '', LDAP_ESCAPE_DN);
29262926
if ('' !== $username && '' !== $password) {
29272927
$ldap_uri = ldap_build_uri(LDAP_HOST, LDAP_PORT);
2928-
$ds = ldap_connect($ldap_uri) or exit(__('ldpaauth103') . ' ' . $ldap_uri);
2928+
$ds = ldap_connect($ldap_uri);
2929+
if (false === $ds) {
2930+
throw new \MailWatch\Users\Application\AuthenticationProviderUnavailable(
2931+
__('ldpaauth103') . ' ' . $ldap_uri
2932+
);
2933+
}
29292934

29302935
$ldap_protocol_version = 3;
29312936
if (defined('LDAP_PROTOCOL_VERSION')) {
@@ -2940,7 +2945,7 @@ function ldap_authenticate($username, $password)
29402945

29412946
$bindResult = @ldap_bind($ds, LDAP_USER, LDAP_PASS);
29422947
if (false === $bindResult) {
2943-
exit(ldap_print_error($ds));
2948+
throw new \MailWatch\Users\Application\AuthenticationProviderUnavailable(ldap_print_error($ds));
29442949
}
29452950

29462951
// search for $user in LDAP directory
@@ -2949,7 +2954,7 @@ function ldap_authenticate($username, $password)
29492954
if (false === $ldap_search_results) {
29502955
@trigger_error(__('ldapnoresult03') . ' "' . $username . '"');
29512956

2952-
exit(__('ldpaauth203'));
2957+
throw new \MailWatch\Users\Application\AuthenticationProviderUnavailable(__('ldpaauth203'));
29532958
}
29542959
if (1 > ldap_count_entries($ds, $ldap_search_results)) {
29552960
@trigger_error(__('ldapresultnodata03') . ' "' . $username . '"');
@@ -2963,7 +2968,10 @@ function ldap_authenticate($username, $password)
29632968
}
29642969

29652970
if ($ldap_search_results) {
2966-
$result = ldap_get_entries($ds, $ldap_search_results) or exit(__('ldpaauth303'));
2971+
$result = ldap_get_entries($ds, $ldap_search_results);
2972+
if (false === $result) {
2973+
throw new \MailWatch\Users\Application\AuthenticationProviderUnavailable(__('ldpaauth303'));
2974+
}
29672975
ldap_free_result($ldap_search_results);
29682976
if (isset($result[0])) {
29692977
if (in_array('group', array_values($result[0]['objectclass']), true)) {
@@ -3016,25 +3024,19 @@ function ldap_authenticate($username, $password)
30163024
return null;
30173025
}
30183026

3019-
$sql = sprintf('SELECT username FROM users WHERE username = %s', quote_smart($email));
3020-
$sth = dbquery($sql);
3021-
if (0 === $sth->num_rows) {
3022-
$sql = sprintf(
3023-
"REPLACE INTO users (username, fullname, type, password) VALUES (%s, %s,'U',NULL)",
3024-
quote_smart($email),
3025-
quote_smart($result[0]['cn'][0])
3026-
);
3027-
dbquery($sql);
3028-
}
3029-
3030-
return $email;
3027+
return new \MailWatch\Users\Domain\ExternalIdentity(
3028+
(string)$email,
3029+
(string)($result[0]['cn'][0] ?? $email),
3030+
\MailWatch\Users\Domain\AuthenticationSource::Ldap,
3031+
true,
3032+
);
30313033
}
30323034

30333035
if (49 === ldap_errno($ds)) {
30343036
// LDAP_INVALID_CREDENTIALS
30353037
return null;
30363038
}
3037-
exit(ldap_print_error($ds));
3039+
throw new \MailWatch\Users\Application\AuthenticationProviderUnavailable(ldap_print_error($ds));
30383040
}
30393041
}
30403042
}
@@ -3141,7 +3143,7 @@ function ldap_get_conf_truefalse($entry)
31413143
* @param string $username
31423144
* @param string $password
31433145
*
3144-
* @return string|null
3146+
* @return \MailWatch\Users\Domain\ExternalIdentity|null
31453147
*/
31463148
function imap_authenticate($username, $password)
31473149
{
@@ -3177,20 +3179,14 @@ function imap_authenticate($username, $password)
31773179
return null;
31783180
}
31793181

3180-
if (defined('IMAP_AUTOCREATE_VALID_USER') && IMAP_AUTOCREATE_VALID_USER === true) {
3181-
$sql = sprintf('SELECT username FROM users WHERE username = %s', quote_smart($username));
3182-
$sth = dbquery($sql);
3183-
if (0 === $sth->num_rows) {
3184-
$sql = sprintf(
3185-
"REPLACE INTO users (username, fullname, type, password) VALUES (%s, %s,'U',NULL)",
3186-
quote_smart($username),
3187-
quote_smart($username)
3188-
);
3189-
dbquery($sql);
3190-
}
3191-
}
3182+
imap_close($mbox);
31923183

3193-
return $username;
3184+
return new \MailWatch\Users\Domain\ExternalIdentity(
3185+
$username,
3186+
$username,
3187+
\MailWatch\Users\Domain\AuthenticationSource::Imap,
3188+
defined('IMAP_AUTOCREATE_VALID_USER') && IMAP_AUTOCREATE_VALID_USER === true,
3189+
);
31943190
}
31953191

31963192
return null;
@@ -4070,23 +4066,6 @@ function xmlrpc_wrapper($host, $msg)
40704066
return $client->send($msg, 0, $method);
40714067
}
40724068

4073-
function updateUserPasswordHash($user, $hash)
4074-
{
4075-
$sqlCheckLenght = "SELECT CHARACTER_MAXIMUM_LENGTH AS passwordfieldlength FROM information_schema.columns WHERE column_name = 'password' AND table_name = 'users'";
4076-
$passwordFiledLengthResult = dbquery($sqlCheckLenght);
4077-
$passwordFiledLength = (int)Database::mysqli_result($passwordFiledLengthResult, 0, 'passwordfieldlength');
4078-
4079-
if ($passwordFiledLength < 255) {
4080-
$sqlUpdateFieldLength = 'ALTER TABLE `users` CHANGE `password` `password` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL';
4081-
dbquery($sqlUpdateFieldLength);
4082-
audit_log(sprintf(__('auditlogquareleased03', true) . ' ', $passwordFiledLength));
4083-
}
4084-
4085-
$sqlUpdateHash = "UPDATE `users` SET `password` = '$hash' WHERE `users`.`username` = '$user'";
4086-
dbquery($sqlUpdateHash);
4087-
audit_log(__('auditlogupdateuser03', true) . ' ' . $user);
4088-
}
4089-
40904069
/**
40914070
* @param string $username username that should be checked if it exists
40924071
*

src/MailWatch/ApplicationFactory.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@
3939
use MailWatch\SpamSettings\Application\GetSpamSettingsSnapshot;
4040
use MailWatch\SpamSettings\Http\SpamSettingsController;
4141
use MailWatch\SpamSettings\Infrastructure\Database\DbalSpamSettingsGateway;
42+
use MailWatch\Users\Application\AuthenticateUser;
4243
use MailWatch\Users\Application\ManageLocalAccounts;
4344
use MailWatch\Users\Application\ManageOwnProfile;
4445
use MailWatch\Users\Application\ManageSavedFilters;
46+
use MailWatch\Users\Infrastructure\Authentication\ImapCredentialVerifier;
47+
use MailWatch\Users\Infrastructure\Authentication\LdapCredentialVerifier;
4548
use MailWatch\Users\Infrastructure\Database\DbalAccountAdministrationGateway;
49+
use MailWatch\Users\Infrastructure\Database\DbalLoginAccountGateway;
4650
use MailWatch\Users\Infrastructure\Database\DbalSavedFilterAdministrationGateway;
4751
use MailWatch\Users\Infrastructure\Database\DbalUserProfileGateway;
4852

@@ -136,6 +140,26 @@ public static function localAccountAdministration(): ManageLocalAccounts
136140
);
137141
}
138142

143+
public static function userAuthentication(): AuthenticateUser
144+
{
145+
$providers = [];
146+
if (\defined('USE_LDAP') && true === USE_LDAP) {
147+
$providers[] = new LdapCredentialVerifier();
148+
}
149+
if (\defined('USE_IMAP') && true === USE_IMAP) {
150+
$providers[] = new ImapCredentialVerifier();
151+
}
152+
$passwords = new NativePasswordHasher();
153+
154+
return new AuthenticateUser(
155+
new DbalLoginAccountGateway(self::databaseConnection()),
156+
$passwords,
157+
$passwords,
158+
$providers,
159+
\defined('SESSION_TIMEOUT') ? (int)SESSION_TIMEOUT : null,
160+
);
161+
}
162+
139163
/**
140164
* Static because rendering depends on nothing the factory is constructed
141165
* with: the page scripts need a renderer without a valid API configuration.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MailWatch\Shared\Application\Port;
6+
7+
interface PasswordVerifier
8+
{
9+
public function verify(string $password, string $hash): bool;
10+
11+
public function needsRehash(string $hash): bool;
12+
}

src/MailWatch/Shared/Infrastructure/Security/NativePasswordHasher.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@
55
namespace MailWatch\Shared\Infrastructure\Security;
66

77
use MailWatch\Shared\Application\Port\PasswordHasher;
8+
use MailWatch\Shared\Application\Port\PasswordVerifier;
89

9-
final readonly class NativePasswordHasher implements PasswordHasher
10+
final readonly class NativePasswordHasher implements PasswordHasher, PasswordVerifier
1011
{
1112
public function hash(string $password): string
1213
{
1314
return password_hash($password, PASSWORD_DEFAULT);
1415
}
16+
17+
public function verify(string $password, string $hash): bool
18+
{
19+
return password_verify($password, $hash);
20+
}
21+
22+
public function needsRehash(string $hash): bool
23+
{
24+
return password_needs_rehash($hash, PASSWORD_DEFAULT);
25+
}
1526
}

0 commit comments

Comments
 (0)