Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.65% covered (warning)
59.65%
34 / 57
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiOATHValidate
59.65% covered (warning)
59.65%
34 / 57
0.00% covered (danger)
0.00%
0 / 6
29.78
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
91.89% covered (success)
91.89%
34 / 37
0.00% covered (danger)
0.00%
0 / 1
10.05
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
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
19namespace MediaWiki\Extension\OATHAuth\Api\Module;
20
21use MediaWiki\Api\ApiBase;
22use MediaWiki\Api\ApiMain;
23use MediaWiki\Api\ApiResult;
24use MediaWiki\Extension\OATHAuth\OATHUserRepository;
25use MediaWiki\Json\FormatJson;
26use MediaWiki\Logger\LoggerFactory;
27use MediaWiki\User\UserFactory;
28use Wikimedia\ParamValidator\ParamValidator;
29
30/**
31 * Validate an OATH token.
32 *
33 * @ingroup API
34 * @ingroup Extensions
35 */
36class ApiOATHValidate extends ApiBase {
37    private OATHUserRepository $oathUserRepository;
38    private UserFactory $userFactory;
39
40    public function __construct(
41        ApiMain $mainModule,
42        string $moduleName,
43        OATHUserRepository $oathUserRepository,
44        UserFactory $userFactory
45    ) {
46        parent::__construct( $mainModule, $moduleName );
47        $this->oathUserRepository = $oathUserRepository;
48        $this->userFactory = $userFactory;
49    }
50
51    public function execute() {
52        $this->requirePostedParameters( [ 'token', 'data' ] );
53        // messages used: right-oathauth-api-all, action-oathauth-api-all,
54        $this->checkUserRightsAny( 'oathauth-api-all' );
55
56        $params = $this->extractRequestParams();
57        if ( $params['user'] === null ) {
58            $user = $this->getUser();
59        } else {
60            $user = $this->userFactory->newFromName( $params['user'] );
61            if ( $user === null ) {
62                $this->dieWithError( 'noname' );
63            }
64        }
65
66        // Don't increase pingLimiter, just check for limit exceeded.
67        if ( $user->pingLimiter( 'badoath', 0 ) ) {
68            $this->dieWithError( 'apierror-ratelimited' );
69        }
70
71        $result = [
72            ApiResult::META_BC_BOOLS => [ 'enabled', 'valid' ],
73            'enabled' => false,
74            'valid' => false,
75        ];
76
77        if ( $user->isNamed() ) {
78            $authUser = $this->oathUserRepository->findByUser( $user );
79            if ( $authUser->isTwoFactorAuthEnabled() ) {
80                $result['enabled'] = true;
81
82                $data = [];
83                $decoded = FormatJson::decode( $params['data'], true );
84                if ( is_array( $decoded ) ) {
85                    $data = $decoded;
86                }
87
88                foreach ( $authUser->getKeys() as $key ) {
89                    if ( $key->verify( $data, $authUser ) !== false ) {
90                        $result['valid'] = true;
91                        break;
92                    }
93                }
94
95                if ( !$result['valid'] ) {
96                    // Increase rate limit counter for failed request
97                    $user->pingLimiter( 'badoath' );
98
99                    LoggerFactory::getInstance( 'authentication' )->info(
100                        'OATHAuth user {user} failed OTP token/recovery code from {clientip}',
101                        [
102                            'user'     => $user,
103                            'clientip' => $user->getRequest()->getIP(),
104                        ]
105                    );
106                }
107            }
108        }
109
110        $this->getResult()->addValue( null, $this->getModuleName(), $result );
111    }
112
113    /** @inheritDoc */
114    public function isInternal() {
115        return true;
116    }
117
118    /** @inheritDoc */
119    public function needsToken() {
120        return 'csrf';
121    }
122
123    /**
124     * @return array
125     */
126    public function getAllowedParams() {
127        return [
128            'user' => [
129                ParamValidator::PARAM_TYPE => 'user',
130            ],
131            'data' => [
132                ParamValidator::PARAM_TYPE => 'string',
133                ParamValidator::PARAM_REQUIRED => true,
134            ]
135        ];
136    }
137
138    /**
139     * @return array
140     */
141    protected function getExamplesMessages() {
142        return [
143            'action=oathvalidate&data={"token":"123456"}&token=123ABC'
144                => 'apihelp-oathvalidate-example-1',
145            'action=oathvalidate&user=Example&data={"token":"123456"}&token=123ABC'
146                => 'apihelp-oathvalidate-example-3',
147        ];
148    }
149}