Passed
Push — master ( b3f945...fcf496 )
by Jelmer
04:58
created

JwtSessionProcessor::shouldTokenBeRefreshed()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 1
crap 3
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 Lcobucci\JWT\Builder;
8
use Lcobucci\JWT\Parser;
9
use Lcobucci\JWT\Signer;
10
use Lcobucci\JWT\Token;
11
use Lcobucci\JWT\ValidationData;
12
use Psr\Http\Message\ResponseInterface;
13
use Psr\Http\Message\ServerRequestInterface;
14
15
/**
16
 * A copy-paste from ocramius/psr7-session, but without needing to pull in
17
 * Stratigility and working with our session container.
18
 *
19
 * @todo this class probably needs a big refactor
20
 */
21
final class JwtSessionProcessor implements SessionProcessorInterface
22
{
23
    const ISSUED_AT_CLAIM = 'iat';
24
    const SESSION_CLAIM = 'session-data';
25
    const DEFAULT_COOKIE = 'slsession';
26
27
    /** @var  Signer */
28
    private $signer;
29
30
    /** @var  string */
31
    private $signatureKey;
32
33
    /** @var  string */
34
    private $verificationKey;
35
36
    /** @var  int */
37
    private $expirationTime;
38
39
    /** @var  int */
40
    private $refreshTime;
41
42
    /** @var  Parser */
43
    private $tokenParser;
44
45
    /** @var  SetCookie */
46
    private $defaultCookie;
47
48 19
    public function __construct(
49
        Signer $signer,
50
        string $signatureKey,
51
        string $verificationKey,
52
        SetCookie $defaultCookie,
53
        Parser $tokenParser,
54
        int $expirationTime,
55
        int $refreshTime = 60
56
    )
57
    {
58 19
        $this->signer = $signer;
59 19
        $this->signatureKey = $signatureKey;
60 19
        $this->verificationKey = $verificationKey;
61 19
        $this->tokenParser = $tokenParser;
62 19
        $this->defaultCookie = clone $defaultCookie;
63 19
        $this->expirationTime = $expirationTime;
64 19
        $this->refreshTime = $refreshTime;
65 19
    }
66
67 1
    public static function fromAsymmetricKeyDefaults(
68
        string $privateRsaKey,
69
        string $publicRsaKey,
70
        int $expirationTime
71
    ): JwtSessionProcessor
72
    {
73 1
        return new self(
74 1
            new Signer\Rsa\Sha256(),
75
            $privateRsaKey,
76
            $publicRsaKey,
77 1
            SetCookie::create(self::DEFAULT_COOKIE)
78 1
                ->withSecure(true)
79 1
                ->withHttpOnly(true)
80 1
                ->withPath('/'),
81 1
            new Parser(),
82
            $expirationTime
83
        );
84
    }
85
86 1
    public function processRequest(ServerRequestInterface $request): ServerRequestInterface
87
    {
88 1
        $token = $this->parseToken($request);
89 1
        $sessionContainer = $this->extractSessionContainer($token);
90
        return $request
91 1
            ->withAttribute('session', $sessionContainer)
92 1
            ->withAttribute('session.token', $token);
93
94
    }
95
96 1
    public function processResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
97
    {
98 1
        return $this->appendToken(
99 1
            $request->getAttribute('session'),
100
            $response,
101 1
            $request->getAttribute('session.token')
102
        );
103
    }
104
105
    /** @return  ?Token */
106 5
    public function parseToken(ServerRequestInterface $request)
107
    {
108 5
        $cookies = $request->getCookieParams();
109 5
        $cookieName = $this->defaultCookie->getName();
110
111 5
        if (!isset($cookies[$cookieName])) {
112 1
            return null;
113
        }
114
115
        try {
116 4
            $token = $this->tokenParser->parse($cookies[$cookieName]);
117 1
        } catch (\InvalidArgumentException $invalidToken) {
118 1
            return null;
119
        }
120
121 3
        if (!$token->validate(new ValidationData())) {
122 1
            return null;
123
        }
124
125 2
        return $token;
126
    }
127
128 9
    public function appendToken(
129
        SessionInterface $session,
130
        ResponseInterface $response,
131
        Token $token = null
132
    ): ResponseInterface
133
    {
134 9
        $sessionContainerChanged = $session->hasChanged();
135
136 9
        if ($sessionContainerChanged && $session->isEmpty()) {
137 2
            return FigResponseCookies::set($response, $this->getExpirationCookie());
138
        }
139
140 7
        if ($sessionContainerChanged || ($this->shouldTokenBeRefreshed($token) && !$session->isEmpty())) {
141 3
            return FigResponseCookies::set($response, $this->getTokenCookie($session));
142
        }
143
144 4
        return $response;
145
    }
146
147 2
    private function getExpirationCookie(): SetCookie
148
    {
149 2
        $expirationDate = new \DateTime('-30 days');
150
        return $this
151 2
            ->defaultCookie
152 2
            ->withValue(null)
153 2
            ->withExpires($expirationDate->getTimestamp());
154
    }
155
156 5
    private function shouldTokenBeRefreshed(Token $token = null): bool
157
    {
158 5
        if (is_null($token)) {
159 1
            return false;
160
        }
161
162 4
        if (!$token->hasClaim(self::ISSUED_AT_CLAIM)) {
163 2
            return false;
164
        }
165
166 2
        return time() >= ($token->getClaim(self::ISSUED_AT_CLAIM) + $this->refreshTime);
167
    }
168
169 3
    private function getTokenCookie(SessionInterface $session): SetCookie
170
    {
171 3
        $timestamp = time();
172
        return $this
173 3
            ->defaultCookie
174 3
            ->withValue(
175 3
                (new Builder())
176 3
                    ->setIssuedAt($timestamp)
177 3
                    ->setExpiration($timestamp + $this->expirationTime)
178 3
                    ->set(self::SESSION_CLAIM, $session->toArray())
179 3
                    ->sign($this->signer, $this->signatureKey)
180 3
                    ->getToken()
181
            )
182 3
            ->withExpires($timestamp + $this->expirationTime);
183
    }
184
185 5
    public function extractSessionContainer(Token $token = null): SessionInterface
186
    {
187
        try {
188 5
            if (is_null($token) || !$token->verify($this->signer, $this->verificationKey)) {
189 2
                return new Session();
190
            }
191
192
            // Re-encode the payload and decode as array to not get stdClass tree
193 2
            return new Session(
194 2
                json_decode(json_encode($token->getClaim(self::SESSION_CLAIM, [])), true)
195
            );
196 1
        } catch (\BadMethodCallException $invalidToken) {
197 1
            return new Session();
198
        }
199
    }
200
}
201