Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiWebAuthn
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 13
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 9
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
6
 mustBePosted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isWriteMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSummaryMessage
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 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 checkPermissions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 checkModule
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAuthInfo
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getRegisterInfo
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 register
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getOATHUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 */
5
6namespace MediaWiki\Extension\OATHAuth\Api\Module;
7
8use MediaWiki\Api\ApiBase;
9use MediaWiki\Api\ApiMain;
10use MediaWiki\Auth\AuthManager;
11use MediaWiki\Extension\OATHAuth\Module\WebAuthn as WebAuthnModule;
12use MediaWiki\Extension\OATHAuth\OATHAuthModuleRegistry;
13use MediaWiki\Extension\OATHAuth\OATHUser;
14use MediaWiki\Extension\OATHAuth\OATHUserRepository;
15use MediaWiki\Extension\OATHAuth\WebAuthnAuthenticator;
16use Wikimedia\ParamValidator\ParamValidator;
17
18/**
19 * This class provides an endpoint for all WebAuthn actions.
20 */
21class ApiWebAuthn extends ApiBase {
22
23    private const ACTION_GET_AUTH_INFO = 'getAuthInfo';
24    private const ACTION_GET_REGISTER_INFO = 'getRegisterInfo';
25    private const ACTION_REGISTER = 'register';
26
27    /**
28     * Array of all functions that are allowed to be called.
29     * Each key must have the appropriate configuration that
30     * defines user requirements for the action.
31     */
32    private const REGISTERED_FUNCTIONS = [
33        self::ACTION_GET_AUTH_INFO => [
34            'permissions' => [],
35            'mustBeLoggedIn' => false,
36        ],
37        self::ACTION_GET_REGISTER_INFO => [
38            'permissions' => [ 'oathauth-enable' ],
39            'mustBeLoggedIn' => true,
40        ],
41        self::ACTION_REGISTER => [
42            'permissions' => [ 'oathauth-enable' ],
43            'mustBeLoggedIn' => true,
44            'loginSecurityLevel' => 'OATHManage'
45        ],
46    ];
47
48    public function __construct(
49        ApiMain $main,
50        string $moduleName,
51        private readonly AuthManager $authManager,
52        private readonly OATHAuthModuleRegistry $moduleRegistry,
53        private readonly OATHUserRepository $userRepo,
54        private readonly WebAuthnAuthenticator $authenticator,
55    ) {
56        parent::__construct( $main, $moduleName );
57    }
58
59    public function execute() {
60        $func = $this->getParameter( 'func' );
61
62        $this->checkPermissions( $func );
63        $this->checkModule();
64
65        $result = match ( $func ) {
66            self::ACTION_GET_REGISTER_INFO => $this->getRegisterInfo(),
67            self::ACTION_GET_AUTH_INFO => $this->getAuthInfo(),
68            self::ACTION_REGISTER => $this->register(),
69        };
70
71        $this->getResult()->addValue( null, $this->getModuleName(), $result );
72    }
73
74    /** @inheritDoc */
75    public function needsToken() {
76        return $this->getRequest()->getVal( 'func' ) === self::ACTION_REGISTER ? 'csrf' : false;
77    }
78
79    /** @inheritDoc */
80    public function mustBePosted() {
81        return $this->getRequest()->getVal( 'func' ) === self::ACTION_REGISTER;
82    }
83
84    /** @inheritDoc */
85    public function isWriteMode() {
86        return $this->getRequest()->getVal( 'func' ) === self::ACTION_REGISTER;
87    }
88
89    /** @inheritDoc */
90    protected function getSummaryMessage() {
91        return "apihelp-oathauth-webauthn-summary";
92    }
93
94    /** @inheritDoc */
95    public function getAllowedParams() {
96        return [
97            'func' => [
98                ParamValidator::PARAM_TYPE => array_keys( self::REGISTERED_FUNCTIONS ),
99                ParamValidator::PARAM_REQUIRED => true,
100                ApiBase::PARAM_HELP_MSG => 'apihelp-oathauth-webauthn-param-func',
101                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
102                    'getAuthInfo' => 'apihelp-oathauth-webauthn-paramvalue-func-getauthinfo',
103                    'getRegisterInfo' => 'apihelp-oathauth-webauthn-paramvalue-func-getregisterinfo',
104                    'register' => 'apihelp-oathauth-webauthn-paramvalue-func-register',
105                ],
106            ],
107            'passkeyMode' => [
108                ParamValidator::PARAM_TYPE => 'boolean',
109                ParamValidator::PARAM_REQUIRED => false,
110                ApiBase::PARAM_HELP_MSG => 'apihelp-oathauth-webauthn-param-passkeymode',
111            ],
112            'credential' => [
113                ParamValidator::PARAM_TYPE => 'string',
114                ParamValidator::PARAM_REQUIRED => false,
115                ApiBase::PARAM_HELP_MSG => 'apihelp-oathauth-webauthn-param-credential',
116            ],
117            'friendlyname' => [
118                ParamValidator::PARAM_TYPE => 'string',
119                ParamValidator::PARAM_REQUIRED => false,
120                ApiBase::PARAM_HELP_MSG => 'apihelp-oathauth-webauthn-param-friendlyname',
121            ],
122        ];
123    }
124
125    private function checkPermissions( string $func ): void {
126        $functionConfig = self::REGISTERED_FUNCTIONS[$func];
127
128        $mustBeLoggedIn = $functionConfig['mustBeLoggedIn'];
129        if ( $mustBeLoggedIn === true ) {
130            $user = $this->getUser();
131            if ( !$user->isNamed() ) {
132                $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-oathauth-enable' ) ] );
133            }
134        }
135
136        $funcPermissions = $functionConfig['permissions'];
137        if ( $funcPermissions ) {
138            $this->checkUserRightsAny( $funcPermissions );
139        }
140
141        if ( isset( $functionConfig[ 'loginSecurityLevel' ] ) ) {
142            $status = $this->authManager->securitySensitiveOperationStatus( $functionConfig[ 'loginSecurityLevel' ] );
143            if ( $status !== AuthManager::SEC_OK ) {
144                $this->dieWithError( 'apierror-oathauth-webauthn-reauthenticate' );
145            }
146        }
147    }
148
149    private function checkModule() {
150        $module = $this->moduleRegistry->getModuleByKey( WebAuthnModule::MODULE_ID );
151        if ( !( $module instanceof WebAuthnModule ) ) {
152            $this->dieWithError( 'apierror-oathauth-webauthn-module-missing' );
153        }
154    }
155
156    private function getAuthInfo(): array {
157        $oathUser = $this->getOATHUser();
158        $startAuthResult = $this->authenticator->startAuthentication( $oathUser );
159        if ( $startAuthResult->isGood() ) {
160            return [
161                'auth_info' => $startAuthResult->getValue()['json']
162            ];
163        }
164        $this->dieWithError( $startAuthResult->getMessage() );
165    }
166
167    private function getRegisterInfo(): array {
168        $oathUser = $this->getOATHUser();
169        $startRegResult = $this->authenticator->startRegistration(
170            $oathUser,
171            (bool)$this->getParameter( 'passkeyMode' )
172        );
173        if ( $startRegResult->isGood() ) {
174            return [
175                'register_info' => $startRegResult->getValue()['json']
176            ];
177        }
178        $this->dieWithError( $startRegResult->getMessage() );
179    }
180
181    private function register(): array {
182        $credentialJson = $this->getParameter( 'credential' );
183
184        if ( !$credentialJson ) {
185            $this->dieWithError( 'apierror-oathauth-webauthn-missing-credential' );
186        }
187
188        $result = $this->authenticator->continueRegistration(
189            $this->getOATHUser(),
190            $credentialJson,
191            $this->getParameter( 'friendlyname' ) ?? '',
192            (bool)$this->getParameter( 'passkeyMode' )
193        );
194
195        if ( !$result->isGood() ) {
196            $this->dieWithError( $result->getMessage() );
197        }
198
199        return [ 'success' => true ];
200    }
201
202    private function getOATHUser(): OATHUser {
203        return $this->userRepo->findByUser( $this->getUser() );
204    }
205}