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 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Auth;
8
9use MediaWiki\MainConfigNames;
10use MediaWiki\Password\Password;
11use MediaWiki\Password\PasswordError;
12use MediaWiki\Password\PasswordFactory;
13use MediaWiki\SpecialPage\SpecialPage;
14use MediaWiki\Status\Status;
15use MediaWiki\User\User;
16use Wikimedia\Assert\Assert;
17use 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 */
26abstract 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}