Completed
Pull Request — master (#1)
by Marco
06:26
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 PSR7Session\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 PSR7Session\Storageless\Time\CurrentTimeProviderInterface;
33
use PSR7Session\Storageless\Time\SystemCurrentTime;
34
use PSR7Session\Storageless\Session\DefaultSessionData;
35
use PSR7Session\Storageless\Session\LazySession;
36
use PSR7Session\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
252 40
        if ($sessionContainerChanged && $sessionContainer->isEmpty()) {
253 3
            return FigResponseCookies::set($response, $this->getExpirationCookie());
254
        }
255
256 40
        if ($sessionContainerChanged || ($this->shouldTokenBeRefreshed($token) && ! $sessionContainer->isEmpty())) {
257 17
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
258
        }
259
260 27
        return $response;
261
    }
262
263
    /**
264
     * {@inheritDoc}
265
     */
266 28
    private function shouldTokenBeRefreshed(Token $token = null) : bool
267
    {
268 28
        if (null === $token) {
269 15
            return false;
270
        }
271
272 13
        if (! $token->hasClaim(self::ISSUED_AT_CLAIM)) {
273 4
            return false;
274
        }
275
276 9
        return $this->timestamp() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
277
    }
278
279
    /**
280
     * @param SessionInterface $sessionContainer
281
     *
282
     * @return SetCookie
283
     */
284 17
    private function getTokenCookie(SessionInterface $sessionContainer) : SetCookie
285
    {
286 17
        $timestamp = $this->timestamp();
287
288
        return $this
289 17
            ->defaultCookie
290 17
            ->withValue(
291 17
                (new Builder())
292 17
                    ->setIssuedAt($timestamp)
293 17
                    ->setExpiration($timestamp + $this->expirationTime)
294 17
                    ->set(self::SESSION_CLAIM, $sessionContainer)
295 17
                    ->sign($this->signer, $this->signatureKey)
296 17
                    ->getToken()
297
            )
298 17
            ->withExpires($timestamp + $this->expirationTime);
299
    }
300
301
    /**
302
     * @return SetCookie
303
     */
304 3
    private function getExpirationCookie() : SetCookie
305
    {
306 3
        $currentTimeProvider = $this->currentTimeProvider;
307 3
        $expirationDate      = $currentTimeProvider();
308 3
        $expirationDate      = $expirationDate->modify('-30 days');
309
310
        return $this
311 3
            ->defaultCookie
312 3
            ->withValue(null)
313 3
            ->withExpires($expirationDate->getTimestamp());
314
    }
315
316 21
    private function timestamp() : int
317
    {
318 21
        $currentTimeProvider = $this->currentTimeProvider;
319
320 21
        return $currentTimeProvider()->getTimestamp();
321
    }
322
}
323