Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 76 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
AuthenticationHandler | |
0.00% |
0 / 76 |
|
0.00% |
0 / 11 |
552 | |
0.00% |
0 / 1 |
factory | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
needsReadAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAuthorizationProvider | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
validate | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
queueError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getQueryParamsCgi | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
errorResponse | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
getLocalizedErrorMessage | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
detectExtraneousBodyFields | |
0.00% |
0 / 1 |
|
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 | |
3 | namespace MediaWiki\Extension\OAuth\Rest\Handler; |
4 | |
5 | use League\OAuth2\Server\Exception\OAuthServerException; |
6 | use LogicException; |
7 | use MediaWiki\Config\Config; |
8 | use MediaWiki\Context\RequestContext; |
9 | use MediaWiki\Extension\OAuth\AuthorizationProvider\AccessToken as AccessTokenProvider; |
10 | use MediaWiki\Extension\OAuth\AuthorizationProvider\Grant\AuthorizationCodeAuthorization; |
11 | use MediaWiki\Extension\OAuth\Backend\Utils; |
12 | use MediaWiki\Extension\OAuth\Response; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Message\Message; |
15 | use MediaWiki\Rest\Handler; |
16 | use MediaWiki\Rest\HttpException; |
17 | use MediaWiki\Rest\LocalizedHttpException; |
18 | use MediaWiki\Rest\Response as RestResponse; |
19 | use MediaWiki\Rest\StringStream; |
20 | use MediaWiki\Rest\Validator\Validator; |
21 | use MediaWiki\Title\Title; |
22 | use MediaWiki\User\User; |
23 | use Psr\Http\Message\ResponseInterface; |
24 | |
25 | abstract 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 | } |