Completed
Pull Request — master (#75)
by Marco
40:03 queued 36:09
created

SessionMiddleware::getExpirationCookie()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
crap 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 Lcobucci\Clock\Clock;
26
use Lcobucci\Clock\SystemClock;
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 Psr\Http\Server\MiddlewareInterface;
35
use Psr\Http\Server\RequestHandlerInterface;
36
use PSR7Sessions\Storageless\Session\DefaultSessionData;
37
use PSR7Sessions\Storageless\Session\LazySession;
38
use PSR7Sessions\Storageless\Session\SessionInterface;
39
40
final class SessionMiddleware implements MiddlewareInterface
41
{
42
    public const ISSUED_AT_CLAIM      = 'iat';
43
    public const SESSION_CLAIM        = 'session-data';
44
    public const SESSION_ATTRIBUTE    = 'session';
45
    public const DEFAULT_COOKIE       = 'slsession';
46
    public 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 Clock
85
     */
86
    private $clock;
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 Clock     $clock
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
        Clock $clock,
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->clock           = $clock;
115 10
        $this->refreshTime     = $refreshTime;
116 10
    }
117
118
    /**
119
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and symmetric key encription
120
     */
121 2
    public static function fromSymmetricKeyDefaults(string $symmetricKey, int $expirationTime) : self
122
    {
123 2
        return new self(
124 2
            new Signer\Hmac\Sha256(),
125 2
            $symmetricKey,
126 2
            $symmetricKey,
127 2
            SetCookie::create(self::DEFAULT_COOKIE)
128 2
                ->withSecure(true)
129 2
                ->withHttpOnly(true)
130 2
                ->withPath('/'),
131 2
            new Parser(),
132 2
            $expirationTime,
133 2
            new SystemClock()
134
        );
135
    }
136
137
    /**
138
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and asymmetric key encription
139
     * based on RSA keys
140
     */
141 2
    public static function fromAsymmetricKeyDefaults(
142
        string $privateRsaKey,
143
        string $publicRsaKey,
144
        int $expirationTime
145
    ) : self {
146 2
        return new self(
147 2
            new Signer\Rsa\Sha256(),
148 2
            $privateRsaKey,
149 2
            $publicRsaKey,
150 2
            SetCookie::create(self::DEFAULT_COOKIE)
151 2
                ->withSecure(true)
152 2
                ->withHttpOnly(true)
153 2
                ->withPath('/'),
154 2
            new Parser(),
155 2
            $expirationTime,
156 2
            new SystemClock()
157
        );
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     *
163
     * @throws \BadMethodCallException
164
     * @throws \InvalidArgumentException
165
     */
166 43
    public function process(Request $request, RequestHandlerInterface $delegate) : Response
167
    {
168 43
        $token            = $this->parseToken($request);
169 43
        $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token) : SessionInterface {
170 38
            return $this->extractSessionContainer($token);
171 43
        });
172
173 43
        return $this->appendToken(
174 43
            $sessionContainer,
175 43
            $delegate->handle($request->withAttribute(self::SESSION_ATTRIBUTE, $sessionContainer)),
176 43
            $token
177
        );
178
    }
179
180
    /**
181
     * Extract the token from the given request object
182
     */
183 43
    private function parseToken(Request $request) : ?Token
184
    {
185 43
        $cookies    = $request->getCookieParams();
186 43
        $cookieName = $this->defaultCookie->getName();
187
188 43
        if (! isset($cookies[$cookieName])) {
189 24
            return null;
190
        }
191
192
        try {
193 29
            $token = $this->tokenParser->parse($cookies[$cookieName]);
194 3
        } catch (\InvalidArgumentException $invalidToken) {
195 3
            return null;
196
        }
197
198 26
        if (! $token->validate(new ValidationData())) {
199 6
            return null;
200
        }
201
202 20
        return $token;
203
    }
204
205
    /**
206
     * @throws \OutOfBoundsException
207
     */
208 38
    private function extractSessionContainer(?Token $token) : SessionInterface
209
    {
210
        try {
211 38
            if (null === $token || ! $token->verify($this->signer, $this->verificationKey)) {
212 33
                return DefaultSessionData::newEmptySession();
213
            }
214
215 11
            return DefaultSessionData::fromDecodedTokenData(
216 11
                (object) $token->getClaim(self::SESSION_CLAIM, new \stdClass())
217
            );
218 3
        } catch (\BadMethodCallException $invalidToken) {
219 3
            return DefaultSessionData::newEmptySession();
220
        }
221
    }
222
223
    /**
224
     * @throws \BadMethodCallException
225
     * @throws \InvalidArgumentException
226
     */
227 43
    private function appendToken(SessionInterface $sessionContainer, Response $response, ?Token $token) : Response
228
    {
229 43
        $sessionContainerChanged = $sessionContainer->hasChanged();
230
231 43
        if ($sessionContainerChanged && $sessionContainer->isEmpty()) {
232 3
            return FigResponseCookies::set($response, $this->getExpirationCookie());
233
        }
234
235 43
        if ($sessionContainerChanged || ($this->shouldTokenBeRefreshed($token) && ! $sessionContainer->isEmpty())) {
236 20
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
237
        }
238
239 27
        return $response;
240
    }
241
242 29
    private function shouldTokenBeRefreshed(?Token $token) : bool
243
    {
244 29
        if (! $token || ! $token->hasClaim(self::ISSUED_AT_CLAIM)) {
245 18
            return false;
246
        }
247
248 11
        return $this->timestamp() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
249
    }
250
251
    /**
252
     * @throws \BadMethodCallException
253
     */
254 20
    private function getTokenCookie(SessionInterface $sessionContainer) : SetCookie
255
    {
256 20
        $timestamp = $this->timestamp();
257
258
        return $this
259 20
            ->defaultCookie
260 20
            ->withValue(
261 20
                (new Builder())
262 20
                    ->setIssuedAt($timestamp)
263 20
                    ->setExpiration($timestamp + $this->expirationTime)
264 20
                    ->set(self::SESSION_CLAIM, $sessionContainer)
265 20
                    ->sign($this->signer, $this->signatureKey)
266 20
                    ->getToken()
267
            )
268 20
            ->withExpires($timestamp + $this->expirationTime);
269
    }
270
271 3
    private function getExpirationCookie() : SetCookie
272
    {
273 3
        $expirationDate = $this->clock->now()->modify('-30 days');
274
275
        return $this
276 3
            ->defaultCookie
277 3
            ->withValue(null)
278 3
            ->withExpires($expirationDate->getTimestamp());
279
    }
280
281 25
    private function timestamp() : int
282
    {
283 25
        return $this->clock->now()->getTimestamp();
284
    }
285
}
286