Completed
Push — master ( 7fcad6...e44ede )
by Marco
28:55 queued 22:33
created

SessionMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

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

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 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 encryption
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 encryption
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