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