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

SessionMiddleware::timestamp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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