Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthenticationHandler
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 11
552
0.00% covered (danger)
0.00%
0 / 1
 factory
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 needsReadAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthorizationProvider
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 validate
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 queueError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getQueryParamsCgi
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 errorResponse
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 getLocalizedErrorMessage
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 detectExtraneousBodyFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGrantType
n/a
0 / 0
n/a
0 / 0
0
 getGrantClass
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3namespace MediaWiki\Extension\OAuth\Rest\Handler;
4
5use League\OAuth2\Server\Exception\OAuthServerException;
6use LogicException;
7use MediaWiki\Config\Config;
8use MediaWiki\Context\RequestContext;
9use MediaWiki\Extension\OAuth\AuthorizationProvider\AccessToken as AccessTokenProvider;
10use MediaWiki\Extension\OAuth\AuthorizationProvider\Grant\AuthorizationCodeAuthorization;
11use MediaWiki\Extension\OAuth\Backend\Utils;
12use MediaWiki\Extension\OAuth\Response;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Message\Message;
15use MediaWiki\Rest\Handler;
16use MediaWiki\Rest\HttpException;
17use MediaWiki\Rest\LocalizedHttpException;
18use MediaWiki\Rest\Response as RestResponse;
19use MediaWiki\Rest\StringStream;
20use MediaWiki\Rest\Validator\Validator;
21use MediaWiki\Title\Title;
22use MediaWiki\User\User;
23use Psr\Http\Message\ResponseInterface;
24
25abstract class AuthenticationHandler extends Handler {
26
27    /**
28     * @var User
29     */
30    protected $user;
31
32    /**
33     * @var Config
34     */
35    protected $config;
36
37    /**
38     * @var OAuthServerException|null
39     */
40    protected $queuedError;
41
42    /**
43     * @return AuthenticationHandler
44     */
45    public static function factory() {
46        $centralId = Utils::getCentralIdFromLocalUser( RequestContext::getMain()->getUser() );
47        $user = $centralId ? Utils::getLocalUserFromCentralId( $centralId ) : User::newFromId( 0 );
48        $config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'mwoauth' );
49        // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
50        return new static( $user, $config );
51    }
52
53    /**
54     * @param User $user
55     * @param Config $config
56     */
57    protected function __construct( User $user, Config $config ) {
58        $this->user = $user;
59        $this->config = $config;
60    }
61
62    /**
63     * We do not want any permission checks
64     *
65     * @return bool
66     */
67    public function needsReadAccess() {
68        return false;
69    }
70
71    /**
72     * We do not want any permission checks
73     *
74     * @return bool
75     */
76    public function needsWriteAccess() {
77        return false;
78    }
79
80    /**
81     * @throws HttpException
82     * @return AccessTokenProvider|AuthorizationCodeAuthorization
83     */
84    protected function getAuthorizationProvider() {
85        $grantType = $this->getGrantType();
86
87        $class = $this->getGrantClass( $grantType );
88        if ( !$class || !is_callable( [ $class, 'factory' ] ) ) {
89            throw new LogicException( 'Could not find grant class factory' );
90        }
91
92        /** @var AccessTokenProvider|AuthorizationCodeAuthorization $authProvider */
93        $authProvider = $class::factory();
94        '@phan-var AccessTokenProvider|AuthorizationCodeAuthorization $authProvider';
95        return $authProvider;
96    }
97
98    public function validate( Validator $restValidator ) {
99        try {
100            parent::validate( $restValidator );
101        } catch ( HttpException $exception ) {
102            if ( $exception instanceof LocalizedHttpException ) {
103                $formatted = $this->getResponseFactory()->formatMessage( $exception->getMessageValue() );
104                $message = $formatted['messageTranslations']['en'] ?? reset( $formatted['messageTranslations'] );
105            } else {
106                $message = $exception->getMessage();
107            }
108            // Catch and store any validation errors, so they can be thrown
109            // during the execution, and get caught by appropriate error handling code
110            $type = $exception->getErrorData()['error'] ?? 'parameter-validation-failed';
111            if ( $type === 'parameter-validation-failed' ) {
112                $missingParam = $exception->getErrorData()['name'] ?? '';
113                $this->queueError( new OAuthServerException(
114                    // OAuthServerException::invalidRequest() but with more useful text
115                    'Invalid request: ' . $message,
116                    3,
117                    'invalid_request',
118                    400,
119                    $missingParam ? \sprintf( 'Check the `%s` parameter', $missingParam ) : null
120                ) );
121                return;
122            }
123            $this->queueError( OAuthServerException::serverError( $message ) );
124        }
125    }
126
127    /**
128     * @param OAuthServerException $ex
129     */
130    protected function queueError( OAuthServerException $ex ) {
131        // If already set, do not override, since we cannot throw more than one error,
132        // and it will probably be more useful to throw first error that occurred
133        if ( !$this->queuedError ) {
134            $this->queuedError = $ex;
135        }
136    }
137
138    /**
139     * @param array $query
140     * @return string
141     */
142    protected function getQueryParamsCgi( $query = [] ) {
143        $queryParams = $this->getRequest()->getQueryParams();
144        unset( $queryParams['title'] );
145
146        $queryParams = array_merge( $queryParams, $query );
147        return wfArrayToCgi( $queryParams );
148    }
149
150    /**
151     * @param OAuthServerException $exception
152     * @param Response|null $response
153     * @return ResponseInterface|RestResponse
154     */
155    protected function errorResponse( $exception, $response = null ) {
156        $response ??= new Response();
157        $response = $exception->generateHttpResponse( $response );
158        if ( $exception->hasRedirect() || $this->getRequest()->getMethod() === 'POST' ) {
159            return $response;
160        }
161
162        $context = RequestContext::getMain();
163        // T379504: Set dummy context title
164        $context->setTitle( Title::makeTitle( NS_SPECIAL, 'Api/Rest' ) );
165        $out = $context->getOutput();
166
167        $out->showErrorPage(
168            'mwoauth-error',
169            $this->getLocalizedErrorMessage( $exception )
170        );
171
172        ob_start();
173        $out->output();
174        $html = ob_get_clean();
175
176        $response = $this->getResponseFactory()->create();
177        $stream = new StringStream( $html );
178        $response->setStatus( $exception->getHttpStatusCode() );
179        $response->setHeader( 'Content-Type', 'text/html' );
180        $response->setBody( $stream );
181
182        return $response;
183    }
184
185    private function getLocalizedErrorMessage( OAuthServerException $exception ): Message {
186        $type = $exception->getErrorType();
187        $map = [
188            'invalid_client' => 'mwoauth-oauth2-error-invalid-client',
189            'invalid_request' => 'mwoauth-oauth2-error-invalid-request',
190            'unauthorized_client' => 'mwoauth-oauth2-error-unauthorized-client',
191            'access_denied' => 'mwoauth-oauth2-error-access-denied',
192            'unsupported_response_type' => 'mwoauth-oauth2-error-unsupported-response-type',
193            'invalid_scope' => 'mwoauth-oauth2-error-invalid-scope',
194            'temporarily_unavailable' => 'mwoauth-oauth2-error-temporarily-unavailable',
195            // 'server_error' is passed through to the catch-all handler below
196        ];
197        $msg = isset( $map[$type] )
198            ? wfMessage( $map[$type] )
199            : wfMessage( 'mwoauth-oauth2-error-server-error', $exception->getMessage() );
200        if ( $exception->getHint() ) {
201            return wfMessage( 'mwoauth-oauth2-error-serverexception-withhint', $msg, $exception->getHint() );
202        } else {
203            return $msg;
204        }
205    }
206
207    /** @inheritDoc */
208    protected function detectExtraneousBodyFields( Validator $restValidator ) {
209        // Ignore unexpected parameters per https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
210        // and https://datatracker.ietf.org/doc/html/rfc6749#section-3.2 :
211        // "The authorization server MUST ignore unrecognized request parameters."
212    }
213
214    /**
215     * @return string
216     */
217    abstract protected function getGrantType();
218
219    /**
220     * @param string $grantType
221     * @return string|false
222     */
223    abstract protected function getGrantClass( $grantType );
224
225}