AuthAction   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 386
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 114
dl 0
loc 386
ccs 0
cts 116
cp 0
rs 9.28
c 0
b 0
f 0
wmc 39

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A authOAuth1() 0 23 4
A withSuccessUrl() 0 5 1
A withCancelUrl() 0 5 1
A process() 0 13 3
A redirectCancel() 0 6 2
A auth() 0 15 4
A redirectSuccess() 0 6 2
B authOAuth2() 0 30 8
A authOpenId() 0 26 5
A redirect() 0 18 2
A authCancel() 0 14 3
A authSuccess() 0 14 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\AuthClient;
6
7
use Exception;
8
use Psr\Http\Message\ResponseFactoryInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use Throwable;
14
use Yiisoft\Aliases\Aliases;
15
use Yiisoft\Http\Status;
16
use Yiisoft\View\Exception\ViewNotFoundException;
17
use Yiisoft\View\WebView;
18
use Yiisoft\Yii\AuthClient\Exception\InvalidConfigException;
19
use Yiisoft\Yii\AuthClient\Exception\NotSupportedException;
20
21
/**
22
 * AuthAction performs authentication via different auth clients.
23
 * It supports {@see OpenId}, {@see OAuth1} and {@see OAuth2} client types.
24
 *
25
 * Usage:
26
 *
27
 * ```php
28
 * class SiteController extends Controller
29
 * {
30
 *     public function actions()
31
 *     {
32
 *         return [
33
 *             'auth' => [
34
 *                 'class' => \Yiisoft\Yii\AuthClient\AuthAction::class,
35
 *                 'successCallback' => [$this, 'successCallback'],
36
 *             ],
37
 *         ]
38
 *     }
39
 *
40
 *     public function successCallback($client)
41
 *     {
42
 *         $attributes = $client->getUserAttributes();
43
 *         // user login or signup comes here
44
 *     }
45
 * }
46
 * ```
47
 *
48
 * Usually authentication via external services is performed inside the popup window.
49
 * This action handles the redirection and closing of popup window correctly.
50
 *
51
 * @see Collection
52
 * @see \Yiisoft\Yii\AuthClient\Widget\AuthChoice
53
 */
