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