LDAPAuthenticator   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 74
dl 0
loc 218
rs 10
c 0
b 0
f 0
wmc 30

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getChangePasswordHandler() 0 3 1
A get_name() 0 3 1
A get_login_form() 0 3 1
A getLoginHandler() 0 3 1
A getLostPasswordHandler() 0 3 1
C authenticate() 0 82 15
A checkPassword() 0 18 4
A supportedServices() 0 8 2
A fallbackAuthenticate() 0 17 4
1
<?php
2
3
namespace SilverStripe\LDAP\Authenticators;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Email\Email;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\LDAP\Forms\LDAPLoginForm;
11
use SilverStripe\LDAP\Services\LDAPService;
12
use SilverStripe\ORM\ValidationResult;
13
use SilverStripe\Security\Authenticator;
14
use SilverStripe\Security\Member;
15
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
16
use Zend\Authentication\Result;
17
18
/**
19
 * Class LDAPAuthenticator
20
 *
21
 * Authenticate a user against LDAP, without the single sign-on component.
22
 */
23
class LDAPAuthenticator extends MemberAuthenticator
24
{
25
    /**
26
     * @var string
27
     */
28
    private $name = 'LDAP';
29
30
    /**
31
     * Set to 'yes' to indicate if this module should look up usernames in LDAP by matching the email addresses.
32
     *
33
     * CAVEAT #1: only set to 'yes' for systems that enforce email uniqueness.
34
     * Otherwise only the first LDAP user with matching email will be accessible.
35
     *
36
     * CAVEAT #2: this is untested for systems that use LDAP with principal style usernames (i.e. [email protected]).
37
     * The system will misunderstand emails for usernames with uncertain outcome.
38
     *
39
     * @var string 'no' or 'yes'
40
     */
41
    private static $allow_email_login = 'no';
42
43
    /**
44
     * Set to 'yes' to fallback login attempts to {@link $fallback_authenticator}.
45
     * This will occur if LDAP fails to authenticate the user.
46
     *
47
     * @var string 'no' or 'yes'
48
     */
49
    private static $fallback_authenticator = 'no';
50
51
    /**
52
     * The class of {@link Authenticator} to use as the fallback authenticator.
53
     *
54
     * @var string
55
     */
56
    private static $fallback_authenticator_class = MemberAuthenticator::class;
57
58
    /**
59
     * @return string
60
     */
61
    public static function get_name()
62
    {
63
        return Config::inst()->get(self::class, 'name');
64
    }
65
66
    /**
67
     * @param Controller $controller
68
     * @return LDAPLoginForm
69
     */
70
    public static function get_login_form(Controller $controller)
71
    {
72
        return LDAPLoginForm::create($controller, LDAPAuthenticator::class, 'LoginForm');
73
    }
74
75
    /**
76
     * Performs the login, but will also create and sync the Member record on-the-fly, if not found.
77
     *
78
     * @param array $data
79
     * @param HTTPRequest $request
80
     * @param ValidationResult|null $result
81
     * @return null|Member
82
     */
83
    public function authenticate(array $data, HTTPRequest $request, ValidationResult &$result = null)
84
    {
85
        $result = $result ?: ValidationResult::create();
86
        /** @var LDAPService $service */
87
        $service = Injector::inst()->get(LDAPService::class);
88
        $login = trim($data['Login']);
89
        if (Email::is_valid_address($login)) {
90
            if (Config::inst()->get(self::class, 'allow_email_login') != 'yes') {
91
                $result->addError(
92
                    _t(
93
                        __CLASS__ . '.PLEASEUSEUSERNAME',
94
                        'Please enter your username instead of your email to log in.'
95
                    )
96
                );
97
                return null;
98
            }
99
            $username = $service->getUsernameByEmail($login);
100
101
            // No user found with this email.
102
            if (!$username) {
103
                if (Config::inst()->get(self::class, 'fallback_authenticator') === 'yes') {
104
                    if ($fallbackMember = $this->fallbackAuthenticate($data, $request)) {
105
                        {
106
                            return $fallbackMember;
107
                        }
108
                    }
109
                }
110
111
                $result->addError(_t(__CLASS__ . '.INVALIDCREDENTIALS', 'Invalid credentials'));
112
                return null;
113
            }
114
        } else {
115
            $username = $login;
116
        }
117
118
        $serviceAuthenticationResult = $service->authenticate($username, $data['Password']);
119
        $success = $serviceAuthenticationResult['success'] === true;
120
121
        if (!$success) {
122
            /*
123
             * Try the fallback method if admin or it failed for anything other than invalid credentials
124
             * This is to avoid having an unhandled exception error thrown by PasswordEncryptor::create_for_algorithm()
125
             */
126
            if (Config::inst()->get(self::class, 'fallback_authenticator') === 'yes') {
127
                if (!in_array($serviceAuthenticationResult['code'], [Result::FAILURE_CREDENTIAL_INVALID])
128
                    || $username === 'admin'
129
                ) {
130
                    if ($fallbackMember = $this->fallbackAuthenticate($data, $request)) {
131
                        return $fallbackMember;
132
                    }
133
                }
134
            }
135
136
            $result->addError($serviceAuthenticationResult['message']);
137
138
            return null;
139
        }
140
        $data = $service->getUserByUsername($serviceAuthenticationResult['identity']);
141
        if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
142
            $result->addError(
143
                _t(
144
                    __CLASS__ . '.PROBLEMFINDINGDATA',
145
                    'There was a problem retrieving your user data'
146
                )
147
            );
148
            return null;
149
        }
150
151
        // LDAPMemberExtension::memberLoggedIn() will update any other AD attributes mapped to Member fields
152
        $member = Member::get()->filter('GUID', $data['objectguid'])->limit(1)->first();
153
        if (!($member && $member->exists())) {
154
            $member = new Member();
155
            $member->GUID = $data['objectguid'];
156
        }
157
158
        // Update the users from LDAP so we are sure that the email is correct.
159
        // This will also write the Member record.
160
        $service->updateMemberFromLDAP($member, $data);
161
162
        $request->getSession()->clear('BackURL');
163
164
        return $member;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $member also could return the type SilverStripe\ORM\DataObject which includes types incompatible with the return type mandated by SilverStripe\Security\Au...ticator::authenticate() of SilverStripe\Security\Member. Consider adding a type-check to rule them out.
Loading history...
165
    }
