Passed
Push — master ( c0f6da...b424db )
by Jelmer
04:57
created

LoadJwtSessionMiddleware::process()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 9
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 2
crap 2
1
<?php declare(strict_types = 1);
2
3
namespace jschreuder\Middle\Session;
4
5
use Dflydev\FigCookies\FigResponseCookies;
6
use Dflydev\FigCookies\SetCookie;
7
use jschreuder\Middle\DelegateInterface;
8
use jschreuder\Middle\HttpMiddlewareInterface;
9
use Lcobucci\JWT\Builder;
10
use Lcobucci\JWT\Parser;
11
use Lcobucci\JWT\Signer;
12
use Lcobucci\JWT\Token;
13
use Lcobucci\JWT\ValidationData;
14
use Psr\Http\Message\ResponseInterface;
15
use Psr\Http\Message\ServerRequestInterface;
16
use PSR7Session\Session\DefaultSessionData;
17
use PSR7Session\Session\LazySession;
18
use PSR7Session\Session\SessionInterface as JwtSessionInterface;
19
use PSR7Session\Time\CurrentTimeProviderInterface;
20
use PSR7Session\Time\SystemCurrentTime;
21
22
class LoadJwtSessionMiddleware implements HttpMiddlewareInterface
23
{
24
    const ISSUED_AT_CLAIM      = 'iat';
25
    const SESSION_CLAIM        = 'session-data';
26
    const DEFAULT_COOKIE       = 'slsession';
27
    const DEFAULT_REFRESH_TIME = 60;
28
29
    /** @var  Signer */
30
    private $signer;
31
32
    /** @var  string */
33
    private $signatureKey;
34
35
    /** @var  string */
36
    private $verificationKey;
37
38
    /** @var  int */
39
    private $expirationTime;
40
41
    /** @var  int */
42
    private $refreshTime;
43
44
    /** @var  Parser */
45
    private $tokenParser;
46
47
    /** @var  SetCookie */
48
    private $defaultCookie;
49
50
    /** @var  CurrentTimeProviderInterface */
51
    private $currentTimeProvider;
52
53
    public function __construct(
54
        Signer $signer,
55
        string $signatureKey,
56
        string $verificationKey,
57
        SetCookie $defaultCookie,
58
        Parser $tokenParser,
59
        int $expirationTime,
60
        CurrentTimeProviderInterface $currentTimeProvider,
61
        int $refreshTime = self::DEFAULT_REFRESH_TIME
62
    ) {
63
        $this->signer              = $signer;
64
        $this->signatureKey        = $signatureKey;
65
        $this->verificationKey     = $verificationKey;
66
        $this->tokenParser         = $tokenParser;
67
        $this->defaultCookie       = clone $defaultCookie;
68
        $this->expirationTime      = $expirationTime;
69
        $this->currentTimeProvider = $currentTimeProvider;
70
        $this->refreshTime         = $refreshTime;
71
    }
72
73
    public static function fromAsymmetricKeyDefaults(
74
        string $privateRsaKey,
75
        string $publicRsaKey,
76
        int $expirationTime
77
    ) : LoadJwtSessionMiddleware {
78
        return new self(
79
            new Signer\Rsa\Sha256(),
80
            $privateRsaKey,
81
            $publicRsaKey,
82
            SetCookie::create(self::DEFAULT_COOKIE)
83
                ->withSecure(true)
84
                ->withHttpOnly(true)
85
                ->withPath('/'),
86
            new Parser(),
87
            $expirationTime,
88
            new SystemCurrentTime()
89
        );
90
    }
91
92
    public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
93
    {
94
        $token            = $this->parseToken($request);
95
        $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token) : JwtSessionInterface {
96
            return $this->extractSessionContainer($token);
97
        });
98
99
        $response = $delegate->next($request->withAttribute('session', new JwtSession($sessionContainer)));
100
101
        return $this->appendToken($sessionContainer, $response, $token);
102
    }
103
104
    /** @return  ?Token */
1 ignored issue
show
Documentation introduced by
The doc-type ?Token could not be parsed: Unknown type name "?Token" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
105
    private function parseToken(ServerRequestInterface $request)
106
    {
107
        $cookies    = $request->getCookieParams();
108
        $cookieName = $this->defaultCookie->getName();
109
110
        if (! isset($cookies[$cookieName])) {
111
            return null;
112
        }
113
114
        try {
115
            $token = $this->tokenParser->parse($cookies[$cookieName]);
116
        } catch (\InvalidArgumentException $invalidToken) {
117
            return null;
118
        }
119
120
        if (! $token->validate(new ValidationData())) {
121
            return null;
122
        }
123
124
        return $token;
125
    }
126
127
    public function extractSessionContainer(Token $token = null) : JwtSessionInterface
128
    {
129
        try {
130
            if (null === $token || ! $token->verify($this->signer, $this->verificationKey)) {
131
                return DefaultSessionData::newEmptySession();
132
            }
133
134
            return DefaultSessionData::fromDecodedTokenData(
135
                (object) $token->getClaim(self::SESSION_CLAIM, new \stdClass())
136
            );
137
        } catch (\BadMethodCallException $invalidToken) {
138
            return DefaultSessionData::newEmptySession();
139
        }
140
    }
141
142
    private function appendToken(
143
        JwtSessionInterface $sessionContainer,
144
        ResponseInterface $response,
145
        Token $token = null
146
    ) : ResponseInterface
147
    {
148
        $sessionContainerChanged = $sessionContainer->hasChanged();
149
150
        if ($sessionContainerChanged && $sessionContainer->isEmpty()) {
151
            return FigResponseCookies::set($response, $this->getExpirationCookie());
152
        }
153
154
        if ($sessionContainerChanged || ($this->shouldTokenBeRefreshed($token) && ! $sessionContainer->isEmpty())) {
155
            return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
156
        }
157
158
        return $response;
159
    }
160
161
    /**
162
     * {@inheritDoc}
163
     */
164
    private function shouldTokenBeRefreshed(Token $token = null) : bool
165
    {
166
        if (null === $token) {
167
            return false;
168
        }
169
170
        if (! $token->hasClaim(self::ISSUED_AT_CLAIM)) {
171
            return false;
172
        }
173
174
        return $this->timestamp() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
175
    }
176
177
    private function getTokenCookie(JwtSessionInterface $sessionContainer) : SetCookie
178
    {
179
        $timestamp = $this->timestamp();
180
181
        return $this
182
            ->defaultCookie
183
            ->withValue(
184
                (new Builder())
185
                    ->setIssuedAt($timestamp)
186
                    ->setExpiration($timestamp + $this->expirationTime)
187
                    ->set(self::SESSION_CLAIM, $sessionContainer)
188
                    ->sign($this->signer, $this->signatureKey)
189
                    ->getToken()
190
            )
191
            ->withExpires($timestamp + $this->expirationTime);
192
    }
193
194
    /**
195
     * @return SetCookie
196
     */
197
    private function getExpirationCookie() : SetCookie
198
    {
199
        $currentTimeProvider = $this->currentTimeProvider;
200
        $expirationDate      = $currentTimeProvider();
201
        $expirationDate      = $expirationDate->modify('-30 days');
202
203
        return $this
204
            ->defaultCookie
205
            ->withValue(null)
206
            ->withExpires($expirationDate->getTimestamp());
207
    }
208
209
    private function timestamp() : int
210
    {
211
        $currentTimeProvider = $this->currentTimeProvider;
212
213
        return $currentTimeProvider()->getTimestamp();
214
    }
215
}
216