Completed
Push — master ( e05cd4...bb1ac9 )
by Luís
10s
created

SessionMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 8
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license.
17
 */
18
19
declare(strict_types=1);
20
21
namespace PSR7Sessions\Storageless\Http;
22
23
use Dflydev\FigCookies\FigResponseCookies;
24
use Dflydev\FigCookies\SetCookie;
25
use Lcobucci\JWT\Builder;
26
use Lcobucci\JWT\Parser;
27
use Lcobucci\JWT\Signer;
28
use Lcobucci\JWT\Token;
29
use Lcobucci\JWT\ValidationData;
30
use Psr\Http\Message\ResponseInterface as Response;
31
use Psr\Http\Message\ServerRequestInterface as Request;
32
use PSR7Sessions\Storageless\Time\CurrentTimeProviderInterface;
33
use PSR7Sessions\Storageless\Time\SystemCurrentTime;
34
use PSR7Sessions\Storageless\Session\DefaultSessionData;
35
use PSR7Sessions\Storageless\Session\LazySession;
36
use PSR7Sessions\Storageless\Session\SessionInterface;
37
use Zend\Stratigility\MiddlewareInterface;
38
39
final class SessionMiddleware implements MiddlewareInterface
40
{
41
    const ISSUED_AT_CLAIM      = 'iat';
42
    const SESSION_CLAIM        = 'session-data';
43
    const SESSION_ATTRIBUTE    = 'session';
44
    const DEFAULT_COOKIE       = 'slsession';
45
    const DEFAULT_REFRESH_TIME = 60;
46
47
    /**
48
     * @var Signer
49
     */
50
    private $signer;
51
52
    /**
53
     * @var string
54
     */
55
    private $signatureKey;
56
57
    /**
58
     * @var string
59
     */
60
    private $verificationKey;
61
62
    /**
63
     * @var int
64
     */
65
    private $expirationTime;
66
67
    /**
68
     * @var int
69
     */
70
    private $refreshTime;
71
72
    /**
73
     * @var Parser
74
     */
75
    private $tokenParser;
76
77
    /**
78
     * @var SetCookie
79
     */
80
    private $defaultCookie;
81
82
    /**
83
     * @var CurrentTimeProviderInterface
84
     */
85
    private $currentTimeProvider;
86
87
    /**
88
     * @param Signer                        $signer
89
     * @param string                        $signatureKey
90
     * @param string                        $verificationKey
91
     * @param SetCookie                     $defaultCookie
92
     * @param Parser                        $tokenParser
93
     * @param int                           $expirationTime
94
     * @param CurrentTimeProviderInterface  $currentTimeProvider
95
     * @param int                           $refreshTime
96
     */
97 7
    public function __construct(
98
        Signer $signer,
99
        string $signatureKey,
100
        string $verificationKey,
101
        SetCookie $defaultCookie,
102
        Parser $tokenParser,
103
        int $expirationTime,
104
        CurrentTimeProviderInterface $currentTimeProvider,
105
        int $refreshTime = self::DEFAULT_REFRESH_TIME
106
    ) {
107 7
        $this->signer              = $signer;
108 7
        $this->signatureKey        = $signatureKey;
109 7
        $this->verificationKey     = $verificationKey;
110 7
        $this->tokenParser         = $tokenParser;
111 7
        $this->defaultCookie       = clone $defaultCookie;
112 7
        $this->expirationTime      = $expirationTime;
113 7
        $this->currentTimeProvider = $currentTimeProvider;
114 7
        $this->refreshTime         = $refreshTime;
115 7
    }
116
117
    /**
118
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and symmetric key encription
119
     *
120
     * @param string $symmetricKey
121
     * @param int    $expirationTime
122
     *
123
     * @return self
124
     */
125 1
    public static function fromSymmetricKeyDefaults(string $symmetricKey, int $expirationTime) : SessionMiddleware
126
    {
127 1
        return new self(
128 1
            new Signer\Hmac\Sha256(),
129
            $symmetricKey,
130
            $symmetricKey,
131 1
            SetCookie::create(self::DEFAULT_COOKIE)
132 1
                ->withSecure(true)
133 1
                ->withHttpOnly(true)
134 1
                ->withPath('/'),
135 1
            new Parser(),
136
            $expirationTime,
137 1
            new SystemCurrentTime()
138
        );
139
    }
140
141
    /**
142
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and asymmetric key encription
143
     * based on RSA keys
144
     *
145
     * @param string $privateRsaKey
146
     * @param string $publicRsaKey
147
     * @param int    $expirationTime
148
     *
149
     * @return self
150
     */
151 1
    public static function fromAsymmetricKeyDefaults(
152
        string $privateRsaKey,
153
        string $publicRsaKey,
154
        int $expirationTime
155
    ) : SessionMiddleware {
156 1
        return new self(
157 1
            new Signer\Rsa\Sha256(),
158
            $privateRsaKey,
159
            $publicRsaKey,
160 1
            SetCookie::create(self::DEFAULT_COOKIE)
161 1
                ->withSecure(true)
162 1
                ->withHttpOnly(true)
163 1
                ->withPath('/'),
164 1
            new Parser(),
165
            $expirationTime,
166 1
            new SystemCurrentTime()
167
        );
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     *
173
     * @throws \InvalidArgumentException
174
     * @throws \OutOfBoundsException
175
     */
176 40
    public function __invoke(Request $request, Response $response, callable $out = null) : Response
177
    {
178 40
        $token            = $this->parseToken($request);
179 40
        $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token) : SessionInterface {
180 38
            return $this->extractSessionContainer($token);
181 40
        });
182
183 40
        if (null !== $out) {
184 38
            $response = $out($request->withAttribute(self::SESSION_ATTRIBUTE, $sessionContainer), $response);
185
        }
186
187 40
        return $this->appendToken($sessionContainer, $response, $token);
188
    }
