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

SessionMiddleware::parseToken()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 10
cts 10
cp 1
rs 9.584
c 0
b 0
f 0
cc 4
nc 4
nop 1
crap 4
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