Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.37% |
38 / 43 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
AbstractPasswordPrimaryAuthenticationProvider | |
88.37% |
38 / 43 |
|
90.00% |
9 / 10 |
29.23 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getPasswordFactory | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getPassword | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
failResponse | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
checkPasswordValidity | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFatalPasswordErrorResponse | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setPasswordResetFlag | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
8 | |||
getPasswordResetData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNewPasswordExpiry | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getAuthenticationRequests | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @ingroup Auth |
20 | */ |
21 | |
22 | namespace MediaWiki\Auth; |
23 | |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\SpecialPage\SpecialPage; |
26 | use MediaWiki\Status\Status; |
27 | use MediaWiki\User\User; |
28 | use Password; |
29 | use PasswordFactory; |
30 | use Wikimedia\Assert\Assert; |
31 | |
32 | /** |
33 | * Basic framework for a primary authentication provider that uses passwords |
34 | * |
35 | * @stable to extend |
36 | * @ingroup Auth |
37 | * @since 1.27 |
38 | */ |
39 | abstract class AbstractPasswordPrimaryAuthenticationProvider |
40 | extends AbstractPrimaryAuthenticationProvider |
41 | { |
42 | /** @var bool Whether this provider should ABSTAIN (false) or FAIL (true) on password failure */ |
43 | protected $authoritative; |
44 | |
45 | private $passwordFactory = null; |
46 | |
47 | /** |
48 | * @stable to call |
49 | * @param array $params Settings |
50 | * - authoritative: Whether this provider should ABSTAIN (false) or FAIL |
51 | * (true) on password failure |
52 | */ |
53 | public function __construct( array $params = [] ) { |
54 | $this->authoritative = !isset( $params['authoritative'] ) || (bool)$params['authoritative']; |
55 | } |
56 | |
57 | /** |
58 | * @return PasswordFactory |
59 | */ |
60 | protected function getPasswordFactory() { |
61 | if ( $this->passwordFactory === null ) { |
62 | $this->passwordFactory = new PasswordFactory( |
63 | $this->config->get( MainConfigNames::PasswordConfig ), |
64 | $this->config->get( MainConfigNames::PasswordDefault ) |
65 | ); |
66 | } |
67 | return $this->passwordFactory; |
68 | } |
69 | |
70 | /** |
71 | * Get a Password object from the hash |
72 | * @param string $hash |
73 | * @return Password |
74 | */ |
75 | protected function getPassword( $hash ) { |
76 | $passwordFactory = $this->getPasswordFactory(); |
77 | try { |
78 | return $passwordFactory->newFromCiphertext( $hash ); |
79 | } catch ( \PasswordError $e ) { |
80 | $class = static::class; |
81 | $this->logger->debug( "Invalid password hash in {$class}::getPassword()" ); |
82 | return $passwordFactory->newFromCiphertext( null ); |
83 | } |
84 | } |
85 | |
86 | /** |
87 | * Return the appropriate response for failure |
88 | * @param PasswordAuthenticationRequest $req |
89 | * @return AuthenticationResponse |
90 | */ |
91 | protected function failResponse( PasswordAuthenticationRequest $req ) { |
92 | if ( $this->authoritative ) { |
93 | return AuthenticationResponse::newFail( |
94 | wfMessage( $req->password === '' ? 'wrongpasswordempty' : 'wrongpassword' ) |
95 | ); |
96 | } else { |
97 | return AuthenticationResponse::newAbstain(); |
98 | } |
99 | } |
100 | |
101 | /** |
102 | * Check that the password is valid |
103 | * |
104 | * This should be called *before* validating the password. If the result is |
105 | * not ok, login should fail immediately. |
106 | * |
107 | * @param string $username |
108 | * @param string $password |
109 | * @return Status |
110 | */ |
111 | protected function checkPasswordValidity( $username, $password ) { |
112 | return User::newFromName( $username )->checkPasswordValidity( $password ); |
113 | } |
114 | |
115 | /** |
116 | * Adds user-friendly description to a fatal password validity check error. |
117 | * These errors prevent login even when the password is correct, so just displaying the |
118 | * description of the error would be somewhat confusing. |
119 | * @param string $username |
120 | * @param Status $status The status returned by checkPasswordValidity(); must be a fatal. |
121 | * @return AuthenticationResponse A FAIL response with an improved description. |
122 | */ |
123 | protected function getFatalPasswordErrorResponse( |
124 | string $username, |
125 | Status $status |
126 | ): AuthenticationResponse { |
127 | Assert::precondition( !$status->isOK(), __METHOD__ . ' expects a fatal Status' ); |
128 | $resetLinkUrl = SpecialPage::getTitleFor( 'PasswordReset' ) |
129 | ->getFullURL( [ 'wpUsername' => $username ] ); |
130 | return AuthenticationResponse::newFail( wfMessage( 'fatalpassworderror', |
131 | $status->getMessage(), $resetLinkUrl ) ); |
132 | } |
133 | |
134 | /** |
135 | * Check if the password should be reset |
136 | * |
137 | * This should be called after a successful login. It sets 'reset-pass' |
138 | * authentication data if necessary, see |
139 | * ResetPassSecondaryAuthenticationProvider. |
140 | * |
141 | * @param string $username |
142 | * @param Status $status From $this->checkPasswordValidity() |
143 | * @param \stdClass|null $data Passed through to $this->getPasswordResetData() |
144 | */ |
145 | protected function setPasswordResetFlag( $username, Status $status, $data = null ) { |
146 | $reset = $this->getPasswordResetData( $username, $data ); |
147 | |
148 | if ( !$reset && $this->config->get( MainConfigNames::InvalidPasswordReset ) && |
149 | !$status->isGood() ) { |
150 | $hard = $status->getValue()['forceChange'] ?? false; |
151 | |
152 | if ( $hard || !empty( $status->getValue()['suggestChangeOnLogin'] ) ) { |
153 | $reset = (object)[ |
154 | 'msg' => $status->getMessage( $hard ? 'resetpass-validity' : 'resetpass-validity-soft' ), |
155 | 'hard' => $hard, |
156 | ]; |
157 | } |
158 | } |
159 | |
160 | if ( $reset ) { |
161 | $this->manager->setAuthenticationSessionData( 'reset-pass', $reset ); |
162 | } |
163 | } |
164 | |
165 | /** |
166 | * Get password reset data, if any |
167 | * |
168 | * @stable to override |
169 | * @param string $username |
170 | * @param \stdClass|null $data |
171 | * @return \stdClass|null { 'hard' => bool, 'msg' => Message } |
172 | */ |
173 | protected function getPasswordResetData( $username, $data ) { |
174 | return null; |
175 | } |
176 | |
177 | /** |
178 | * Get expiration date for a new password, if any |
179 | * |
180 | * @stable to override |
181 | * @param string $username |
182 | * @return string|null |
183 | */ |
184 | protected function getNewPasswordExpiry( $username ) { |
185 | $days = $this->config->get( MainConfigNames::PasswordExpirationDays ); |
186 | $expires = $days ? wfTimestamp( TS_MW, time() + $days * 86400 ) : null; |
187 | |
188 | // Give extensions a chance to force an expiration |
189 | $this->getHookRunner()->onResetPasswordExpiration( |
190 | User::newFromName( $username ), $expires ); |
191 | |
192 | return $expires; |
193 | } |
194 | |
195 | /** |
196 | * @stable to override |
197 | * @param string $action |
198 | * @param array $options |
199 | * |
200 | * @return AuthenticationRequest[] |
201 | */ |
202 | public function getAuthenticationRequests( $action, array $options ) { |
203 | switch ( $action ) { |
204 | case AuthManager::ACTION_LOGIN: |
205 | case AuthManager::ACTION_REMOVE: |
206 | case AuthManager::ACTION_CREATE: |
207 | case AuthManager::ACTION_CHANGE: |
208 | return [ new PasswordAuthenticationRequest() ]; |
209 | default: |
210 | return []; |
211 | } |
212 | } |
213 | } |