189
190
    /**
191
     * Extract the token from the given request object
192
     *
193
     * @param Request $request
194
     *
195
     * @return Token|null
196
     */
197 40
    private function parseToken(Request $request)
198
    {
199 40
        $cookies    = $request->getCookieParams();
200 40
        $cookieName = $this->defaultCookie->getName();
201
202 40
        if (! isset($cookies[$cookieName])) {
203 22
            return null;
204
        }
205
206
        try {
207 28
            $token = $this->tokenParser->parse($cookies[$cookieName]);
208 3
        } catch (\InvalidArgumentException $invalidToken) {
209 3
            return null;
210
        }
211
212 25
        if (! $token->validate(new ValidationData())) {
213 6
            return null;
214
        }
215
216 19
        return $token;
217
    }
218
219
    /**
220
     * @param Token|null $token
221
     *
222
     * @return SessionInterface
223
     */
224 38
    public function extractSessionContainer(Token $token = null) : SessionInterface
225
    {
226
        try {
227 38
            if (null === $token || ! $token->verify($this->signer, $this->verificationKey)) {
228 31
                return DefaultSessionData::newEmptySession();
229
            }
230
231 13
            return DefaultSessionData::fromDecodedTokenData(
232 13
                (object) $token->getClaim(self::SESSION_CLAIM, new \stdClass())
233
            );
234 3
        } catch (\BadMethodCallException $invalidToken) {
235 3
            return DefaultSessionData::newEmptySession();
236
        }
237
    }
238
239
    /**
240
     * @param SessionInterface $sessionContainer
241
     * @param Response         $response
242
     * @param Token            $token
243
     *
244
     * @return Response
245
     *
246
     * @throws \InvalidArgumentException
247
     */
248 40
    private function appendToken(SessionInterface $sessionContainer, Response $response, Token $token = null) : Response
249
    {
250 40
        $sessionContainerChanged = $sessionContainer->hasChanged();
251
        $sessionContainerEmpty   = $sessionContainer->isEmpty();
252 40
253 3
        if ($sessionContainerChanged && $sessionContainerEmpty) {
254
            return FigResponseCookies::set($response, $this->getExpirationCookie());
255
        }
256 40
257 17
        if ($sessionContainerChanged || (! $sessionContainerEmpty && $token && $this->shouldTokenBeRefreshed($token))) {
258
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
259
        }
260 27
261
        return $response;
262
    }
263
264
    /**
265
     * {@inheritDoc}
266 28
     */
267
    private function shouldTokenBeRefreshed(Token $token) : bool
268 28
    {
269 15
        if (! $token->hasClaim(self::ISSUED_AT_CLAIM)) {
270
            return false;
271
        }
272 13
273 4
        return $this->timestamp() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
274
    }
275
276 9
    /**
277
     * @param SessionInterface $sessionContainer
278
     *
279
     * @return SetCookie
280
     */
281
    private function getTokenCookie(SessionInterface $sessionContainer) : SetCookie
282
    {
283
        $timestamp = $this->timestamp();
284 17
285
        return $this
286 17
            ->defaultCookie
287
            ->withValue(
288
                (new Builder())
289 17
                    ->setIssuedAt($timestamp)
290 17
                    ->setExpiration($timestamp + $this->expirationTime)
291 17
                    ->set(self::SESSION_CLAIM, $sessionContainer)
292 17
                    ->sign($this->signer, $this->signatureKey)
293 17
                    ->getToken()
294 17
            )
295 17
            ->withExpires($timestamp + $this->expirationTime);
296 17
    }
297
298 17
    /**
299
     * @return SetCookie
300
     */
301
    private function getExpirationCookie() : SetCookie
302
    {
303
        $currentTimeProvider = $this->currentTimeProvider;
304 3
        $expirationDate      = $currentTimeProvider();
305
        $expirationDate      = $expirationDate->modify('-30 days');
306 3
307 3
        return $this
308 3
            ->defaultCookie
309
            ->withValue(null)
310
            ->withExpires($expirationDate->getTimestamp());
311 3
    }
312 3
313 3
    private function timestamp() : int
314
    {
315
        $currentTimeProvider = $this->currentTimeProvider;
316 21
317
        return $currentTimeProvider->__invoke()->getTimestamp();
318 21
    }
319
}
320