Completed
Pull Request — master (#106)
by Marco
02:15 queued 24s
created

SessionMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 0
cts 0
cp 0
rs 9.6333
c 0
b 0
f 0
cc 1
nc 1
nop 8
crap 2

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 BadMethodCallException;
24
use Dflydev\FigCookies\FigResponseCookies;
25
use Dflydev\FigCookies\Modifier\SameSite;
26
use Dflydev\FigCookies\SetCookie;
27
use InvalidArgumentException;
28
use Lcobucci\Clock\Clock;
29
use Lcobucci\Clock\SystemClock;
30
use Lcobucci\JWT\Builder;
31
use Lcobucci\JWT\Parser;
32
use Lcobucci\JWT\Signer;
33
use Lcobucci\JWT\Token;
34
use Lcobucci\JWT\ValidationData;
35
use OutOfBoundsException;
36
use Psr\Http\Message\ResponseInterface as Response;
37
use Psr\Http\Message\ServerRequestInterface as Request;
38
use Psr\Http\Server\MiddlewareInterface;
39
use Psr\Http\Server\RequestHandlerInterface;
40
use PSR7Sessions\Storageless\Session\DefaultSessionData;
41
use PSR7Sessions\Storageless\Session\LazySession;
42
use PSR7Sessions\Storageless\Session\SessionInterface;
43
use stdClass;
44
45
final class SessionMiddleware implements MiddlewareInterface
46
{
47
    public const ISSUED_AT_CLAIM      = 'iat';
48
    public const SESSION_CLAIM        = 'session-data';
49
    public const SESSION_ATTRIBUTE    = 'session';
50
    public const DEFAULT_COOKIE       = 'slsession';
51
    public const DEFAULT_REFRESH_TIME = 60;
52
53
    private Signer $signer;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
54
55
    private string $signatureKey;
56
57
    private string $verificationKey;
58
59
    private int $expirationTime;
60
61
    private int $refreshTime;
62
63
    private Parser $tokenParser;
64
65
    private SetCookie $defaultCookie;
66
67
    private Clock $clock;
68
69 12
    public function __construct(
70
        Signer $signer,
71
        string $signatureKey,
72
        string $verificationKey,
73
        SetCookie $defaultCookie,
74
        Parser $tokenParser,
75
        int $expirationTime,
76
        Clock $clock,
77
        int $refreshTime = self::DEFAULT_REFRESH_TIME
78
    ) {
79 12
        $this->signer          = $signer;
80 12
        $this->signatureKey    = $signatureKey;
81 12
        $this->verificationKey = $verificationKey;
82 12
        $this->tokenParser     = $tokenParser;
83 12
        $this->defaultCookie   = clone $defaultCookie;
84 12
        $this->expirationTime  = $expirationTime;
85 12
        $this->clock           = $clock;
86 12
        $this->refreshTime     = $refreshTime;
87 12
    }
88
89
    /**
90
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and symmetric key encryption
91
     */
92 3
    public static function fromSymmetricKeyDefaults(string $symmetricKey, int $expirationTime) : self
93
    {
94 3
        return new self(
95 3
            new Signer\Hmac\Sha256(),
96
            $symmetricKey,
97
            $symmetricKey,
98 3
            SetCookie::create(self::DEFAULT_COOKIE)
99 3
                ->withSecure(true)
100 3
                ->withHttpOnly(true)
101 3
                ->withSameSite(SameSite::lax())
102 3
                ->withPath('/'),
103 3
            new Parser(),
104
            $expirationTime,
105 3
            new SystemClock()
106
        );
107
    }
108
109
    /**
110
     * This constructor simplifies instantiation when using HTTPS (REQUIRED!) and asymmetric key encryption
111
     * based on RSA keys
112
     */
113 3
    public static function fromAsymmetricKeyDefaults(
114
        string $privateRsaKey,
115
        string $publicRsaKey,
116
        int $expirationTime
117
    ) : self {
118 3
        return new self(
119 3
            new Signer\Rsa\Sha256(),
120
            $privateRsaKey,
121
            $publicRsaKey,
122 3
            SetCookie::create(self::DEFAULT_COOKIE)
123 3
                ->withSecure(true)
124 3
                ->withHttpOnly(true)
125 3
                ->withSameSite(SameSite::lax())
126 3
                ->withPath('/'),
127 3
            new Parser(),
128
            $expirationTime,
129 3
            new SystemClock()
130
        );
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     *
136
     * @throws BadMethodCallException
137
     * @throws InvalidArgumentException
138
     */
139 48
    public function process(Request $request, RequestHandlerInterface $delegate) : Response
140
    {
141 48
        $token            = $this->parseToken($request);
142
        $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token) : SessionInterface {
143 43
            return $this->extractSessionContainer($token);
144 48
        });
145
146 48
        return $this->appendToken(
147 48
            $sessionContainer,
148 48
            $delegate->handle($request->withAttribute(self::SESSION_ATTRIBUTE, $sessionContainer)),
149
            $token
150
        );
151
    }
152
153
    /**
154
     * Extract the token from the given request object
155
     */
156 48
    private function parseToken(Request $request) : ?Token
157
    {
158 48
        $cookies    = $request->getCookieParams();
159 48
        $cookieName = $this->defaultCookie->getName();
160
161 48
        if (! isset($cookies[$cookieName])) {
162 26
            return null;
163
        }
164
165
        try {
166 32
            $token = $this->tokenParser->parse($cookies[$cookieName]);
167 3
        } catch (InvalidArgumentException $invalidToken) {
168 3
            return null;
169
        }
170
171 29
        if (! $token->validate(new ValidationData())) {
172 6
            return null;
173
        }
174
175 23
        return $token;
176
    }
177
178
    /**
179
     * @throws OutOfBoundsException
180
     */
181 43
    private function extractSessionContainer(?Token $token) : SessionInterface
182
    {
183 43
        if (! $token) {
184 35
            return DefaultSessionData::newEmptySession();
185
        }
186
187
        try {
188 18
            if (! $token->verify($this->signer, $this->verificationKey)) {
189 1
                return DefaultSessionData::newEmptySession();
190
            }
191
192 14
            return DefaultSessionData::fromDecodedTokenData(
193 14
                (object) $token->getClaim(self::SESSION_CLAIM, new stdClass())
194
            );
195 3
        } catch (BadMethodCallException $invalidToken) {
196 3
            return DefaultSessionData::newEmptySession();
197
        }
198
    }
199
200
    /**
201
     * @throws BadMethodCallException
202
     * @throws InvalidArgumentException
203
     */
204 48
    private function appendToken(SessionInterface $sessionContainer, Response $response, ?Token $token) : Response
205
    {
206 48
        $sessionContainerChanged = $sessionContainer->hasChanged();
207
208 48
        if ($sessionContainerChanged && $sessionContainer->isEmpty()) {
209 3
            return FigResponseCookies::set($response, $this->getExpirationCookie());
210
        }
211
212 48
        if ($sessionContainerChanged || ($this->shouldTokenBeRefreshed($token) && ! $sessionContainer->isEmpty())) {
213 25
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
214
        }
215
216 27
        return $response;
217
    }
218
219 32
    private function shouldTokenBeRefreshed(?Token $token) : bool
220
    {
221 32
        if (! ($token && $token->hasClaim(self::ISSUED_AT_CLAIM))) {
222 18
            return false;
223
        }
224
225 14
        return $this->timestamp() >= $token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime;
226
    }
227
228
    /**
229
     * @throws BadMethodCallException
230
     */
231 25
    private function getTokenCookie(SessionInterface $sessionContainer) : SetCookie
232
    {
233 25
        $timestamp = $this->timestamp();
234
235
        return $this
236 25
            ->defaultCookie
237 25
            ->withValue(
238 25
                (new Builder())
239 25
                    ->setIssuedAt($timestamp)
240 25
                    ->setExpiration($timestamp + $this->expirationTime)
241 25
                    ->set(self::SESSION_CLAIM, $sessionContainer)
242 25
                    ->sign($this->signer, $this->signatureKey)
243 25
                    ->getToken()
244 25
                    ->__toString()
245
            )
246 25
            ->withExpires($timestamp + $this->expirationTime);
247
    }
248
249 3
    private function getExpirationCookie() : SetCookie
250
    {
251 3
        $expirationDate = $this->clock->now()->modify('-30 days');
252
253
        return $this
254 3
            ->defaultCookie
255 3
            ->withValue(null)
256 3
            ->withExpires($expirationDate->getTimestamp());
257
    }
258
259 30
    private function timestamp() : int
260
    {
261 30
        return $this->clock->now()->getTimestamp();
262
    }
263
}
264