Completed
Pull Request — master (#59)
by Geert
03:08
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 Interop\Http\ServerMiddleware\DelegateInterface;
26
use Interop\Http\ServerMiddleware\MiddlewareInterface;
27
use Lcobucci\JWT\Builder;
28
use Lcobucci\JWT\Parser;
29
use Lcobucci\JWT\Signer;
30
use Lcobucci\JWT\Token;
31
use Lcobucci\JWT\ValidationData;
32
use Psr\Http\Message\ResponseInterface as Response;
33
use Psr\Http\Message\ServerRequestInterface as Request;
34
use PSR7Sessions\Storageless\Session\DefaultSessionData;
35
use PSR7Sessions\Storageless\Session\LazySession;
36
use PSR7Sessions\Storageless\Session\SessionInterface;
37
use PSR7Sessions\Storageless\Time\CurrentTimeProviderInterface;
38
use PSR7Sessions\Storageless\Time\SystemCurrentTime;
39
40
final class SessionMiddleware implements MiddlewareInterface
41
{
42
    const ISSUED_AT_CLAIM      = 'iat';
43
    const SESSION_CLAIM        = 'session-data';
44
    const SESSION_ATTRIBUTE    = 'session';
45
    const DEFAULT_COOKIE       = 'slsession';
46
    const DEFAULT_REFRESH_TIME = 60;
47
48
    /**
49
     * @var Signer
50
     */
51
    private $signer;
52
53
    /**
54
     * @var string
55
     */
56
    private $signatureKey;
57
58
    /**
59
     * @var string
60
     */
61
    private $verificationKey;
62
63
    /**
64
     * @var int
65
     */
66
    private $expirationTime;
67
68
    /**
69
     * @var int
70
     */
71
    private $refreshTime;
72
73
    /**
74
     * @var Parser
75
     */
76
    private $tokenParser;
77
78
    /**
79
     * @var SetCookie
80
     */
81
    private $defaultCookie;
82
83
    /**
84
     * @var CurrentTimeProviderInterface
85
     */
86
    private $currentTimeProvider;
87
88
    /**
89
     * @param Signer                        $signer
90
     * @param string                        $signatureKey
91
     * @param string                        $verificationKey
92
     * @param SetCookie                     $defaultCookie
93
     * @param Parser                        $tokenParser
94
     * @param int                           $expirationTime
95
     * @param CurrentTimeProviderInterface  $currentTimeProvider
96
     * @param int                           $refreshTime
97
     */
98 10
    public function __construct(
99
        Signer $signer,
100
        string $signatureKey,
101
        string $verificationKey,
102
        SetCookie $defaultCookie,
103
        Parser $tokenParser,
104
        int $expirationTime,
105
        CurrentTimeProviderInterface $currentTimeProvider,
106
        int $refreshTime = self::DEFAULT_REFRESH_TIME
107
    ) {
108 10
        $this->signer              = $signer;
109 10
        $this->signatureKey        = $signatureKey;
110 10
        $this->verificationKey     = $verificationKey;
111 10
        $this->tokenParser         = $tokenParser;
112 10
        $this->defaultCookie       = clone $defaultCookie;
113 10
        $this->expirationTime      = $expirationTime;
114 10
        $this->currentTimeProvider = $currentTimeProvider;
115 10
        $this->refreshTime         = $refreshTime;
116 10
    }
117
118
    /**
119
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and symmetric key encription
120
     *
121
     * @param string $symmetricKey
122
     * @param int    $expirationTime
123
     *
124
     * @return self
125
     */
126 2
    public static function fromSymmetricKeyDefaults(string $symmetricKey, int $expirationTime) : SessionMiddleware
127
    {
128 2
        return new self(
129 2
            new Signer\Hmac\Sha256(),
130
            $symmetricKey,
131
            $symmetricKey,
132 2
            SetCookie::create(self::DEFAULT_COOKIE)
133 2
                ->withSecure(true)
134 2
                ->withHttpOnly(true)
135 2
                ->withPath('/'),
136 2
            new Parser(),
137
            $expirationTime,
138 2
            new SystemCurrentTime()
139
        );
140
    }
141
142
    /**
143
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and asymmetric key encription
144
     * based on RSA keys
145
     *
146
     * @param string $privateRsaKey
147
     * @param string $publicRsaKey
148
     * @param int    $expirationTime
149
     *
150
     * @return self
151
     */
152 2
    public static function fromAsymmetricKeyDefaults(
153
        string $privateRsaKey,
154
        string $publicRsaKey,
155
        int $expirationTime
156
    ) : SessionMiddleware {
157 2
        return new self(
158 2
            new Signer\Rsa\Sha256(),
159
            $privateRsaKey,
160
            $publicRsaKey,
161 2
            SetCookie::create(self::DEFAULT_COOKIE)
162 2
                ->withSecure(true)
163 2
                ->withHttpOnly(true)
164 2
                ->withPath('/'),
165 2
            new Parser(),
166
            $expirationTime,
167 2
            new SystemCurrentTime()
168
        );
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * @throws \InvalidArgumentException
175
     * @throws \OutOfBoundsException
176
     */
177 43
    public function process(Request $request, DelegateInterface $delegate) : Response
178
    {
179 43
        $token            = $this->parseToken($request);
180 43
        $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token) : SessionInterface {
181 43
            return $this->extractSessionContainer($token);
182 43
        });
183
184 43
        $response = $delegate->process($request->withAttribute(self::SESSION_ATTRIBUTE, $sessionContainer));
185
186 43
        return $this->appendToken($sessionContainer, $response, $token);
187
    }
188
189
    /**
190
     * Extract the token from the given request object
191
     *
192
     * @param Request $request
193
     *
194
     * @return Token|null
195
     */
196 43
    private function parseToken(Request $request)
197
    {
198 43
        $cookies    = $request->getCookieParams();
199 43
        $cookieName = $this->defaultCookie->getName();
200
201 43
        if (! isset($cookies[$cookieName])) {
202 24
            return null;
203
        }
204
205
        try {
206 29
            $token = $this->tokenParser->parse($cookies[$cookieName]);
207 3
        } catch (\InvalidArgumentException $invalidToken) {
208 3
            return null;
209
        }
210
211 26
        if (! $token->validate(new ValidationData())) {
212 6
            return null;
213
        }
214
215 20
        return $token;
216
    }
217
218
    /**
219
     * @param Token|null $token
220
     *
221
     * @return SessionInterface
222
     */
223 43
    public function extractSessionContainer(Token $token = null) : SessionInterface
224
    {
225
        try {
226 43
            if (null === $token || ! $token->verify($this->signer, $this->verificationKey)) {
227 33
                return DefaultSessionData::newEmptySession();
228
            }
229
230 15
            return DefaultSessionData::fromDecodedTokenData(
231 15
                (object) $token->getClaim(self::SESSION_CLAIM, new \stdClass())
232
            );
233 4
        } catch (\BadMethodCallException $invalidToken) {
234 4
            return DefaultSessionData::newEmptySession();
235
        }
236
    }
237
238
    /**
239
     * @param SessionInterface $sessionContainer
240
     * @param Response         $response
241
     * @param Token            $token
242
     *
243
     * @return Response
244
     *
245
     * @throws \InvalidArgumentException
246
     */
247 43
    private function appendToken(SessionInterface $sessionContainer, Response $response, Token $token = null) : Response
248
    {
249 43
        $sessionContainerChanged = $sessionContainer->hasChanged();
250 43
        $sessionContainerEmpty   = $sessionContainer->isEmpty();
251
252 43
        if ($sessionContainerChanged && $sessionContainerEmpty) {
253 3
            return FigResponseCookies::set($response, $this->getExpirationCookie());
254
        }
255
256 43
        if ($sessionContainerChanged || (! $sessionContainerEmpty && $token && $this->shouldTokenBeRefreshed($token))) {
257 20
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
258
        }
259
260 27
        return $response;
261
    }
262
263
    /**
264
     * {@inheritDoc}
265
     */
266 9
    private function shouldTokenBeRefreshed(Token $token) : bool
267
    {
268 9
        if (! $token->hasClaim(self::ISSUED_AT_CLAIM)) {
269 3
            return false;
270
        }
271
272 6
        return $this->timestamp() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
273
    }
274
275
    /**
276
     * @param SessionInterface $sessionContainer
277
     *
278
     * @return SetCookie
279
     */
280 20
    private function getTokenCookie(SessionInterface $sessionContainer) : SetCookie
281
    {
282 20
        $timestamp = $this->timestamp();
283
284
        return $this
285 20
            ->defaultCookie
286 20
            ->withValue(
287 20
                (new Builder())
288 20
                    ->setIssuedAt($timestamp)
289 20
                    ->setExpiration($timestamp + $this->expirationTime)
290 20
                    ->set(self::SESSION_CLAIM, $sessionContainer)
291 20
                    ->sign($this->signer, $this->signatureKey)
292 20
                    ->getToken()
293
            )
294 20
            ->withExpires($timestamp + $this->expirationTime);
295
    }
296
297
    /**
298
     * @return SetCookie
299
     */
300 3
    private function getExpirationCookie() : SetCookie
301
    {
302 3
        $currentTimeProvider = $this->currentTimeProvider;
303 3
        $expirationDate      = $currentTimeProvider();
304 3
        $expirationDate      = $expirationDate->modify('-30 days');
305
306
        return $this
307 3
            ->defaultCookie
308 3
            ->withValue(null)
309 3
            ->withExpires($expirationDate->getTimestamp());
310
    }
311
312 21
    private function timestamp() : int
313
    {
314 21
        $currentTimeProvider = $this->currentTimeProvider;
315
316 21
        return $currentTimeProvider->__invoke()->getTimestamp();
317
    }
318
}
319