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