Passed
Pull Request — 4 (#8209)
by Ingo
09:07
created

MemberAuthenticator::recordLoginAttempt()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 9
nop 4
dl 0
loc 40
rs 8.0355
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\MemberAuthenticator;
4
5
use InvalidArgumentException;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Core\Extensible;
8
use SilverStripe\ORM\ValidationResult;
9
use SilverStripe\Security\Authenticator;
10
use SilverStripe\Security\DefaultAdminService;
11
use SilverStripe\Security\LoginAttempt;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\PasswordEncryptor;
14
use SilverStripe\Security\Security;
15
16
/**
17
 * Authenticator for the default "member" method
18
 *
19
 * @author Sam Minnee <[email protected]>
20
 * @author Simon Erkelens <[email protected]>
21
 */
22
class MemberAuthenticator implements Authenticator
23
{
24
    use Extensible;
25
26
    public function supportedServices()
27
    {
28
        // Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
29
        return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
30
            | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
31
    }
32
33
    public function authenticate(array $data, HTTPRequest $request, ValidationResult &$result = null)
34
    {
35
        // Find authenticated member
36
        $member = $this->authenticateMember($data, $result);
37
38
        // Optionally record every login attempt as a {@link LoginAttempt} object
39
        $this->recordLoginAttempt($data, $request, $member, $result->isValid());
40
41
        if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
42
            $request->getSession()->clear('BackURL');
43
        }
44
45
        return $result->isValid() ? $member : null;
46
    }
47
48
    /**
49
     * Attempt to find and authenticate member if possible from the given data
50
     *
51
     * @skipUpgrade
52
     * @param array $data Form submitted data
53
     * @param ValidationResult $result
54
     * @param Member $member This third parameter is used in the CMSAuthenticator(s)
55
     * @return Member Found member, regardless of successful login
56
     */
57
    protected function authenticateMember($data, ValidationResult &$result = null, Member $member = null)
58
    {
59
        $email = !empty($data['Email']) ? $data['Email'] : null;
60
        $result = $result ?: ValidationResult::create();
61
62
        // Check default login (see Security::setDefaultAdmin())
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
63
        $asDefaultAdmin = DefaultAdminService::isDefaultAdmin($email);
64
        if ($asDefaultAdmin) {
65
            // If logging is as default admin, ensure record is setup correctly
66
            $member = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
67
            $member->validateCanLogin($result);
68
            if ($result->isValid()) {
69
                // Check if default admin credentials are correct
70
                if (DefaultAdminService::isDefaultAdminCredentials($email, $data['Password'])) {
71
                    return $member;
72
                } else {
73
                    $result->addError(_t(
74
                        'SilverStripe\\Security\\Member.ERRORWRONGCRED',
75
                        "The provided details don't seem to be correct. Please try again."
76
                    ));
77
                }
78
            }
79
        }
80
81
        // Attempt to identify user by email
82
        if (!$member && $email) {
83
            // Find user by email
84
            $identifierField = Member::config()->get('unique_identifier_field');
85
            /** @var Member $member */
86
            $member = Member::get()
87
                ->filter([$identifierField => $email])
88
                ->first();
89
        }
90
91
        // Validate against member if possible
92
        if ($member && !$asDefaultAdmin) {
93
            $this->checkPassword($member, $data['Password'], $result);
94
        } elseif (!$asDefaultAdmin) {
95
            // spoof a login attempt
96
            $tempMember = Member::create();
97
            $tempMember->{Member::config()->get('unique_identifier_field')} = $email;
98
            $tempMember->validateCanLogin($result);
99
        }
100
101
        // Emit failure to member and form (if available)
102
        if (!$result->isValid()) {
103
            if ($member) {
104
                $member->registerFailedLogin();
105
            }
106
        } elseif ($member) {
107
            $member->registerSuccessfulLogin();
108
        } else {
109
            // A non-existing member occurred. This will make the result "valid" so let's invalidate
110
            $result->addError(_t(
111
                'SilverStripe\\Security\\Member.ERRORWRONGCRED',
112
                "The provided details don't seem to be correct. Please try again."
113
            ));
114
            return null;
115
        }
116
117
        return $member;
118
    }
119
120
    /**
121
     * Check if the passed password matches the stored one (if the member is not locked out).
122
     *
123
     * Note, we don't return early, to prevent differences in timings to give away if a member
124
     * password is invalid.
125
     *
126
     * @param Member $member
127
     * @param string $password
128
     * @param ValidationResult $result
129
     * @return ValidationResult
130
     */
131
    public function checkPassword(Member $member, $password, ValidationResult &$result = null)
132
    {
133
        // Check if allowed to login
134
        $result = $member->validateCanLogin($result);
135
        if (!$result->isValid()) {
136
            return $result;
137
        }
138
139
        // Allow default admin to login as self
140
        if (DefaultAdminService::isDefaultAdminCredentials($member->Email, $password)) {
141
            return $result;
142
        }
143
144
        // Check a password is set on this member
145
        if (empty($member->Password) && $member->exists()) {
146
            $result->addError(_t(__CLASS__ . '.NoPassword', 'There is no password on this member.'));
147
        }
148
149
        $encryptor = PasswordEncryptor::create_for_algorithm($member->PasswordEncryption);
150
        if (!$encryptor->check($member->Password, $password, $member->Salt, $member)) {
151
            $result->addError(_t(
152
                __CLASS__ . '.ERRORWRONGCRED',
153
                'The provided details don\'t seem to be correct. Please try again.'
154
            ));
155
        }
156
157
        return $result;
158
    }
159
160
161
    /**
162
     * Log login attempt
163
     * TODO We could handle this with an extension
164
     *
165
     * @param array $data
166
     * @param HTTPRequest $request
167
     * @param Member $member
168
     * @param boolean $success
169
     */
170
    protected function recordLoginAttempt($data, HTTPRequest $request, $member, $success)
171
    {
172
        if (!Security::config()->get('login_recording')
173
            && !Member::config()->get('lock_out_after_incorrect_logins')
174
        ) {
175
            return;
176
        }
177
178
        // Check email is valid
179
        /** @skipUpgrade */
180
        $email = isset($data['Email']) ? $data['Email'] : null;
181
        if (is_array($email)) {
182
            throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email");
183
        }
184
185
        $attempt = LoginAttempt::create();
186
        if ($success && $member) {
187
            // successful login (member is existing with matching password)
188
            $attempt->MemberID = $member->ID;
189
            $attempt->Status = LoginAttempt::SUCCESS;
190
191
            // Audit logging hook
192
            $member->extend('authenticationSucceeded');
193
        } else {
194
            // Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
195
            $attempt->Status = LoginAttempt::FAILURE;
196
            if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
197
                // Audit logging hook
198
                $attempt->MemberID = $member->ID;
199
                $member->extend('authenticationFailed', $data, $request);
200
            } else {
201
                // Audit logging hook
202
                Member::singleton()
203
                   ->extend('authenticationFailedUnknownUser', $data, $request);
204
            }
205
        }
206
207
        $attempt->Email = $email;
208
        $attempt->IP = $request->getIP();
209
        $attempt->write();
210
    }
211
212
    /**
213
     * @param string $link
214
     * @return LostPasswordHandler
215
     */
216
    public function getLostPasswordHandler($link)
217
    {
218
        return LostPasswordHandler::create($link, $this);
219
    }
220
221
    /**
222
     * @param string $link
223
     * @return ChangePasswordHandler
224
     */
225
    public function getChangePasswordHandler($link)
226
    {
227
        return ChangePasswordHandler::create($link, $this);
228
    }
229
230
    /**
231
     * @param string $link
232
     * @return LoginHandler
233
     */
234
    public function getLoginHandler($link)
235
    {
236
        return LoginHandler::create($link, $this);
237
    }
238
239
    /**
240
     * @param string $link
241
     * @return LogoutHandler
242
     */
243
    public function getLogoutHandler($link)
244
    {
245
        return LogoutHandler::create($link, $this);
246
    }
247
}
248