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 PSR7SessionsTest\Storageless\Http; |
22
|
|
|
|
23
|
|
|
use DateTime; |
24
|
|
|
use DateTimeImmutable; |
25
|
|
|
use Dflydev\FigCookies\FigResponseCookies; |
26
|
|
|
use Dflydev\FigCookies\Modifier\SameSite; |
27
|
|
|
use Dflydev\FigCookies\SetCookie; |
28
|
|
|
use InvalidArgumentException; |
29
|
|
|
use Lcobucci\Clock\FrozenClock; |
30
|
|
|
use Lcobucci\Clock\SystemClock; |
31
|
|
|
use Lcobucci\JWT\Builder; |
32
|
|
|
use Lcobucci\JWT\Parser; |
33
|
|
|
use Lcobucci\JWT\Signer; |
34
|
|
|
use Lcobucci\JWT\Signer\Hmac\Sha256; |
35
|
|
|
use OutOfBoundsException; |
36
|
|
|
use PHPUnit\Framework\TestCase; |
37
|
|
|
use Psr\Http\Message\RequestInterface; |
38
|
|
|
use Psr\Http\Message\ResponseInterface; |
39
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
40
|
|
|
use Psr\Http\Server\RequestHandlerInterface; |
41
|
|
|
use PSR7Sessions\Storageless\Http\SessionMiddleware; |
42
|
|
|
use PSR7Sessions\Storageless\Session\DefaultSessionData; |
43
|
|
|
use PSR7Sessions\Storageless\Session\SessionInterface; |
44
|
|
|
use PSR7SessionsTest\Storageless\Asset\MutableBadCookie; |
45
|
|
|
use ReflectionProperty; |
46
|
|
|
use Zend\Diactoros\Response; |
47
|
|
|
use Zend\Diactoros\ServerRequest; |
48
|
|
|
use function assert; |
49
|
|
|
use function file_get_contents; |
50
|
|
|
use function random_int; |
51
|
|
|
use function time; |
52
|
|
|
use function uniqid; |
53
|
|
|
|
54
|
|
|
final class SessionMiddlewareTest extends TestCase |
55
|
|
|
{ |
56
|
|
|
public function testFromSymmetricKeyDefaultsUsesASecureCookie() : void |
57
|
|
|
{ |
58
|
|
|
$response = SessionMiddleware::fromSymmetricKeyDefaults('not relevant', 100) |
59
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()); |
60
|
|
|
|
61
|
|
|
$cookie = $this->getCookie($response); |
62
|
|
|
|
63
|
|
|
self::assertTrue($cookie->getSecure()); |
64
|
|
|
self::assertTrue($cookie->getHttpOnly()); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
public function testFromAsymmetricKeyDefaultsUsesASecureCookie() : void |
68
|
|
|
{ |
69
|
|
|
$response = SessionMiddleware::fromAsymmetricKeyDefaults( |
70
|
|
|
self::privateKey(), |
71
|
|
|
self::publicKey(), |
72
|
|
|
200 |
73
|
|
|
) |
74
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()); |
75
|
|
|
|
76
|
|
|
$cookie = $this->getCookie($response); |
77
|
|
|
|
78
|
|
|
self::assertTrue($cookie->getSecure()); |
79
|
|
|
self::assertTrue($cookie->getHttpOnly()); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* @dataProvider validMiddlewaresProvider |
84
|
|
|
*/ |
85
|
|
|
public function testSkipsInjectingSessionCookieOnEmptyContainer(SessionMiddleware $middleware) : void |
86
|
|
|
{ |
87
|
|
|
$response = $this->ensureSameResponse($middleware, new ServerRequest(), $this->emptyValidationMiddleware()); |
88
|
|
|
|
89
|
|
|
self::assertNull($this->getCookie($response)->getValue()); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @dataProvider validMiddlewaresProvider |
94
|
|
|
*/ |
95
|
|
|
public function testExtractsSessionContainerFromEmptyRequest(SessionMiddleware $middleware) : void |
96
|
|
|
{ |
97
|
|
|
$this->ensureSameResponse($middleware, new ServerRequest(), $this->emptyValidationMiddleware()); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @dataProvider validMiddlewaresProvider |
102
|
|
|
*/ |
103
|
|
|
public function testInjectsSessionInResponseCookies(SessionMiddleware $middleware) : void |
104
|
|
|
{ |
105
|
|
|
$initialResponse = new Response(); |
106
|
|
|
$response = $middleware->process(new ServerRequest(), $this->writingMiddleware()); |
107
|
|
|
|
108
|
|
|
self::assertNotSame($initialResponse, $response); |
109
|
|
|
self::assertEmpty($this->getCookie($response, 'non-existing')->getValue()); |
110
|
|
|
|
111
|
|
|
$token = $this->getCookie($response)->getValue(); |
112
|
|
|
|
113
|
|
|
self::assertIsString($token); |
114
|
|
|
self::assertEquals((object) ['foo' => 'bar'], (new Parser())->parse($token)->getClaim('session-data')); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* @dataProvider validMiddlewaresProvider |
119
|
|
|
*/ |
120
|
|
|
public function testSessionContainerCanBeReusedOverMultipleRequests(SessionMiddleware $middleware) : void |
121
|
|
|
{ |
122
|
|
|
$sessionValue = uniqid('', true); |
123
|
|
|
|
124
|
|
|
$checkingMiddleware = $this->fakeDelegate( |
125
|
|
|
static function (ServerRequestInterface $request) use ($sessionValue) { |
126
|
|
|
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); |
127
|
|
|
assert($session instanceof SessionInterface); |
128
|
|
|
|
129
|
|
|
self::assertSame($sessionValue, $session->get('foo')); |
130
|
|
|
self::assertFalse($session->hasChanged()); |
131
|
|
|
|
132
|
|
|
$session->set('foo', $sessionValue . 'changed'); |
133
|
|
|
|
134
|
|
|
self::assertTrue( |
135
|
|
|
$session->hasChanged(), |
136
|
|
|
'ensuring that the cookie is sent again: ' |
137
|
|
|
. 'non-modified session containers are not to be re-serialized into a token' |
138
|
|
|
); |
139
|
|
|
|
140
|
|
|
return new Response(); |
141
|
|
|
} |
142
|
|
|
); |
143
|
|
|
|
144
|
|
|
$firstResponse = $middleware->process(new ServerRequest(), $this->writingMiddleware($sessionValue)); |
145
|
|
|
|
146
|
|
|
$response = $middleware->process( |
147
|
|
|
$this->requestWithResponseCookies($firstResponse), |
148
|
|
|
$checkingMiddleware |
149
|
|
|
); |
150
|
|
|
|
151
|
|
|
self::assertNotSame($response, $firstResponse); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @dataProvider validMiddlewaresProvider |
156
|
|
|
*/ |
157
|
|
|
public function testSessionContainerCanBeCreatedEvenIfTokenDataIsMalformed(SessionMiddleware $middleware) : void |
158
|
|
|
{ |
159
|
|
|
$sessionValue = uniqid('not valid session data', true); |
160
|
|
|
|
161
|
|
|
$checkingMiddleware = $this->fakeDelegate( |
162
|
|
|
static function (ServerRequestInterface $request) use ($sessionValue) { |
163
|
|
|
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); |
164
|
|
|
assert($session instanceof SessionInterface); |
165
|
|
|
|
166
|
|
|
self::assertSame($sessionValue, $session->get('scalar')); |
167
|
|
|
self::assertFalse($session->hasChanged()); |
168
|
|
|
|
169
|
|
|
return new Response(); |
170
|
|
|
} |
171
|
|
|
); |
172
|
|
|
|
173
|
|
|
$this->createTokenWithCustomClaim( |
174
|
|
|
$middleware, |
175
|
|
|
new DateTime('-1 day'), |
176
|
|
|
new DateTime('+1 day'), |
177
|
|
|
'not valid session data' |
178
|
|
|
); |
179
|
|
|
|
180
|
|
|
$middleware->process( |
181
|
|
|
(new ServerRequest()) |
182
|
|
|
->withCookieParams([ |
183
|
|
|
SessionMiddleware::DEFAULT_COOKIE => $this->createTokenWithCustomClaim( |
184
|
|
|
$middleware, |
185
|
|
|
new DateTime('-1 day'), |
186
|
|
|
new DateTime('+1 day'), |
187
|
|
|
$sessionValue |
188
|
|
|
), |
189
|
|
|
]), |
190
|
|
|
$checkingMiddleware |
191
|
|
|
); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* @dataProvider validMiddlewaresProvider |
196
|
|
|
*/ |
197
|
|
|
public function testWillIgnoreRequestsWithExpiredTokens(SessionMiddleware $middleware) : void |
198
|
|
|
{ |
199
|
|
|
$expiredToken = (new ServerRequest()) |
200
|
|
|
->withCookieParams([ |
201
|
|
|
SessionMiddleware::DEFAULT_COOKIE => $this->createToken( |
202
|
|
|
$middleware, |
203
|
|
|
new DateTime('-1 day'), |
204
|
|
|
new DateTime('-2 day') |
205
|
|
|
), |
206
|
|
|
]); |
207
|
|
|
|
208
|
|
|
$this->ensureSameResponse($middleware, $expiredToken, $this->emptyValidationMiddleware()); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* @dataProvider validMiddlewaresProvider |
213
|
|
|
*/ |
214
|
|
|
public function testWillIgnoreRequestsWithTokensFromFuture(SessionMiddleware $middleware) : void |
215
|
|
|
{ |
216
|
|
|
$tokenInFuture = (new ServerRequest()) |
217
|
|
|
->withCookieParams([ |
218
|
|
|
SessionMiddleware::DEFAULT_COOKIE => $this->createToken( |
219
|
|
|
$middleware, |
220
|
|
|
new DateTime('+1 day'), |
221
|
|
|
new DateTime('-2 day') |
222
|
|
|
), |
223
|
|
|
]); |
224
|
|
|
|
225
|
|
|
$this->ensureSameResponse($middleware, $tokenInFuture, $this->emptyValidationMiddleware()); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* @dataProvider validMiddlewaresProvider |
230
|
|
|
*/ |
231
|
|
|
public function testWillIgnoreUnSignedTokens(SessionMiddleware $middleware) : void |
232
|
|
|
{ |
233
|
|
|
$unsignedToken = (new ServerRequest()) |
234
|
|
|
->withCookieParams([ |
235
|
|
|
SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder()) |
|
|
|
|
236
|
|
|
->setIssuedAt((new DateTime('-1 day'))->getTimestamp()) |
237
|
|
|
->setExpiration((new DateTime('+1 day'))->getTimestamp()) |
238
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
239
|
|
|
->getToken(), |
240
|
|
|
]); |
241
|
|
|
|
242
|
|
|
$this->ensureSameResponse($middleware, $unsignedToken, $this->emptyValidationMiddleware()); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @dataProvider validMiddlewaresProvider |
247
|
|
|
*/ |
248
|
|
|
public function testWillNotRefreshSignedTokensWithoutIssuedAt(SessionMiddleware $middleware) : void |
249
|
|
|
{ |
250
|
|
|
$unsignedToken = (new ServerRequest()) |
251
|
|
|
->withCookieParams([ |
252
|
|
|
SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder()) |
|
|
|
|
253
|
|
|
->setExpiration((new DateTime('+1 day'))->getTimestamp()) |
254
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
255
|
|
|
->sign($this->getSigner($middleware), $this->getSignatureKey($middleware)) |
256
|
|
|
->getToken(), |
257
|
|
|
]); |
258
|
|
|
|
259
|
|
|
$this->ensureSameResponse($middleware, $unsignedToken); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
public function testWillRefreshTokenWithIssuedAtExactlyAtTokenRefreshTimeThreshold() : void |
263
|
|
|
{ |
264
|
|
|
// forcing ourselves to think of time as a mutable value: |
265
|
|
|
$time = time() + random_int(-100, +100); |
266
|
|
|
|
267
|
|
|
$clock = new FrozenClock(new DateTimeImmutable('@' . $time)); |
268
|
|
|
|
269
|
|
|
$middleware = new SessionMiddleware( |
270
|
|
|
new Sha256(), |
271
|
|
|
'foo', |
272
|
|
|
'foo', |
273
|
|
|
SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), |
274
|
|
|
new Parser(), |
275
|
|
|
1000, |
276
|
|
|
$clock, |
277
|
|
|
100 |
278
|
|
|
); |
279
|
|
|
|
280
|
|
|
$requestWithTokenIssuedInThePast = (new ServerRequest()) |
281
|
|
|
->withCookieParams([ |
282
|
|
|
SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder()) |
|
|
|
|
283
|
|
|
->setExpiration($time + 10000) |
284
|
|
|
->setIssuedAt($time - 100) |
285
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
286
|
|
|
->sign($this->getSigner($middleware), $this->getSignatureKey($middleware)) |
287
|
|
|
->getToken(), |
288
|
|
|
]); |
289
|
|
|
|
290
|
|
|
$tokenString = $this |
291
|
|
|
->getCookie($middleware->process($requestWithTokenIssuedInThePast, $this->fakeDelegate(static function () { |
292
|
|
|
return new Response(); |
293
|
|
|
}))) |
294
|
|
|
->getValue(); |
295
|
|
|
|
296
|
|
|
self::assertIsString($tokenString); |
297
|
|
|
|
298
|
|
|
$token = (new Parser())->parse($tokenString); |
299
|
|
|
|
300
|
|
|
self::assertEquals($time, $token->getClaim(SessionMiddleware::ISSUED_AT_CLAIM), 'Token was refreshed'); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* @dataProvider validMiddlewaresProvider |
305
|
|
|
*/ |
306
|
|
|
public function testWillSkipInjectingSessionCookiesWhenSessionIsNotChanged(SessionMiddleware $middleware) : void |
307
|
|
|
{ |
308
|
|
|
$this->ensureSameResponse( |
309
|
|
|
$middleware, |
310
|
|
|
$this->requestWithResponseCookies( |
311
|
|
|
$middleware->process(new ServerRequest(), $this->writingMiddleware()) |
312
|
|
|
), |
313
|
|
|
$this->fakeDelegate( |
314
|
|
|
static function (ServerRequestInterface $request) { |
315
|
|
|
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); |
316
|
|
|
assert($session instanceof SessionInterface); |
317
|
|
|
|
318
|
|
|
// note: we set the same data just to make sure that we are indeed interacting with the session |
319
|
|
|
$session->set('foo', 'bar'); |
320
|
|
|
|
321
|
|
|
self::assertFalse($session->hasChanged()); |
322
|
|
|
|
323
|
|
|
return new Response(); |
324
|
|
|
} |
325
|
|
|
) |
326
|
|
|
); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* @dataProvider validMiddlewaresProvider |
331
|
|
|
*/ |
332
|
|
|
public function testWillSendExpirationCookieWhenSessionContentsAreCleared(SessionMiddleware $middleware) : void |
333
|
|
|
{ |
334
|
|
|
$this->ensureClearsSessionCookie( |
335
|
|
|
$middleware, |
336
|
|
|
$this->requestWithResponseCookies( |
337
|
|
|
$middleware->process(new ServerRequest(), $this->writingMiddleware()) |
338
|
|
|
), |
339
|
|
|
$this->fakeDelegate( |
340
|
|
|
static function (ServerRequestInterface $request) { |
341
|
|
|
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); |
342
|
|
|
assert($session instanceof SessionInterface); |
343
|
|
|
|
344
|
|
|
$session->clear(); |
345
|
|
|
|
346
|
|
|
return new Response(); |
347
|
|
|
} |
348
|
|
|
) |
349
|
|
|
); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* @dataProvider validMiddlewaresProvider |
354
|
|
|
*/ |
355
|
|
|
public function testWillIgnoreMalformedTokens(SessionMiddleware $middleware) : void |
356
|
|
|
{ |
357
|
|
|
$this->ensureSameResponse( |
358
|
|
|
$middleware, |
359
|
|
|
(new ServerRequest())->withCookieParams([SessionMiddleware::DEFAULT_COOKIE => 'malformed content']), |
360
|
|
|
$this->emptyValidationMiddleware() |
361
|
|
|
); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
public function testRejectsTokensWithInvalidSignature() : void |
365
|
|
|
{ |
366
|
|
|
$middleware = new SessionMiddleware( |
367
|
|
|
new Sha256(), |
368
|
|
|
'foo', |
369
|
|
|
'bar', // wrong symmetric key (on purpose) |
370
|
|
|
SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), |
371
|
|
|
new Parser(), |
372
|
|
|
100, |
373
|
|
|
new SystemClock() |
374
|
|
|
); |
375
|
|
|
|
376
|
|
|
$this->ensureSameResponse( |
377
|
|
|
$middleware, |
378
|
|
|
$this->requestWithResponseCookies( |
379
|
|
|
$middleware->process(new ServerRequest(), $this->writingMiddleware()) |
380
|
|
|
), |
381
|
|
|
$this->emptyValidationMiddleware() |
382
|
|
|
); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
public function testAllowsModifyingCookieDetails() : void |
386
|
|
|
{ |
387
|
|
|
$defaultCookie = SetCookie::create('a-different-cookie-name') |
388
|
|
|
->withDomain('foo.bar') |
389
|
|
|
->withPath('/yadda') |
390
|
|
|
->withHttpOnly(false) |
391
|
|
|
->withMaxAge(123123) |
392
|
|
|
->withValue('a-random-value'); |
393
|
|
|
|
394
|
|
|
$dateTime = new DateTimeImmutable(); |
395
|
|
|
$middleware = new SessionMiddleware( |
396
|
|
|
new Sha256(), |
397
|
|
|
'foo', |
398
|
|
|
'foo', |
399
|
|
|
$defaultCookie, |
400
|
|
|
new Parser(), |
401
|
|
|
123456, |
402
|
|
|
new FrozenClock($dateTime), |
403
|
|
|
123 |
404
|
|
|
); |
405
|
|
|
|
406
|
|
|
$response = $middleware->process(new ServerRequest(), $this->writingMiddleware()); |
407
|
|
|
|
408
|
|
|
self::assertNull($this->getCookie($response)->getValue()); |
409
|
|
|
|
410
|
|
|
$tokenCookie = $this->getCookie($response, 'a-different-cookie-name'); |
411
|
|
|
|
412
|
|
|
self::assertNotEmpty($tokenCookie->getValue()); |
413
|
|
|
self::assertNotSame($defaultCookie->getValue(), $tokenCookie->getValue()); |
414
|
|
|
self::assertSame($defaultCookie->getDomain(), $tokenCookie->getDomain()); |
415
|
|
|
self::assertSame($defaultCookie->getPath(), $tokenCookie->getPath()); |
416
|
|
|
self::assertSame($defaultCookie->getHttpOnly(), $tokenCookie->getHttpOnly()); |
417
|
|
|
self::assertSame($defaultCookie->getMaxAge(), $tokenCookie->getMaxAge()); |
418
|
|
|
self::assertEquals($dateTime->getTimestamp() + 123456, $tokenCookie->getExpires()); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
public function testSessionTokenParsingIsDelayedWhenSessionIsNotBeingUsed() : void |
422
|
|
|
{ |
423
|
|
|
$signer = $this->createMock(Signer::class); |
424
|
|
|
|
425
|
|
|
$signer->expects(self::never())->method('verify'); |
426
|
|
|
$signer->method('getAlgorithmId')->willReturn('HS256'); |
|
|
|
|
427
|
|
|
|
428
|
|
|
$currentTimeProvider = new SystemClock(); |
429
|
|
|
$setCookie = SetCookie::create(SessionMiddleware::DEFAULT_COOKIE); |
430
|
|
|
$middleware = new SessionMiddleware($signer, 'foo', 'foo', $setCookie, new Parser(), 100, $currentTimeProvider); |
431
|
|
|
$request = (new ServerRequest()) |
432
|
|
|
->withCookieParams([ |
433
|
|
|
SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder()) |
|
|
|
|
434
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
435
|
|
|
->setIssuedAt(time()) |
436
|
|
|
->sign(new Sha256(), 'foo') |
437
|
|
|
->getToken(), |
438
|
|
|
]); |
439
|
|
|
|
440
|
|
|
$middleware->process( |
441
|
|
|
$request, |
442
|
|
|
$this->fakeDelegate(static function (ServerRequestInterface $request) { |
443
|
|
|
self::assertInstanceOf( |
444
|
|
|
SessionInterface::class, |
445
|
|
|
$request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE) |
446
|
|
|
); |
447
|
|
|
|
448
|
|
|
return new Response(); |
449
|
|
|
}) |
450
|
|
|
); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
public function testShouldRegenerateTokenWhenRequestHasATokenThatIsAboutToExpire() : void |
454
|
|
|
{ |
455
|
|
|
$dateTime = new DateTimeImmutable(); |
456
|
|
|
$middleware = new SessionMiddleware( |
457
|
|
|
new Sha256(), |
458
|
|
|
'foo', |
459
|
|
|
'foo', |
460
|
|
|
SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), |
461
|
|
|
new Parser(), |
462
|
|
|
1000, |
463
|
|
|
new FrozenClock($dateTime), |
464
|
|
|
300 |
465
|
|
|
); |
466
|
|
|
|
467
|
|
|
$expiringToken = (new ServerRequest()) |
468
|
|
|
->withCookieParams([ |
469
|
|
|
SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder()) |
|
|
|
|
470
|
|
|
->setIssuedAt((new DateTime('-800 second'))->getTimestamp()) |
471
|
|
|
->setExpiration((new DateTime('+200 second'))->getTimestamp()) |
472
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
473
|
|
|
->sign($this->getSigner($middleware), $this->getSignatureKey($middleware)) |
474
|
|
|
->getToken(), |
475
|
|
|
]); |
476
|
|
|
|
477
|
|
|
$initialResponse = new Response(); |
478
|
|
|
|
479
|
|
|
$response = $middleware->process($expiringToken, $this->fakeDelegate(static function () use ($initialResponse) { |
480
|
|
|
return $initialResponse; |
481
|
|
|
})); |
482
|
|
|
|
483
|
|
|
self::assertNotSame($initialResponse, $response); |
484
|
|
|
|
485
|
|
|
$tokenCookie = $this->getCookie($response); |
486
|
|
|
|
487
|
|
|
self::assertNotEmpty($tokenCookie->getValue()); |
488
|
|
|
self::assertEquals($dateTime->getTimestamp() + 1000, $tokenCookie->getExpires()); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
public function testShouldNotRegenerateTokenWhenRequestHasATokenThatIsFarFromExpiration() : void |
492
|
|
|
{ |
493
|
|
|
$middleware = new SessionMiddleware( |
494
|
|
|
new Sha256(), |
495
|
|
|
'foo', |
496
|
|
|
'foo', |
497
|
|
|
SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), |
498
|
|
|
new Parser(), |
499
|
|
|
1000, |
500
|
|
|
new SystemClock(), |
501
|
|
|
300 |
502
|
|
|
); |
503
|
|
|
|
504
|
|
|
$validToken = (new ServerRequest()) |
505
|
|
|
->withCookieParams([ |
506
|
|
|
SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder()) |
|
|
|
|
507
|
|
|
->setIssuedAt((new DateTime('-100 second'))->getTimestamp()) |
508
|
|
|
->setExpiration((new DateTime('+900 second'))->getTimestamp()) |
509
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
510
|
|
|
->sign($this->getSigner($middleware), $this->getSignatureKey($middleware)) |
511
|
|
|
->getToken(), |
512
|
|
|
]); |
513
|
|
|
|
514
|
|
|
$this->ensureSameResponse($middleware, $validToken); |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* @return SessionMiddleware[][] |
519
|
|
|
*/ |
520
|
|
|
public function validMiddlewaresProvider() : array |
521
|
|
|
{ |
522
|
|
|
return [ |
523
|
|
|
[new SessionMiddleware( |
524
|
|
|
new Sha256(), |
525
|
|
|
'foo', |
526
|
|
|
'foo', |
527
|
|
|
SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), |
528
|
|
|
new Parser(), |
529
|
|
|
100, |
530
|
|
|
new SystemClock() |
531
|
|
|
), |
532
|
|
|
], |
533
|
|
|
[SessionMiddleware::fromSymmetricKeyDefaults('not relevant', 100)], |
534
|
|
|
[SessionMiddleware::fromAsymmetricKeyDefaults( |
535
|
|
|
self::privateKey(), |
536
|
|
|
self::publicKey(), |
537
|
|
|
200 |
538
|
|
|
), |
539
|
|
|
], |
540
|
|
|
]; |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
/** |
544
|
|
|
* @group #46 |
545
|
|
|
*/ |
546
|
|
|
public function testFromSymmetricKeyDefaultsWillHaveADefaultSessionPath() : void |
547
|
|
|
{ |
548
|
|
|
self::assertSame( |
549
|
|
|
'/', |
550
|
|
|
$this |
551
|
|
|
->getCookie( |
552
|
|
|
SessionMiddleware::fromSymmetricKeyDefaults('not relevant', 100) |
553
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()) |
554
|
|
|
) |
555
|
|
|
->getPath() |
556
|
|
|
); |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
public function testFromSymmetricKeyDefaultsWillHaveALaxSameSitePolicy() : void |
560
|
|
|
{ |
561
|
|
|
self::assertEquals( |
562
|
|
|
SameSite::lax(), |
563
|
|
|
$this |
564
|
|
|
->getCookie( |
565
|
|
|
SessionMiddleware::fromSymmetricKeyDefaults('not relevant', 100) |
566
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()) |
567
|
|
|
) |
568
|
|
|
->getSameSite() |
569
|
|
|
); |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
/** |
573
|
|
|
* @throws InvalidArgumentException |
574
|
|
|
* @throws OutOfBoundsException |
575
|
|
|
* |
576
|
|
|
* @group #46 |
577
|
|
|
*/ |
578
|
|
|
public function testFromAsymmetricKeyDefaultsWillHaveADefaultSessionPath() : void |
579
|
|
|
{ |
580
|
|
|
self::assertSame( |
581
|
|
|
'/', |
582
|
|
|
$this |
583
|
|
|
->getCookie( |
584
|
|
|
SessionMiddleware::fromAsymmetricKeyDefaults( |
585
|
|
|
self::privateKey(), |
586
|
|
|
self::publicKey(), |
587
|
|
|
200 |
588
|
|
|
) |
589
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()) |
590
|
|
|
) |
591
|
|
|
->getPath() |
592
|
|
|
); |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
public function testFromAsymmetricKeyDefaultsWillHaveALaxSameSitePolicy() : void |
596
|
|
|
{ |
597
|
|
|
self::assertEquals( |
598
|
|
|
SameSite::lax(), |
599
|
|
|
$this |
600
|
|
|
->getCookie( |
601
|
|
|
SessionMiddleware::fromAsymmetricKeyDefaults( |
602
|
|
|
self::privateKey(), |
603
|
|
|
self::publicKey(), |
604
|
|
|
200 |
605
|
|
|
) |
606
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()) |
607
|
|
|
) |
608
|
|
|
->getSameSite() |
609
|
|
|
); |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
public function testMutableCookieWillNotBeUsed() : void |
613
|
|
|
{ |
614
|
|
|
$cookie = MutableBadCookie::create(SessionMiddleware::DEFAULT_COOKIE); |
615
|
|
|
|
616
|
|
|
assert($cookie instanceof MutableBadCookie); |
|
|
|
|
617
|
|
|
|
618
|
|
|
$middleware = new SessionMiddleware( |
619
|
|
|
new Sha256(), |
620
|
|
|
'foo', |
621
|
|
|
'foo', |
622
|
|
|
$cookie, |
623
|
|
|
new Parser(), |
624
|
|
|
1000, |
625
|
|
|
new SystemClock() |
626
|
|
|
); |
627
|
|
|
|
628
|
|
|
$cookie->mutated = true; |
629
|
|
|
|
630
|
|
|
self::assertStringStartsWith( |
631
|
|
|
'slsession=', |
632
|
|
|
$middleware |
633
|
|
|
->process(new ServerRequest(), $this->writingMiddleware()) |
634
|
|
|
->getHeaderLine('Set-Cookie') |
635
|
|
|
); |
636
|
|
|
} |
637
|
|
|
|
638
|
|
|
private function ensureSameResponse( |
639
|
|
|
SessionMiddleware $middleware, |
640
|
|
|
ServerRequestInterface $request, |
641
|
|
|
?RequestHandlerInterface $next = null |
642
|
|
|
) : ResponseInterface { |
643
|
|
|
$initialResponse = new Response(); |
644
|
|
|
|
645
|
|
|
$handleRequest = $this->createMock(RequestHandlerInterface::class); |
646
|
|
|
|
647
|
|
|
if ($next === null) { |
648
|
|
|
$handleRequest |
649
|
|
|
->expects(self::once()) |
650
|
|
|
->method('handle') |
651
|
|
|
->willReturn($initialResponse); |
652
|
|
|
} else { |
653
|
|
|
// capturing `$initialResponse` from the `$next` handler |
654
|
|
|
$handleRequest |
655
|
|
|
->expects(self::once()) |
656
|
|
|
->method('handle') |
657
|
|
|
->willReturnCallback(static function (ServerRequestInterface $serverRequest) use ($next, & $initialResponse) { |
658
|
|
|
$response = $next->handle($serverRequest); |
659
|
|
|
|
660
|
|
|
$initialResponse = $response; |
661
|
|
|
|
662
|
|
|
return $response; |
663
|
|
|
}); |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
$response = $middleware->process($request, $handleRequest); |
667
|
|
|
|
668
|
|
|
self::assertSame($initialResponse, $response); |
669
|
|
|
|
670
|
|
|
return $response; |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
private function ensureClearsSessionCookie( |
674
|
|
|
SessionMiddleware $middleware, |
675
|
|
|
ServerRequestInterface $request, |
676
|
|
|
RequestHandlerInterface $next |
677
|
|
|
) : ResponseInterface { |
678
|
|
|
$response = $middleware->process($request, $next); |
679
|
|
|
|
680
|
|
|
$cookie = $this->getCookie($response); |
681
|
|
|
|
682
|
|
|
self::assertLessThan((new DateTime('-29 day'))->getTimestamp(), $cookie->getExpires()); |
683
|
|
|
self::assertEmpty($cookie->getValue()); |
684
|
|
|
|
685
|
|
|
return $response; |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
private function createToken(SessionMiddleware $middleware, DateTime $issuedAt, DateTime $expiration) : string |
689
|
|
|
{ |
690
|
|
|
return (string) (new Builder()) |
|
|
|
|
691
|
|
|
->setIssuedAt($issuedAt->getTimestamp()) |
692
|
|
|
->setExpiration($expiration->getTimestamp()) |
693
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar'])) |
694
|
|
|
->sign($this->getSigner($middleware), $this->getSignatureKey($middleware)) |
695
|
|
|
->getToken(); |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
/** @param mixed $claim */ |
699
|
|
|
private function createTokenWithCustomClaim( |
700
|
|
|
SessionMiddleware $middleware, |
701
|
|
|
DateTime $issuedAt, |
702
|
|
|
DateTime $expiration, |
703
|
|
|
$claim |
704
|
|
|
) : string { |
705
|
|
|
return (string) (new Builder()) |
|
|
|
|
706
|
|
|
->setIssuedAt($issuedAt->getTimestamp()) |
707
|
|
|
->setExpiration($expiration->getTimestamp()) |
708
|
|
|
->set(SessionMiddleware::SESSION_CLAIM, $claim) |
709
|
|
|
->sign($this->getSigner($middleware), $this->getSignatureKey($middleware)) |
710
|
|
|
->getToken(); |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
private function emptyValidationMiddleware() : RequestHandlerInterface |
714
|
|
|
{ |
715
|
|
|
return $this->fakeDelegate( |
716
|
|
|
static function (ServerRequestInterface $request) { |
717
|
|
|
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); |
718
|
|
|
|
719
|
|
|
self::assertInstanceOf(SessionInterface::class, $session); |
720
|
|
|
self::assertTrue($session->isEmpty()); |
721
|
|
|
|
722
|
|
|
return new Response(); |
723
|
|
|
} |
724
|
|
|
); |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
private function writingMiddleware(string $value = 'bar') : RequestHandlerInterface |
728
|
|
|
{ |
729
|
|
|
return $this->fakeDelegate( |
730
|
|
|
static function (ServerRequestInterface $request) use ($value) { |
731
|
|
|
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); |
732
|
|
|
assert($session instanceof SessionInterface); |
733
|
|
|
$session->set('foo', $value); |
734
|
|
|
|
735
|
|
|
return new Response(); |
736
|
|
|
} |
737
|
|
|
); |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
private function fakeDelegate(callable $callback) : RequestHandlerInterface |
741
|
|
|
{ |
742
|
|
|
$middleware = $this->createMock(RequestHandlerInterface::class); |
743
|
|
|
|
744
|
|
|
$middleware |
745
|
|
|
->expects(self::once()) |
746
|
|
|
->method('handle') |
747
|
|
|
->willReturnCallback($callback) |
748
|
|
|
->with(self::isInstanceOf(RequestInterface::class)); |
749
|
|
|
|
750
|
|
|
return $middleware; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* @return ServerRequest |
755
|
|
|
*/ |
756
|
|
|
private function requestWithResponseCookies(ResponseInterface $response) : ServerRequestInterface |
757
|
|
|
{ |
758
|
|
|
return (new ServerRequest())->withCookieParams([ |
759
|
|
|
SessionMiddleware::DEFAULT_COOKIE => $this->getCookie($response)->getValue(), |
760
|
|
|
]); |
761
|
|
|
} |
762
|
|
|
|
763
|
|
|
private function getCookie(ResponseInterface $response, string $name = SessionMiddleware::DEFAULT_COOKIE) : SetCookie |
764
|
|
|
{ |
765
|
|
|
return FigResponseCookies::get($response, $name); |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
private function getSigner(SessionMiddleware $middleware) : Signer |
769
|
|
|
{ |
770
|
|
|
$property = new ReflectionProperty(SessionMiddleware::class, 'signer'); |
771
|
|
|
|
772
|
|
|
$property->setAccessible(true); |
773
|
|
|
|
774
|
|
|
return $property->getValue($middleware); |
775
|
|
|
} |
776
|
|
|
|
777
|
|
|
private function getSignatureKey(SessionMiddleware $middleware) : string |
778
|
|
|
{ |
779
|
|
|
$property = new ReflectionProperty(SessionMiddleware::class, 'signatureKey'); |
780
|
|
|
|
781
|
|
|
$property->setAccessible(true); |
782
|
|
|
|
783
|
|
|
return $property->getValue($middleware); |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
private static function privateKey() : string |
787
|
|
|
{ |
788
|
|
|
$key = file_get_contents(__DIR__ . '/../../keys/private_key.pem'); |
789
|
|
|
|
790
|
|
|
self::assertIsString($key); |
791
|
|
|
|
792
|
|
|
return $key; |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
private static function publicKey() : string |
796
|
|
|
{ |
797
|
|
|
$key = file_get_contents(__DIR__ . '/../../keys/public_key.pem'); |
798
|
|
|
|
799
|
|
|
self::assertIsString($key); |
800
|
|
|
|
801
|
|
|
return $key; |
802
|
|
|
} |
803
|
|
|
} |
804
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.