166
167
    /**
168
     * Try to authenticate using the fallback authenticator.
169
     *
170
     * @param array $data
171
     * @param HTTPRequest $request
172
     * @return null|Member
173
     */
174
    protected function fallbackAuthenticate($data, HTTPRequest $request)
175
    {
176
        // Set Email from Login
177
        if (array_key_exists('Login', $data) && !array_key_exists('Email', $data)) {
178
            $data['Email'] = $data['Login'];
179
        }
180
        $authenticatorClass = Config::inst()->get(self::class, 'fallback_authenticator_class');
181
        if ($authenticator = Injector::inst()->get($authenticatorClass)) {
182
            $result = call_user_func(
183
                [
184
                    $authenticator,
185
                    'authenticate'
186
                ],
187
                $data,
188
                $request
189
            );
190
            return $result;
191
        }
192
    }
193
194
    public function getLoginHandler($link)
195
    {
196
        return LDAPLoginHandler::create($link, $this);
197
    }
198
199
    public function supportedServices()
200
    {
201
        $result = Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHECK_PASSWORD;
202
203
        if ((bool)LDAPService::config()->get('allow_password_change')) {
204
            $result |=  Authenticator::RESET_PASSWORD | Authenticator::CHANGE_PASSWORD;
205
        }
206
        return $result;
207
    }
208
209
    public function getLostPasswordHandler($link)
210
    {
211
        return LDAPLostPasswordHandler::create($link, $this);
212
    }
213
214
    /**
215
     * @param string $link
216
     * @return LDAPChangePasswordHandler
217
     */
218
    public function getChangePasswordHandler($link)
219
    {
220
        return LDAPChangePasswordHandler::create($link, $this);
221
    }
222
223
    public function checkPassword(Member $member, $password, ValidationResult &$result = null)
224
    {
225
        $result = $result ?: ValidationResult::create();
226
227
        /** @var LDAPService $service */
228
        $service = Injector::inst()->get(LDAPService::class);
229
230
        // Support email or username
231
        $handle = Config::inst()->get(self::class, 'allow_email_login') === 'yes' ? 'Email' : 'Username';
232
233
        /** @var array $ldapResult */
234
        $ldapResult = $service->authenticate($member->{$handle}, $password);
235
236
        if (empty($ldapResult['success'])) {
237
            $result->addError($ldapResult['message']);
238
        }
239
240
        return $result;
241
    }
242
}
243