Completed
Push — master ( 48acde...f7424d )
by Alejandro
07:34
created

CheckAuthenticationMiddleware::__invoke()   C

Complexity

Conditions 12
Paths 76

Size

Total Lines 68
Code Lines 45

Duplication

Lines 16
Ratio 23.53 %

Code Coverage

Tests 33
CRAP Score 13.4171

Importance

Changes 0
Metric Value
cc 12
eloc 45
nc 76
nop 3
dl 16
loc 68
ccs 33
cts 42
cp 0.7856
crap 13.4171
rs 5.7751
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Shlinkio\Shlink\Rest\Middleware;
3
4
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
5
use Psr\Http\Message\ResponseInterface as Response;
6
use Psr\Http\Message\ServerRequestInterface as Request;
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\NullLogger;
9
use Shlinkio\Shlink\Rest\Authentication\JWTService;
10
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
11
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
12
use Shlinkio\Shlink\Rest\Util\RestUtils;
13
use Zend\Diactoros\Response\JsonResponse;
14
use Zend\Expressive\Router\RouteResult;
15
use Zend\I18n\Translator\TranslatorInterface;
16
use Zend\Stdlib\ErrorHandler;
17
use Zend\Stratigility\MiddlewareInterface;
18
19
class CheckAuthenticationMiddleware implements MiddlewareInterface
20
{
21
    const AUTHORIZATION_HEADER = 'Authorization';
22
23
    /**
24
     * @var TranslatorInterface
25
     */
26
    private $translator;
27
    /**
28
     * @var JWTServiceInterface
29
     */
30
    private $jwtService;
31
    /**
32
     * @var LoggerInterface
33
     */
34
    private $logger;
35
36
    /**
37
     * CheckAuthenticationMiddleware constructor.
38
     * @param JWTServiceInterface|JWTService $jwtService
39
     * @param TranslatorInterface $translator
40
     * @param LoggerInterface $logger
41
     *
42
     * @Inject({JWTService::class, "translator", "Logger_Shlink"})
43
     */
44 6
    public function __construct(
45
        JWTServiceInterface $jwtService,
46
        TranslatorInterface $translator,
47
        LoggerInterface $logger = null
48
    ) {
49 6
        $this->translator = $translator;
50 6
        $this->jwtService = $jwtService;
51 6
        $this->logger = $logger ?: new NullLogger();
52 6
    }
53
54
    /**
55
     * Process an incoming request and/or response.
56
     *
57
     * Accepts a server-side request and a response instance, and does
58
     * something with them.
59
     *
60
     * If the response is not complete and/or further processing would not
61
     * interfere with the work done in the middleware, or if the middleware
62
     * wants to delegate to another process, it can use the `$out` callable
63
     * if present.
64
     *
65
     * If the middleware does not return a value, execution of the current
66
     * request is considered complete, and the response instance provided will
67
     * be considered the response to return.
68
     *
69
     * Alternately, the middleware may return a response instance.
70
     *
71
     * Often, middleware will `return $out();`, with the assumption that a
72
     * later middleware will return a response.
73
     *
74
     * @param Request $request
75
     * @param Response $response
76
     * @param null|callable $out
77
     * @return null|Response
78
     */
79 6
    public function __invoke(Request $request, Response $response, callable $out = null)
80
    {
81
        // If current route is the authenticate route or an OPTIONS request, continue to the next middleware
82
        /** @var RouteResult $routeResult */
83 6
        $routeResult = $request->getAttribute(RouteResult::class);
84 6
        if (! isset($routeResult)
85 6
            || $routeResult->isFailure()
86 6
            || $routeResult->getMatchedRouteName() === 'rest-authenticate'
87 6
            || $request->getMethod() === 'OPTIONS'
88
        ) {
89 1
            return $out($request, $response);
90
        }
91
92
        // Check that the auth header was provided, and that it belongs to a non-expired token
93 5
        if (! $request->hasHeader(self::AUTHORIZATION_HEADER)) {
94 1
            return $this->createTokenErrorResponse();
95
        }
96
97
        // Get token making sure the an authorization type is provided
98 4
        $authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER);
99 4
        $authTokenParts = explode(' ', $authToken);
100 4 View Code Duplication
        if (count($authTokenParts) === 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101 1
            return new JsonResponse([
102 1
                'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
103 1
                'message' => sprintf($this->translator->translate(
104 1
                    'You need to provide the Bearer type in the %s header.'
105 1
                ), self::AUTHORIZATION_HEADER),
106 1
            ], 401);
107
        }
108
109
        // Make sure the authorization type is Bearer
110 3
        list($authType, $jwt) = $authTokenParts;
111 3 View Code Duplication
        if (strtolower($authType) !== 'bearer') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
112 1
            return new JsonResponse([
113 1
                'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
114 1
                'message' => sprintf($this->translator->translate(
115 1
                    'Provided authorization type %s is not supported. Use Bearer instead.'
116
                ), $authType),
117 1
            ], 401);
118
        }
119
120
        try {
121 2
            ErrorHandler::start();
122 2
            if (! $this->jwtService->verify($jwt)) {
123 1
                return $this->createTokenErrorResponse();
124
            }
125 1
            ErrorHandler::stop(true);
126
127
            // Update the token expiration and continue to next middleware
128 1
            $jwt = $this->jwtService->refresh($jwt);
129
            /** @var Response $response */
130 1
            $response = $out($request, $response);
131
132
            // Return the response with the updated token on it
133 1
            return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt);
134
        } catch (AuthenticationException $e) {
135
            $this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e);
136
            return $this->createTokenErrorResponse();
137
        } catch (\Exception $e) {
138
            $this->logger->warning('Unexpected error occurred.' . PHP_EOL . $e);
139
            return $this->createTokenErrorResponse();
140
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
141
            $this->logger->warning('Unexpected error occurred.' . PHP_EOL . $e);
142
            return $this->createTokenErrorResponse();
143
        } finally {
144 2
            ErrorHandler::clean();
145
        }
146
    }
147
148 2
    protected function createTokenErrorResponse()
149
    {
150 2
        return new JsonResponse([
151 2
            'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
152 2
            'message' => sprintf(
153 2
                $this->translator->translate(
154
                    'Missing or invalid auth token provided. Perform a new authentication request and send provided '
155 2
                    . 'token on every new request on the "%s" header'
156
                ),
157 2
                self::AUTHORIZATION_HEADER
158
            ),
159 2
        ], 401);
160
    }
161
}
162