Completed
Pull Request — master (#59)
by Geert
03:41
created

SessionMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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
    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
        $this->signer              = $signer;
109
        $this->signatureKey        = $signatureKey;
110
        $this->verificationKey     = $verificationKey;
111
        $this->tokenParser         = $tokenParser;
112
        $this->defaultCookie       = clone $defaultCookie;
113
        $this->expirationTime      = $expirationTime;
114
        $this->currentTimeProvider = $currentTimeProvider;
115
        $this->refreshTime         = $refreshTime;
116
    }
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
    public static function fromSymmetricKeyDefaults(string $symmetricKey, int $expirationTime) : SessionMiddleware
127
    {
128
        return new self(
129
            new Signer\Hmac\Sha256(),
130
            $symmetricKey,
131
            $symmetricKey,
132
            SetCookie::create(self::DEFAULT_COOKIE)
133
                ->withSecure(true)
134
                ->withHttpOnly(true)
135
                ->withPath('/'),
136
            new Parser(),
137
            $expirationTime,
138
            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
    public static function fromAsymmetricKeyDefaults(
153
        string $privateRsaKey,
154
        string $publicRsaKey,
155
        int $expirationTime
156
    ) : SessionMiddleware {
157
        return new self(
158
            new Signer\Rsa\Sha256(),
159
            $privateRsaKey,
160
            $publicRsaKey,
161
            SetCookie::create(self::DEFAULT_COOKIE)
162
                ->withSecure(true)
163
                ->withHttpOnly(true)
164
                ->withPath('/'),
165
            new Parser(),
166
            $expirationTime,
167
            new SystemCurrentTime()
168
        );
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * @throws \InvalidArgumentException
175
     * @throws \OutOfBoundsException
176
     */
177
    public function process(Request $request, DelegateInterface $delegate) : Response
178
    {
179
        $token            = $this->parseToken($request);
180
        $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token) : SessionInterface {
181
            return $this->extractSessionContainer($token);
182
        });
183
184
        $response = $delegate->process($request->withAttribute(self::SESSION_ATTRIBUTE, $sessionContainer));
185
186
        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
    private function parseToken(Request $request)
197
    {
198
        $cookies    = $request->getCookieParams();
199
        $cookieName = $this->defaultCookie->getName();
200
201
        if (! isset($cookies[$cookieName])) {
202
            return null;
203
        }
204
205
        try {
206
            $token = $this->tokenParser->parse($cookies[$cookieName]);
207
        } catch (\InvalidArgumentException $invalidToken) {
208
            return null;
209
        }
210
211
        if (! $token->validate(new ValidationData())) {
212
            return null;
213
        }
214
215
        return $token;
216
    }
217
218
    /**
219
     * @param Token|null $token
220
     *
221
     * @return SessionInterface
222
     */
223
    public function extractSessionContainer(Token $token = null) : SessionInterface
224
    {
225
        try {
226
            if (null === $token || ! $token->verify($this->signer, $this->verificationKey)) {
227
                return DefaultSessionData::newEmptySession();
228
            }
229
230
            return DefaultSessionData::fromDecodedTokenData(
231
                (object) $token->getClaim(self::SESSION_CLAIM, new \stdClass())
232
            );
233
        } catch (\BadMethodCallException $invalidToken) {
234
            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
    private function appendToken(SessionInterface $sessionContainer, Response $response, Token $token = null) : Response
248
    {
249
        $sessionContainerChanged = $sessionContainer->hasChanged();
250
        $sessionContainerEmpty   = $sessionContainer->isEmpty();
251
252
        if ($sessionContainerChanged && $sessionContainerEmpty) {
253
            return FigResponseCookies::set($response, $this->getExpirationCookie());
254
        }
255
256
        if ($sessionContainerChanged || (! $sessionContainerEmpty && $token && $this->shouldTokenBeRefreshed($token))) {
257
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
258
        }
259
260
        return $response;
261
    }
262
263
    /**
264
     * {@inheritDoc}
265
     */
266
    private function shouldTokenBeRefreshed(Token $token) : bool
267
    {
268
        if (! $token->hasClaim(self::ISSUED_AT_CLAIM)) {
269
            return false;
270
        }
271
272
        return $this->timestamp() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
273
    }
274
275
    /**
276
     * @param SessionInterface $sessionContainer
277
     *
278
     * @return SetCookie
279
     */
280
    private function getTokenCookie(SessionInterface $sessionContainer) : SetCookie
281
    {
282
        $timestamp = $this->timestamp();
283
284
        return $this
285
            ->defaultCookie
286
            ->withValue(
287
                (new Builder())
288
                    ->setIssuedAt($timestamp)
289
                    ->setExpiration($timestamp + $this->expirationTime)
290
                    ->set(self::SESSION_CLAIM, $sessionContainer)
291
                    ->sign($this->signer, $this->signatureKey)
292
                    ->getToken()
293
            )
294
            ->withExpires($timestamp + $this->expirationTime);
295
    }
296
297
    /**
298
     * @return SetCookie
299
     */
300
    private function getExpirationCookie() : SetCookie
301
    {
302
        $currentTimeProvider = $this->currentTimeProvider;
303
        $expirationDate      = $currentTimeProvider();
304
        $expirationDate      = $expirationDate->modify('-30 days');
305
306
        return $this
307
            ->defaultCookie
308
            ->withValue(null)
309
            ->withExpires($expirationDate->getTimestamp());
310
    }
311
312
    private function timestamp() : int
313
    {
314
        $currentTimeProvider = $this->currentTimeProvider;
315
316
        return $currentTimeProvider->__invoke()->getTimestamp();
317
    }
318
}
319