Completed
Pull Request — master (#5741)
by Damian
12:40
created

MemberAuthenticator::authenticate()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 9
c 1
b 0
f 0
nc 8
nop 2
dl 0
loc 22
rs 8.6737
1
<?php
2
3
namespace SilverStripe\Security;
4
5
6
use SilverStripe\ORM\ValidationResult;
7
use InvalidArgumentException;
8
use Controller;
9
use Form;
10
use Session;
11
12
/**
13
 * Authenticator for the default "member" method
14
 *
15
 * @author Markus Lanthaler <[email protected]>
16
 * @package framework
17
 * @subpackage security
18
 */
19
class MemberAuthenticator extends Authenticator {
20
21
	/**
22
	 * Contains encryption algorithm identifiers.
23
	 * If set, will migrate to new precision-safe password hashing
24
	 * upon login. See http://open.silverstripe.org/ticket/3004
25
	 *
26
	 * @var array
27
	 */
28
	private static $migrate_legacy_hashes = array(
29
		'md5' => 'md5_v2.4',
30
		'sha1' => 'sha1_v2.4'
31
	);
32
33
	/**
34
	 * Attempt to find and authenticate member if possible from the given data
35
	 *
36
	 * @param array $data
37
	 * @param Form $form
38
	 * @param bool &$success Success flag
39
	 * @return Member Found member, regardless of successful login
40
	 */
41
	protected static function authenticate_member($data, $form, &$success) {
42
		// Default success to false
43
		$success = false;
44
45
		// Attempt to identify by temporary ID
46
		$member = null;
47
		$email = null;
48
		if(!empty($data['tempid'])) {
49
			// Find user by tempid, in case they are re-validating an existing session
50
			$member = Member::member_from_tempid($data['tempid']);
51
			if($member) $email = $member->Email;
52
		}
53
54
		// Otherwise, get email from posted value instead
55
		if(!$member && !empty($data['Email'])) {
56
			$email = $data['Email'];
57
		}
58
59
		// 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...
60
		$asDefaultAdmin = $email === Security::default_admin_username();
61
		if($asDefaultAdmin) {
62
			// If logging is as default admin, ensure record is setup correctly
63
			$member = Member::default_admin();
64
			$success = !$member->isLockedOut() && Security::check_default_admin($email, $data['Password']);
65
			//protect against failed login
66
			if($success) {
67
				return $member;
68
			}
69
		}
70
71
		// Attempt to identify user by email
72
		if(!$member && $email) {
73
			// Find user by email
74
			$member = Member::get()
75
				->filter(Member::config()->unique_identifier_field, $email)
76
				->first();
77
		}
78
79
		// Validate against member if possible
80
		if($member && !$asDefaultAdmin) {
81
			$result = $member->checkPassword($data['Password']);
82
			$success = $result->valid();
83
		} else {
84
			$result = new ValidationResult(false, _t('Member.ERRORWRONGCRED'));
85
		}
86
87
		// Emit failure to member and form (if available)
88
		if(!$success) {
89
			if($member) $member->registerFailedLogin();
90
			if($form) $form->sessionMessage($result->message(), 'bad');
91
		} else {
92
			if($member) $member->registerSuccessfulLogin();
93
		}
94
95
		return $member;
96
	}
97
98
	/**
99
	 * Log login attempt
100
	 * TODO We could handle this with an extension
101
	 *
102
	 * @param array $data
103
	 * @param Member $member
104
	 * @param bool $success
105
	 */
106
	protected static function record_login_attempt($data, $member, $success) {
107
		if(!Security::config()->login_recording) return;
108
109
		// Check email is valid
110
		$email = isset($data['Email']) ? $data['Email'] : null;
111
		if(is_array($email)) {
112
			throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email");
113
		}
114
115
		$attempt = new LoginAttempt();
116
		if($success) {
117
			// successful login (member is existing with matching password)
118
			$attempt->MemberID = $member->ID;
119
			$attempt->Status = 'Success';
120
121
			// Audit logging hook
122
			$member->extend('authenticated');
123
124
		} else {
125
			// Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
126
			$attempt->Status = 'Failure';
127
			if($member) {
128
				// Audit logging hook
129
				$attempt->MemberID = $member->ID;
130
				$member->extend('authenticationFailed');
131
132
			} else {
133
				// Audit logging hook
134
				Member::singleton()->extend('authenticationFailedUnknownUser', $data);
135
			}
136
		}
137
138
		$attempt->Email = $email;
139
		$attempt->IP = Controller::curr()->getRequest()->getIP();
140
		$attempt->write();
141
	}
142
143
	/**
144
	 * Method to authenticate an user
145
	 *
146
	 * @param array $data Raw data to authenticate the user
147
	 * @param Form $form Optional: If passed, better error messages can be
148
	 *                             produced by using
149
	 *                             {@link Form::sessionMessage()}
150
	 * @return bool|Member Returns FALSE if authentication fails, otherwise
151
	 *                     the member object
152
	 * @see Security::setDefaultAdmin()
153
	 */
154
	public static function authenticate($data, Form $form = null) {
155
		// Find authenticated member
156
		$member = static::authenticate_member($data, $form, $success);
0 ignored issues
show
Bug introduced by
It seems like $form defined by parameter $form on line 154 can be null; however, SilverStripe\Security\Me...::authenticate_member() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
157
158
		// Optionally record every login attempt as a {@link LoginAttempt} object
159
		static::record_login_attempt($data, $member, $success);
0 ignored issues
show
Bug introduced by
It seems like $member defined by static::authenticate_mem...$data, $form, $success) on line 156 can be null; however, SilverStripe\Security\Me...:record_login_attempt() 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...
160
161
		// Legacy migration to precision-safe password hashes.
162
		// A login-event with cleartext passwords is the only time
163
		// when we can rehash passwords to a different hashing algorithm,
164
		// bulk-migration doesn't work due to the nature of hashing.
165
		// See PasswordEncryptor_LegacyPHPHash class.
166
		if($success && $member && isset(self::$migrate_legacy_hashes[$member->PasswordEncryption])) {
167
			$member->Password = $data['Password'];
168
			$member->PasswordEncryption = self::$migrate_legacy_hashes[$member->PasswordEncryption];
169
			$member->write();
170
		}
171
172
		if($success) Session::clear('BackURL');
173
174
		return $success ? $member : null;
175
	}
176
177
178
	/**
179
	 * Method that creates the login form for this authentication method
180
	 *
181
	 * @param Controller $controller The parent controller, necessary to create the
182
	 *                   appropriate form action tag
183
	 * @return Form Returns the login form to use with this authentication
184
	 *              method
185
	 */
186
	public static function get_login_form(Controller $controller) {
187
		/** @skipUpgrade */
188
		return MemberLoginForm::create($controller, "LoginForm");
189
	}
190
191
	public static function get_cms_login_form(\Controller $controller) {
192
		/** @skipUpgrade */
193
		return CMSMemberLoginForm::create($controller, "LoginForm");
194
	}
195
196
	public static function supports_cms() {
197
		// Don't automatically support subclasses of MemberAuthenticator
198
		return get_called_class() === __CLASS__;
199
	}
200
201
202
	/**
203
	 * Get the name of the authentication method
204
	 *
205
	 * @return string Returns the name of the authentication method.
206
	 */
207
	public static function get_name() {
208
		return _t('MemberAuthenticator.TITLE', "E-mail &amp; Password");
209
	}
210
}
211
212