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

CheckAuthenticationMiddleware   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 143
Duplicated Lines 11.19 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 83.64%

Importance

Changes 0
Metric Value
dl 16
loc 143
ccs 46
cts 55
cp 0.8364
rs 10
c 0
b 0
f 0
wmc 15
lcom 1
cbo 9

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
C __invoke() 16 68 12
A createTokenErrorResponse() 0 13 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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