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\SpecialPage\SpecialPage;
26use MediaWiki\Status\Status;
27use MediaWiki\User\User;
28use Password;
29use PasswordFactory;
30use 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 */
39abstract 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}