Completed
Pull Request — master (#55)
by Jefersson
02:55
created

SessionMiddleware::appendToken()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 7
cts 7
cp 1
rs 8.2222
c 0
b 0
f 0
cc 7
eloc 8
nc 3
nop 3
crap 7
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