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