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

SessionMiddleware   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 24
lcom 1
cbo 15
dl 0
loc 279
ccs 86
cts 86
cp 1
rs 9.1666
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
A fromSymmetricKeyDefaults() 0 15 1
A fromAsymmetricKeyDefaults() 0 18 1
A process() 0 11 1
A parseToken() 0 21 4
A extractSessionContainer() 0 14 4
B appendToken() 0 15 7
A shouldTokenBeRefreshed() 0 8 2
A getTokenCookie() 0 16 1
A getExpirationCookie() 0 11 1
A timestamp() 0 6 1
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