Completed
Pull Request — master (#7026)
by Damian
08:24
created

MemberAuthenticator::authenticateMember()   C

Complexity

Conditions 13
Paths 196

Size

Total Lines 57
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 57
c 0
b 0
f 0
cc 13
eloc 32
nc 196
nop 3
rs 5.9461

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Security\MemberAuthenticator;
4
5
use InvalidArgumentException;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\Session;
8
use SilverStripe\Core\Extensible;
9
use SilverStripe\ORM\ValidationResult;
10
use SilverStripe\Security\Authenticator;
11
use SilverStripe\Security\LoginAttempt;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\PasswordEncryptor;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\DefaultAdminService;
16
17
/**
18
 * Authenticator for the default "member" method
19
 *
20
 * @author Sam Minnee <[email protected]>
21
 * @author Simon Erkelens <[email protected]>
22
 */
23
class MemberAuthenticator implements Authenticator
24
{
25
    use Extensible;
26
27
    public function supportedServices()
28
    {
29
        // Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
30
        return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
31
            | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
32
    }
33
34
    /**
35
     * @param array $data
36
     * @param null|ValidationResult $result
37
     * @return null|Member
38
     */
39
    public function authenticate($data, ValidationResult &$result = null)
40
    {
41
        // Find authenticated member
42
        $member = $this->authenticateMember($data, $result);
43
44
        // Optionally record every login attempt as a {@link LoginAttempt} object
45
        $this->recordLoginAttempt($data, $member, $result->isValid());
0 ignored issues
show
Bug introduced by
It seems like $result is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
Bug introduced by
It seems like $member defined by $this->authenticateMember($data, $result) on line 42 can be null; however, SilverStripe\Security\Me...r::recordLoginAttempt() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
46
47
        if ($member) {
48
            Session::clear('BackURL');
49
        }
50
51
        return $result->isValid() ? $member : null;
52
    }
53
54
    /**
55
     * Attempt to find and authenticate member if possible from the given data
56
     *
57
     * @param array $data Form submitted data
58
     * @param ValidationResult $result
59
     * @param Member $member This third parameter is used in the CMSAuthenticator(s)
60
     * @return Member Found member, regardless of successful login
61
     */
62
    protected function authenticateMember($data, ValidationResult &$result = null, Member $member = null)
63
    {
64
        $email = !empty($data['Email']) ? $data['Email'] : null;
65
        $result = $result ?: ValidationResult::create();
66
67
        // 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...
68
        $asDefaultAdmin = DefaultAdminService::isDefaultAdmin($email);
69
        if ($asDefaultAdmin) {
70
            // If logging is as default admin, ensure record is setup correctly
71
            $member = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
72
            $member->validateCanLogin($result);
73
            if ($result->isValid()) {
74
                // Check if default admin credentials are correct
75
                if (DefaultAdminService::isDefaultAdminCredentials($email, $data['Password'])) {
76
                    return $member;
77
                } else {
78
                    $result->addError(_t(
79
                        'SilverStripe\\Security\\Member.ERRORWRONGCRED',
80
                        "The provided details don't seem to be correct. Please try again."
81
                    ));
82
                }
83
            }
84
        }
85
86
        // Attempt to identify user by email
87
        if (!$member && $email) {
88
            // Find user by email
89
            $identifierField = Member::config()->get('unique_identifier_field');
90
            /** @var Member $member */
91
            $member = Member::get()
92
                ->filter([$identifierField => $email])
93
                ->first();
94
        }
95
96
        // Validate against member if possible
97
        if ($member && !$asDefaultAdmin) {
98
            $this->checkPassword($member, $data['Password'], $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 Member $member
167
     * @param boolean $success
168
     */
169
    protected function recordLoginAttempt($data, $member, $success)
170
    {
171
        if (!Security::config()->get('login_recording')) {
172
            return;
173
        }
174
175
        // Check email is valid
176
        /** @skipUpgrade */
177
        $email = isset($data['Email']) ? $data['Email'] : null;
178
        if (is_array($email)) {
179
            throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email");
180
        }
181
182
        $attempt = LoginAttempt::create();
183
        if ($success && $member) {
184
            // successful login (member is existing with matching password)
185
            $attempt->MemberID = $member->ID;
186
            $attempt->Status = 'Success';
187
188
            // Audit logging hook
189
            $member->extend('authenticationSucceeded');
190
        } else {
191
            // Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
192
            $attempt->Status = 'Failure';
193
            if ($member) {
194
                // Audit logging hook
195
                $attempt->MemberID = $member->ID;
196
                $member->extend('authenticationFailed');
197
            } else {
198
                // Audit logging hook
199
                Member::singleton()->extend('authenticationFailedUnknownUser', $data);
200
            }
201
        }
202
203
        $attempt->Email = $email;
204
        $attempt->IP = Controller::curr()->getRequest()->getIP();
205
        $attempt->write();
206
    }
207
208
    /**
209
     * @param string $link
210
     * @return LostPasswordHandler
211
     */
212
    public function getLostPasswordHandler($link)
213
    {
214
        return LostPasswordHandler::create($link, $this);
215
    }
216
217
    /**
218
     * @param string $link
219
     * @return ChangePasswordHandler
220
     */
221
    public function getChangePasswordHandler($link)
222
    {
223
        return ChangePasswordHandler::create($link, $this);
224
    }
225
226
    /**
227
     * @param string $link
228
     * @return LoginHandler
229
     */
230
    public function getLoginHandler($link)
231
    {
232
        return LoginHandler::create($link, $this);
233
    }
234
235
    /**
236
     * @param string $link
237
     * @return LogoutHandler
238
     */
239
    public function getLogoutHandler($link)
240
    {
241
        return LogoutHandler::create($link, $this);
242
    }
243
}
244