54
final class AuthAction implements MiddlewareInterface
55
{
56
    public const AUTH_NAME = 'auth_displayname';
57
    /**
58
     * @var Collection
59
     * It should point to {@see Collection} instance.
60
     */
61
    private Collection $clientCollection;
62
    /**
63
     * @var string name of the GET param, which is used to passed auth client id to this action.
64
     * Note: watch for the naming, make sure you do not choose name used in some auth protocol.
65
     */
66
    private string $clientIdGetParamName = 'authclient';
67
    /**
68
     * @var callable PHP callback, which should be triggered in case of successful authentication.
69
     * This callback should accept {@see AuthClientInterface} instance as an argument.
70
     * For example:
71
     *
72
     * ```php
73
     * public function onAuthSuccess(ClientInterface $client)
74
     * {
75
     *     $attributes = $client->getUserAttributes();
76
     *     // user login or signup comes here
77
     * }
78
     * ```
79
     *
80
     * If this callback returns {@see ResponseInterface} instance, it will be used as action response,
81
     * otherwise redirection to {@see successUrl} will be performed.
82
     */
83
    private $successCallback;
84
    /**
85
     * @var callable PHP callback, which should be triggered in case of authentication cancellation.
86
     * This callback should accept {@see AuthClientInterface} instance as an argument.
87
     * For example:
88
     *
89
     * ```php
90
     * public function onAuthCancel(ClientInterface $client)
91
     * {
92
     *     // set flash, logging, etc.
93
     * }
94
     * ```
95
     *
96
     * If this callback returns {@see ResponseInterface} instance, it will be used as action response,
97
     * otherwise redirection to {@see cancelUrl} will be performed.
98
     */
99
    private $cancelCallback;
100
    /**
101
     * @var string name or alias of the view file, which should be rendered in order to perform redirection.
102
     * If not set - default one will be used.
103
     */
104
    private string $redirectView;
105
106
    /**
107
     * @var string the redirect url after successful authorization.
108
     */
109
    private string $successUrl;
110
    /**
111
     * @var string the redirect url after unsuccessful authorization (e.g. user canceled).
112
     */
113
    private string $cancelUrl;
114
    private ResponseFactoryInterface $responseFactory;
115
    private Aliases $aliases;
116
    private WebView $view;
117
118
    public function __construct(
119
        Collection $clientCollection,
120
        Aliases $aliases,
121
        WebView $view,
122
        ResponseFactoryInterface $responseFactory
123
    ) {
124
        $this->clientCollection = $clientCollection;
125
        $this->responseFactory = $responseFactory;
126
        $this->aliases = $aliases;
127
        $this->view = $view;
128
    }
129
130
    /**
131
     * @param string $url successful URL.
132
     *
133
     * @return AuthAction
134
     */
135
    public function withSuccessUrl(string $url): self
136
    {
137
        $new = clone $this;
138
        $new->successUrl = $url;
139
        return $new;
140
    }
141
142
    /**
143
     * @param string $url cancel URL.
144
     *
145
     * @return AuthAction
146
     */
147
    public function withCancelUrl(string $url): self
148
    {
149
        $new = clone $this;
150
        $new->cancelUrl = $url;
151
        return $new;
152
    }
153
154
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
155
    {
156
        $clientId = $request->getAttribute($this->clientIdGetParamName);
157
        if (!empty($clientId)) {
158
            if (!$this->clientCollection->hasClient($clientId)) {
159
                return $this->responseFactory->createResponse(Status::NOT_FOUND, "Unknown auth client '{$clientId}'");
160
            }
161
            $client = $this->clientCollection->getClient($clientId);
162
163
            return $this->auth($client, $request);
164
        }
165
166
        return $this->responseFactory->createResponse(Status::NOT_FOUND);
167
    }
168
169
    /**
170
     * Perform authentication for the given client.
171
     *
172
     * @param mixed $client auth client instance.
173
     * @param ServerRequestInterface $request
174
     *
175
     * @throws InvalidConfigException
176
     * @throws NotSupportedException on invalid client.
177
     * @throws Throwable
178
     * @throws ViewNotFoundException
179
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
180
     *
181
     * @return ResponseInterface response instance.
182
     */
183
    private function auth(AuthClientInterface $client, ServerRequestInterface $request): ResponseInterface
184
    {
185
        if ($client instanceof OAuth2) {
186
            return $this->authOAuth2($client, $request);
187
        }
188
189
        if ($client instanceof OAuth1) {
190
            return $this->authOAuth1($client, $request);
191
        }
192
193
        if ($client instanceof OpenId) {
194
            return $this->authOpenId($client, $request);
195
        }
196
197
        throw new NotSupportedException('Provider "' . get_class($client) . '" is not supported.');
198
    }
199
200
    /**
201
     * Performs OAuth2 auth flow.
202
     *
203
     * @param OAuth2 $client auth client instance.
204
     * @param ServerRequestInterface $request
205
     *
206
     * @throws InvalidConfigException
207
     * @throws Throwable
208
     * @throws ViewNotFoundException
209
     *
210
     * @return ResponseInterface action response.
211
     */
212
    private function authOAuth2(OAuth2 $client, ServerRequestInterface $request): ResponseInterface
213
    {
214
        $queryParams = $request->getQueryParams();
215
216
        if (isset($queryParams['error']) && ($error = $queryParams['error']) !== null) {
217
            if ($error === 'access_denied') {
218
                // user denied error
219
                return $this->authCancel($client);
220
            }
221
            // request error
222
            $errorMessage = $queryParams['error_description'] ?? $queryParams['error_message'] ?? null;
223
            if ($errorMessage === null) {
224
                $errorMessage = http_build_query($queryParams);
225
            }
226
            throw new Exception('Auth error: ' . $errorMessage);
227
        }
228
229
        // Get the access_token and save them to the session.
230
        if (isset($queryParams['code']) && ($code = $queryParams['code']) !== null) {
231
            $token = $client->fetchAccessToken($request, $code);
232
            if (!empty($token->getToken())) {
233
                return $this->authSuccess($client);
234
            }
235
            return $this->authCancel($client);
236
        }
237
238
        $url = $client->buildAuthUrl($request);
239
        return $this->responseFactory
240
            ->createResponse(Status::MOVED_PERMANENTLY)
241
            ->withHeader('Location', $url);
242
    }
243
244
    /**
245
     * This method is invoked in case of authentication cancellation.
246
     *
247
     * @param AuthClientInterface $client auth client instance.
248
     *
249
     * @throws Throwable
250
     * @throws ViewNotFoundException
251
     *
252
     * @return ResponseInterface response instance.
253
     */
254
    private function authCancel(AuthClientInterface $client): ResponseInterface
255
    {
256
        if (!is_callable($this->cancelCallback)) {
257
            throw new InvalidConfigException(
258
                '"' . self::class . '::$successCallback" should be a valid callback.'
259
            );
260
        }
261
262
        $response = ($this->cancelCallback)($client);
263
        if ($response instanceof ResponseInterface) {
264
            return $response;
265
        }
266
267
        return $this->redirectCancel();
268
    }
269
270
    /**
271
     * Redirect to the {@see cancelUrl} or simply close the popup window.
272
     *
273
     * @param string $url URL to redirect.
274
     *
275
     * @throws Throwable
276
     * @throws ViewNotFoundException
277
     *
278
     * @return ResponseInterface response instance.
279
     */
280
    private function redirectCancel(?string $url = null): ResponseInterface
281
    {
282
        if ($url === null) {
283
            $url = $this->cancelUrl;
284
        }
285
        return $this->redirect($url, false);
286
    }
287
288
    /**
289
     * Redirect to the given URL or simply close the popup window.
290
     *
291
     * @param string $url URL to redirect, could be a string or array config to generate a valid URL.
292
     * @param bool $enforceRedirect indicates if redirect should be performed even in case of popup window.
293
     *
294
     * @throws Throwable
295
     * @throws ViewNotFoundException
296
     *
297
     * @return ResponseInterface response instance.
298
     */
299
    private function redirect(string $url, bool $enforceRedirect = true): ResponseInterface
300
    {
301
        $viewFile = $this->redirectView;
302
        if ($viewFile === null) {
0 ignored issues
show
introduced by
The condition $viewFile === null is always false.
Loading history...
303
            $viewFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php';
304
        } else {
305
            $viewFile = $this->aliases->get($viewFile);
306
        }
307
308
        $viewData = [
309
            'url' => $url,
310
            'enforceRedirect' => $enforceRedirect,
311
        ];
312
313
        $response = $this->responseFactory->createResponse();
314
        $response->getBody()->write($this->view->renderFile($viewFile, $viewData));
315
316
        return $response;
317
    }
318
319
    /**
320
     * This method is invoked in case of successful authentication via auth client.
321
     *
322
     * @param AuthClientInterface $client auth client instance.
323
     *
324
     * @throws InvalidConfigException on invalid success callback.
325
     * @throws Throwable
326
     * @throws ViewNotFoundException
327
     *
328
     * @return ResponseInterface response instance.
329
     */
330
    private function authSuccess(AuthClientInterface $client): ResponseInterface
331
    {
332
        if (!is_callable($this->successCallback)) {
333
            throw new InvalidConfigException(
334
                '"' . self::class . '::$successCallback" should be a valid callback.'
335
            );
336
        }
337
338
        $response = ($this->successCallback)($client);
339
        if ($response instanceof ResponseInterface) {
340
            return $response;
341
        }
342
343
        return $this->redirectSuccess();
344
    }
345
346
    /**
347
     * Redirect to the URL. If URL is null, {@see successUrl} will be used.
348
     *
349
     * @param string|null $url URL to redirect.
350
     *
351
     * @throws Throwable
352
     * @throws ViewNotFoundException
353
     *
354
     * @return ResponseInterface response instance.
355
     */
356
    private function redirectSuccess(?string $url = null): ResponseInterface
357
    {
358
        if ($url === null) {
359
            $url = $this->successUrl;
360
        }
361
        return $this->redirect($url);
362
    }
363
364
    /**
365
     * Performs OAuth1 auth flow.
366
     *
367
     * @param OAuth1 $client auth client instance.
368
     * @param ServerRequestInterface $request
369
     *
370
     * @throws InvalidConfigException
371
     * @throws Throwable
372
     * @throws ViewNotFoundException
373
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
374
     *
375
     * @return ResponseInterface action response.
376
     */
377
    private function authOAuth1(OAuth1 $client, ServerRequestInterface $request): ResponseInterface
378
    {
379
        $queryParams = $request->getQueryParams();
380
381
        // user denied error
382
        if (isset($queryParams['denied']) && $queryParams['denied'] !== null) {
383
            return $this->authCancel($client);
384
        }
385
386
        if (($oauthToken = $queryParams['oauth_token'] ?? $request->getParsedBody()['oauth_token'] ?? null) !== null) {
387
            // Upgrade to access token.
388
            $client->fetchAccessToken($request, $oauthToken);
389
            return $this->authSuccess($client);
390
        }
391
392
        // Get request token.
393
        $requestToken = $client->fetchRequestToken($request);
394
        // Get authorization URL.
395
        $url = $client->buildAuthUrl($requestToken);
0 ignored issues
show
Bug introduced by
$requestToken of type Yiisoft\Yii\AuthClient\OAuthToken is incompatible with the type Psr\Http\Message\ServerRequestInterface expected by parameter $incomingRequest of Yiisoft\Yii\AuthClient\OAuth1::buildAuthUrl(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

395
        $url = $client->buildAuthUrl(/** @scrutinizer ignore-type */ $requestToken);
Loading history...
396
        // Redirect to authorization URL.
397
        return $this->responseFactory
398
            ->createResponse(Status::MOVED_PERMANENTLY)
399
            ->withHeader('Location', $url);
400
    }
401
402
    /**
403
     * Performs OpenID auth flow.
404
     *
405
     * @param OpenId $client auth client instance.
406
     * @param ServerRequestInterface $request
407
     *
408
     * @throws InvalidConfigException
409
     * @throws Throwable
410
     * @throws ViewNotFoundException
411
     *
412
     * @return ResponseInterface action response.
413
     */
414
    private function authOpenId(OpenId $client, ServerRequestInterface $request): ResponseInterface
415
    {
416
        $queryParams = $request->getQueryParams();
417
        $bodyParams = $request->getParsedBody();
418
        $mode = $queryParams['openid_mode'] ?? $bodyParams['openid_mode'] ?? null;
419
420
        if (empty($mode)) {
421
            return $this->responseFactory
422
                ->createResponse(Status::MOVED_PERMANENTLY)
423
                ->withHeader('Location', $client->buildAuthUrl($request));
424
        }
425
426
        switch ($mode) {
427
            case 'id_res':
428
                if ($client->validate()) {
429
                    return $this->authSuccess($client);
430
                }
431
                $response = $this->responseFactory->createResponse(Status::BAD_REQUEST);
432
                $response->getBody()->write(
433
                    'Unable to complete the authentication because the required data was not received.'
434
                );
435
                return $response;
436
            case 'cancel':
437
                return $this->authCancel($client);
438
            default:
439
                return $this->responseFactory->createResponse(Status::BAD_REQUEST);
440
        }
441
    }
442
}
443