Completed
Push — master ( 9ab4b9...065cdd )
by Alejandro
09:44
created

CheckAuthenticationMiddleware::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 2
Metric Value
cc 2
eloc 7
c 4
b 0
f 2
nc 2
nop 3
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 9.6666
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\Stratigility\MiddlewareInterface;
17
18
class CheckAuthenticationMiddleware implements MiddlewareInterface
19
{
20
    const AUTHORIZATION_HEADER = 'Authorization';
21
22
    /**
23
     * @var TranslatorInterface
24
     */
25
    private $translator;
26
    /**
27
     * @var JWTServiceInterface
28
     */
29
    private $jwtService;
30
    /**
31
     * @var LoggerInterface
32
     */
33
    private $logger;
34
35
    /**
36
     * CheckAuthenticationMiddleware constructor.
37
     * @param JWTServiceInterface|JWTService $jwtService
38
     * @param TranslatorInterface $translator
39
     * @param LoggerInterface $logger
40
     *
41
     * @Inject({JWTService::class, "translator", "Logger_Shlink"})
42
     */
43 6
    public function __construct(
44
        JWTServiceInterface $jwtService,
45
        TranslatorInterface $translator,
46
        LoggerInterface $logger = null
47
    ) {
48 6
        $this->translator = $translator;
49 6
        $this->jwtService = $jwtService;
50 6
        $this->logger = $logger ?: new NullLogger();
51 6
    }
52
53
    /**
54
     * Process an incoming request and/or response.
55
     *
56
     * Accepts a server-side request and a response instance, and does
57
     * something with them.
58
     *
59
     * If the response is not complete and/or further processing would not
60
     * interfere with the work done in the middleware, or if the middleware
61
     * wants to delegate to another process, it can use the `$out` callable
62
     * if present.
63
     *
64
     * If the middleware does not return a value, execution of the current
65
     * request is considered complete, and the response instance provided will
66
     * be considered the response to return.
67
     *
68
     * Alternately, the middleware may return a response instance.
69
     *
70
     * Often, middleware will `return $out();`, with the assumption that a
71
     * later middleware will return a response.
72
     *
73
     * @param Request $request
74
     * @param Response $response
75
     * @param null|callable $out
76
     * @return null|Response
77
     */
78 6
    public function __invoke(Request $request, Response $response, callable $out = null)
79
    {
80
        // If current route is the authenticate route or an OPTIONS request, continue to the next middleware
81
        /** @var RouteResult $routeResult */
82 6
        $routeResult = $request->getAttribute(RouteResult::class);
83 6
        if (! isset($routeResult)
84 6
            || $routeResult->isFailure()
85 6
            || $routeResult->getMatchedRouteName() === 'rest-authenticate'
86 6
            || $request->getMethod() === 'OPTIONS'
87 6
        ) {
88 1
            return $out($request, $response);
89
        }
90
91
        // Check that the auth header was provided, and that it belongs to a non-expired token
92 5
        if (! $request->hasHeader(self::AUTHORIZATION_HEADER)) {
93 1
            return $this->createTokenErrorResponse();
94
        }
95
96
        // Get token making sure the an authorization type is provided
97 4
        $authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER);
98 4
        $authTokenParts = explode(' ', $authToken);
99 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...
100 1
            return new JsonResponse([
101 1
                'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
102 1
                'message' => sprintf($this->translator->translate(
103
                    'You need to provide the Bearer type in the %s header.'
104 1
                ), self::AUTHORIZATION_HEADER),
105 1
            ], 401);
106
        }
107
108
        // Make sure the authorization type is Bearer
109 3
        list($authType, $jwt) = $authTokenParts;
110 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...
111 1
            return new JsonResponse([
112 1
                'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
113 1
                'message' => sprintf($this->translator->translate(
114
                    'Provided authorization type %s is not supported. Use Bearer instead.'
115 1
                ), $authType),
116 1
            ], 401);
117
        }
118
119
        try {
120 2
            if (! $this->jwtService->verify($jwt)) {
121 1
                return $this->createTokenErrorResponse();
122
            }
123
124
            // Update the token expiration and continue to next middleware
125 1
            $jwt = $this->jwtService->refresh($jwt);
126
            /** @var Response $response */
127 1
            $response = $out($request, $response);
128
129
            // Return the response with the updated token on it
130 1
            return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt);
131
        } catch (AuthenticationException $e) {
132
            $this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e);
133
            return $this->createTokenErrorResponse();
134
        }
135
    }
136
137 2
    protected function createTokenErrorResponse()
138
    {
139 2
        return new JsonResponse([
140 2
            'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
141 2
            'message' => sprintf(
142 2
                $this->translator->translate(
143
                    'Missing or invalid auth token provided. Perform a new authentication request and send provided '
144
                    . 'token on every new request on the "%s" header'
145 2
                ),
146
                self::AUTHORIZATION_HEADER
147 2
            ),
148 2
        ], 401);
149
    }
150
}
151