From 16b06a7fdba8706930e4a92c740766eaf41c53d7 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Sat, 6 May 2023 10:12:36 +0200 Subject: [PATCH 1/6] add `StoragelessSession` service --- src/Storageless/Service/SessionStorage.php | 34 ++ .../Service/StoragelessSession.php | 266 +++++++++++++++ .../Service/StoragelessSessionTest.php | 309 ++++++++++++++++++ 3 files changed, 609 insertions(+) create mode 100644 src/Storageless/Service/SessionStorage.php create mode 100644 src/Storageless/Service/StoragelessSession.php create mode 100644 test/StoragelessTest/Service/StoragelessSessionTest.php diff --git a/src/Storageless/Service/SessionStorage.php b/src/Storageless/Service/SessionStorage.php new file mode 100644 index 00000000..9b1c521d --- /dev/null +++ b/src/Storageless/Service/SessionStorage.php @@ -0,0 +1,34 @@ +cookie = clone $cookie; + } + + public static function fromSymmetricKeyDefaults( + Signer\Key $symmetricKey, + int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT, + Cookie|SetCookie|null $cookie = null, + ClockInterface|null $clock = null, + ): self { + return new self( + Configuration::forSymmetricSigner( + new Signer\Hmac\Sha256(), + $symmetricKey, + ), + $idleTimeout, + $cookie ?? SetCookie::create(self::DEFAULT_COOKIE) + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite(SameSite::lax()) + ->withPath('/'), + $clock ?? SystemClock::fromUTC(), + ); + } + + public static function fromRsaAsymmetricKeyDefaults( + Signer\Key $privateRsaKey, + Signer\Key $publicRsaKey, + int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT, + Cookie|SetCookie|null $cookie = null, + ClockInterface|null $clock = null, + ): self { + return new self( + Configuration::forAsymmetricSigner( + new Signer\Rsa\Sha256(), + $privateRsaKey, + $publicRsaKey, + ), + $idleTimeout, + $cookie ?? SetCookie::create(self::DEFAULT_COOKIE) + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite(SameSite::lax()) + ->withPath('/'), + $clock ?? SystemClock::fromUTC(), + ); + } + + public function withSession(ServerRequestInterface|ResponseInterface $message, SessionInterface $session): RequestInterface|ResponseInterface + { + if ($message instanceof ResponseInterface) { + return $this->withResponseSession($message, $session, $this->clock->now()); + } + + return $this->withRequestSession($message, $session, $this->clock->now()); + } + + public function get(ServerRequestInterface|ResponseInterface $message): SessionInterface + { + $cookie = $this->getCookieFromMessage($message); + + return $cookie === null + ? DefaultSessionData::newEmptySession() + : $this->cookieToSession($cookie); + } + + public function getCookieFromMessage(ServerRequestInterface|ResponseInterface $message): SetCookie|Cookie|null + { + // TODO: Why we cannot use Cookies::fromRequest() ? + // See: https://github.com/dflydev/dflydev-fig-cookies/issues/57 + if ($message instanceof ServerRequestInterface) { + $cookies = $message->getCookieParams(); + + if (! array_key_exists($this->cookie->getName(), $cookies)) { + return null; + } + + $cookieValue = $cookies[$this->cookie->getName()]; + assert(is_string($cookieValue)); + + return Cookie::create($this->cookie->getName(), $cookieValue === '' ? null : $cookieValue); + } + + return SetCookies::fromResponse($message)->get($this->cookie->getName()); + } + + public function cookieToToken(SetCookie|Cookie|null $cookie): UnencryptedToken|null + { + if ($cookie === null) { + return null; + } + + $jwt = $cookie->getValue(); + + if ($jwt === null) { + return null; + } + + if ($jwt === '') { + return null; + } + + try { + $token = $this->configuration->parser()->parse($jwt); + } catch (Throwable) { + return null; + } + + if (! $token instanceof UnencryptedToken) { + return null; + } + + $isValid = $this + ->configuration + ->validator() + ->validate( + $token, + new StrictValidAt($this->clock), + new SignedWith($this->configuration->signer(), $this->configuration->verificationKey()), + ); + + if ($isValid === false) { + return null; + } + + return $token; + } + + private function withRequestSession(RequestInterface $request, SessionInterface $session, DateTimeImmutable $now): RequestInterface + { + if ($session->hasChanged() === false) { + return $request; + } + + if (! $this->cookie instanceof Cookie) { + throw new Exception( + 'The default cookie is not a Cookie type.', + ); + } + + return FigRequestCookies::set( + $request, + $this->appendCookieSession($this->cookie, $session, $now), + ); + } + + private function withResponseSession(ResponseInterface $response, SessionInterface $session, DateTimeImmutable $now): ResponseInterface + { + if (! $this->cookie instanceof SetCookie) { + throw new Exception( + 'The default cookie is not a SetCookie type.', + ); + } + + if ($session->isEmpty()) { + return FigResponseCookies::set( + $response, + $this->cookie->withExpires($now->modify('-30 days')), + ); + } + + return FigResponseCookies::set( + $response, + $this + ->appendCookieSession( + $this->cookie->withExpires($now->add(new DateInterval(sprintf('PT%sS', $this->idleTimeout)))), + $session, + $now, + ), + ); + } + + /** @psalm-return ($cookie is SetCookie ? SetCookie : Cookie) */ + private function appendCookieSession(SetCookie|Cookie $cookie, SessionInterface $session, DateTimeImmutable $now): SetCookie|Cookie + { + $value = $session->isEmpty() + ? null + : $this->configuration->builder(ChainedFormatter::withUnixTimestampDates()) + ->issuedAt($now) + ->canOnlyBeUsedAfter($now) + ->expiresAt($now->add(new DateInterval(sprintf('PT%sS', $this->idleTimeout)))) + ->withClaim(self::SESSION_CLAIM, $session) + ->getToken($this->configuration->signer(), $this->configuration->signingKey()) + ->toString(); + + return $cookie->withValue($value); + } + + private function cookieToSession(SetCookie|Cookie $cookie): SessionInterface + { + $token = $this->cookieToToken($cookie); + + if ($token === null) { + return DefaultSessionData::newEmptySession(); + } + + return DefaultSessionData::fromDecodedTokenData( + (object) $token->claims()->get(self::SESSION_CLAIM, new stdClass()), + ); + } +} diff --git a/test/StoragelessTest/Service/StoragelessSessionTest.php b/test/StoragelessTest/Service/StoragelessSessionTest.php new file mode 100644 index 00000000..561784d8 --- /dev/null +++ b/test/StoragelessTest/Service/StoragelessSessionTest.php @@ -0,0 +1,309 @@ + */ + public function itThrowsExceptionWhenCookieTypeIsInvalidProvider(): Generator + { + yield [ + static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + 0, + SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), + new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), + ), + new ServerRequest(), + 'The default cookie is not a Cookie type.', + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( + Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), + Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), + 0, + Cookie::create(SessionMiddleware::DEFAULT_COOKIE), + new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), + ), + new Response(), + 'The default cookie is not a SetCookie type.', + ]; + } + + /** + * @param Closure(): SessionStorage $sessionStorageClosure + * + * @dataProvider itThrowsExceptionWhenCookieTypeIsInvalidProvider + */ + public function testItThrowsExceptionWhenCookieTypeIsInvalid( + Closure $sessionStorageClosure, + ServerRequestInterface|ResponseInterface $message, + string $exceptionMessage, + ): void { + $sessionStorage = $sessionStorageClosure(); + + $session = $sessionStorage->get($message); + $session->set('foo', 'bar'); + + $this->expectException(Throwable::class); + $this->expectExceptionMessage($exceptionMessage); + + $sessionStorage->withSession($message, $session); + } + + /** @return Generator */ + public function itCanCustomizeACookieProvider(): Generator + { + yield [ + static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + ), + SessionMiddleware::buildDefaultCookie(), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( + Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), + Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), + ), + SessionMiddleware::buildDefaultCookie(), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + 100, + SessionMiddleware::buildDefaultCookie()->withPath('/foo'), + ), + SessionMiddleware::buildDefaultCookie()->withPath('/foo'), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( + Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), + Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), + 100, + SessionMiddleware::buildDefaultCookie()->withPath('/foo'), + ), + SessionMiddleware::buildDefaultCookie()->withPath('/foo'), + ]; + } + + /** @return Generator */ + public function itCanCustomizeAClockProvider(): Generator + { + yield [ + static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + 0, + ), + SystemClock::fromUTC(), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( + Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), + Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), + 0, + ), + SystemClock::fromUTC(), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + 0, + null, + new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), + ), + new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( + Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), + Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), + 0, + null, + new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), + ), + new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), + ]; + } + + /** + * @param Closure(): SessionStorage $sessionStorageClosure + * + * @dataProvider itCanCustomizeACookieProvider + */ + public function testItCanCustomizeACookie(Closure $sessionStorageClosure, SetCookie|null $cookie): void + { + $sessionStorage = $sessionStorageClosure(); + + $response = new Response(); + + $session = $sessionStorage->get($response); + $session->set('foo', 'bar'); + + $response = $sessionStorage->withSession($response, $session); + + $cookie ??= SessionMiddleware::buildDefaultCookie(); + + $this->assertEquals( + $cookie->getPath(), + $this->getCookie($response)->getPath(), + ); + + $this->assertEquals( + $cookie->getHttpOnly(), + $this->getCookie($response)->getHttpOnly(), + ); + + $this->assertEquals( + $cookie->getSecure(), + $this->getCookie($response)->getSecure(), + ); + } + + /** @return Generator */ + public function itCanCreateACookieWhenItIsNotSetProvider(): Generator + { + yield [ + static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + ), + ]; + + yield [ + static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( + Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), + Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), + ), + ]; + } + + /** + * @param Closure(): SessionStorage $sessionStorageClosure + * + * @dataProvider itCanCreateACookieWhenItIsNotSetProvider + */ + public function testItCanCreateACookieWhenItIsNotSet(Closure $sessionStorageClosure): void + { + $sessionStorage = $sessionStorageClosure(); + + $response = new Response(); + + $session = $sessionStorage->get($response); + $session->set('foo', 'bar'); + + $response = $sessionStorage->withSession($response, $session); + + $this->assertTrue( + $this->getCookie($response)->getSecure(), + ); + + $this->assertTrue( + $this->getCookie($response)->getHttpOnly(), + ); + + $this->assertEquals( + SameSite::lax(), + $this->getCookie($response)->getSameSite(), + ); + + $this->assertEquals( + '/', + $this->getCookie($response)->getPath(), + ); + } + + /** + * @param Closure(): SessionStorage $sessionStorageClosure + * + * @dataProvider itCanCustomizeAClockProvider + */ + public function testItCanUseACustomClock(Closure $sessionStorageClosure, ClockInterface $clock): void + { + $sessionStorage = $sessionStorageClosure(); + + $response = new Response(); + $session = $sessionStorage->get($response); + $session->set('foo', 'bar'); + + $cookie = $this->getCookie($sessionStorage->withSession($response, $session)); + + $this->assertEquals($clock->now()->getTimestamp(), $cookie->getExpires()); + } + + /** @return Generator */ + public function itCanDetectNullOrEmptyJWTProvider(): Generator + { + yield [SessionMiddleware::buildDefaultCookie()->withValue('')]; + + yield [SessionMiddleware::buildDefaultCookie()->withValue(null)]; + } + + /** @dataProvider itCanDetectNullOrEmptyJWTProvider */ + public function testItCanDetectNullOrEmptyJWT(SetCookie $cookie): void + { + $sessionStorage = StoragelessSession::fromSymmetricKeyDefaults( + self::makeRandomSymmetricKey(), + ); + + $this->assertNull($sessionStorage->cookieToToken($cookie)); + } + + private function getCookie(ResponseInterface $response, string $name = SessionMiddleware::DEFAULT_COOKIE): SetCookie + { + return FigResponseCookies::get($response, $name); + } + + private static function makeRandomSymmetricKey(): Signer\Key\InMemory + { + return Signer\Key\InMemory::plainText('test-key_' . base64_encode(random_bytes(128))); + } +} From b65b06068d770e10ccb4b0e76a2f64ecc596b4b9 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Sat, 6 May 2023 10:20:15 +0200 Subject: [PATCH 2/6] refactor: let `SessionMiddleware` use `StoragelessSession` service --- examples/index.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/index.php b/examples/index.php index 9522aa9e..154d7526 100644 --- a/examples/index.php +++ b/examples/index.php @@ -30,6 +30,7 @@ use Psr\Http\Server\RequestHandlerInterface; use PSR7Sessions\Storageless\Http\Configuration; use PSR7Sessions\Storageless\Http\SessionMiddleware; +use PSR7Sessions\Storageless\Service\StoragelessSession; use PSR7Sessions\Storageless\Session\SessionInterface; require_once __DIR__ . '/../vendor/autoload.php'; @@ -41,6 +42,7 @@ // simply run `php -S localhost:9999 index.php` // then point your browser at `http://localhost:9999/` +$clock = SystemClock::fromUTC(); $sessionMiddleware = new SessionMiddleware( (new Configuration( JwtConfig::forSymmetricSigner( From 1d54d46969ea4a872112eac1643b7691a663559c Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 26 Jan 2024 13:27:05 +0100 Subject: [PATCH 3/6] refactor: (wip) change almost everything --- examples/index.php | 1 - src/Storageless/Http/SessionMiddleware.php | 167 +--------- src/Storageless/Service/SessionStorage.php | 7 +- .../Service/StoragelessSession.php | 291 +++++++---------- src/Storageless/Session/LazySession.php | 24 ++ .../Service/StoragelessSessionTest.php | 309 ------------------ 6 files changed, 153 insertions(+), 646 deletions(-) delete mode 100644 test/StoragelessTest/Service/StoragelessSessionTest.php diff --git a/examples/index.php b/examples/index.php index 154d7526..1c3f813f 100644 --- a/examples/index.php +++ b/examples/index.php @@ -30,7 +30,6 @@ use Psr\Http\Server\RequestHandlerInterface; use PSR7Sessions\Storageless\Http\Configuration; use PSR7Sessions\Storageless\Http\SessionMiddleware; -use PSR7Sessions\Storageless\Service\StoragelessSession; use PSR7Sessions\Storageless\Session\SessionInterface; require_once __DIR__ . '/../vendor/autoload.php'; diff --git a/src/Storageless/Http/SessionMiddleware.php b/src/Storageless/Http/SessionMiddleware.php index abd8b3d0..2b38202a 100644 --- a/src/Storageless/Http/SessionMiddleware.php +++ b/src/Storageless/Http/SessionMiddleware.php @@ -21,27 +21,13 @@ namespace PSR7Sessions\Storageless\Http; use BadMethodCallException; -use DateInterval; -use Dflydev\FigCookies\FigResponseCookies; -use Dflydev\FigCookies\SetCookie; use InvalidArgumentException; -use Lcobucci\JWT\Encoding\ChainedFormatter; -use Lcobucci\JWT\Token; -use Lcobucci\JWT\UnencryptedToken; -use Lcobucci\JWT\Validation\Constraint\SignedWith; -use Lcobucci\JWT\Validation\Constraint\StrictValidAt; -use OutOfBoundsException; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use PSR7Sessions\Storageless\Http\ClientFingerprint\SameOriginRequest; -use PSR7Sessions\Storageless\Session\DefaultSessionData; +use PSR7Sessions\Storageless\Service\StoragelessSession; use PSR7Sessions\Storageless\Session\LazySession; -use PSR7Sessions\Storageless\Session\SessionInterface; -use stdClass; - -use function sprintf; /** @immutable */ final class SessionMiddleware implements MiddlewareInterface @@ -49,9 +35,12 @@ final class SessionMiddleware implements MiddlewareInterface public const SESSION_CLAIM = 'session-data'; public const SESSION_ATTRIBUTE = 'session'; + private readonly StoragelessSession $service; + public function __construct( private readonly Configuration $config, ) { + $this->service = new StoragelessSession($config); } /** @@ -62,149 +51,11 @@ public function __construct( */ public function process(Request $request, RequestHandlerInterface $handler): Response { - $sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); - $token = $this->parseToken($request, $sameOriginRequest); - $sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token): SessionInterface { - return $this->extractSessionContainer($token); - }); - - return $this->appendToken( - $sessionContainer, - $handler->handle($request->withAttribute($this->config->getSessionAttribute(), $sessionContainer)), - $token, - $sameOriginRequest, + return $this->service->appendSession( + LazySession::fromToken($this->service->requestToToken($request)), + $request, + null, + $handler, ); } - - /** - * Extract the token from the given request object - */ - private function parseToken(Request $request, SameOriginRequest $sameOriginRequest): UnencryptedToken|null - { - /** @var array $cookies */ - $cookies = $request->getCookieParams(); - $cookieName = $this->config->getCookie()->getName(); - - if (! isset($cookies[$cookieName])) { - return null; - } - - $cookie = $cookies[$cookieName]; - if ($cookie === '') { - return null; - } - - $jwtConfiguration = $this->config->getJwtConfiguration(); - try { - $token = $jwtConfiguration->parser()->parse($cookie); - } catch (InvalidArgumentException) { - return null; - } - - if (! $token instanceof UnencryptedToken) { - return null; - } - - $constraints = [ - new StrictValidAt($this->config->getClock()), - new SignedWith($jwtConfiguration->signer(), $jwtConfiguration->verificationKey()), - $sameOriginRequest, - ]; - - if (! $jwtConfiguration->validator()->validate($token, ...$constraints)) { - return null; - } - - return $token; - } - - /** @throws OutOfBoundsException */ - private function extractSessionContainer(UnencryptedToken|null $token): SessionInterface - { - if (! $token) { - return DefaultSessionData::newEmptySession(); - } - - try { - return DefaultSessionData::fromDecodedTokenData( - (object) $token->claims()->get(self::SESSION_CLAIM, new stdClass()), - ); - } catch (BadMethodCallException) { - return DefaultSessionData::newEmptySession(); - } - } - - /** - * @throws BadMethodCallException - * @throws InvalidArgumentException - */ - private function appendToken( - SessionInterface $sessionContainer, - Response $response, - Token|null $token, - SameOriginRequest $sameOriginRequest, - ): Response { - $sessionContainerChanged = $sessionContainer->hasChanged(); - - if ($sessionContainerChanged && $sessionContainer->isEmpty()) { - return FigResponseCookies::set($response, $this->getExpirationCookie()); - } - - if ($sessionContainerChanged || $this->shouldTokenBeRefreshed($token)) { - return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer, $sameOriginRequest)); - } - - return $response; - } - - private function shouldTokenBeRefreshed(Token|null $token): bool - { - if ($token === null) { - return false; - } - - return $token->hasBeenIssuedBefore( - $this->config->getClock() - ->now() - ->sub(new DateInterval(sprintf('PT%sS', $this->config->getRefreshTime()))), - ); - } - - /** @throws BadMethodCallException */ - private function getTokenCookie(SessionInterface $sessionContainer, SameOriginRequest $sameOriginRequest): SetCookie - { - $now = $this->config->getClock()->now(); - $expiresAt = $now->add(new DateInterval(sprintf('PT%sS', $this->config->getIdleTimeout()))); - - $jwtConfiguration = $this->config->getJwtConfiguration(); - - $builder = $jwtConfiguration->builder(ChainedFormatter::withUnixTimestampDates()) - ->issuedAt($now) - ->canOnlyBeUsedAfter($now) - ->expiresAt($expiresAt) - ->withClaim(self::SESSION_CLAIM, $sessionContainer); - - $builder = $sameOriginRequest->configure($builder); - - return $this - ->config->getCookie() - ->withValue( - $builder - ->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey()) - ->toString(), - ) - ->withExpires($expiresAt); - } - - private function getExpirationCookie(): SetCookie - { - return $this - ->config->getCookie() - ->withValue(null) - ->withExpires( - $this->config->getClock() - ->now() - ->modify('-30 days'), - ); - } } diff --git a/src/Storageless/Service/SessionStorage.php b/src/Storageless/Service/SessionStorage.php index 9b1c521d..fb2714bf 100644 --- a/src/Storageless/Service/SessionStorage.php +++ b/src/Storageless/Service/SessionStorage.php @@ -20,15 +20,14 @@ namespace PSR7Sessions\Storageless\Service; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use PSR7Sessions\Storageless\Session\SessionInterface; interface SessionStorage { - /** @psalm-return ($message is ResponseInterface ? ResponseInterface : RequestInterface) */ - public function withSession(ServerRequestInterface|ResponseInterface $message, SessionInterface $session): RequestInterface|ResponseInterface; + public function appendSession(SessionInterface $session, ServerRequestInterface $request, ResponseInterface|null $response, RequestHandlerInterface|null $handler = null): ResponseInterface; - public function get(ServerRequestInterface|ResponseInterface $message): SessionInterface; + public function getSession(ServerRequestInterface $request): SessionInterface; } diff --git a/src/Storageless/Service/StoragelessSession.php b/src/Storageless/Service/StoragelessSession.php index a3622a13..5d2465a4 100644 --- a/src/Storageless/Service/StoragelessSession.php +++ b/src/Storageless/Service/StoragelessSession.php @@ -20,154 +20,148 @@ namespace PSR7Sessions\Storageless\Service; +use BadMethodCallException; use DateInterval; -use DateTimeImmutable; -use Dflydev\FigCookies\Cookie; -use Dflydev\FigCookies\FigRequestCookies; use Dflydev\FigCookies\FigResponseCookies; -use Dflydev\FigCookies\Modifier\SameSite; use Dflydev\FigCookies\SetCookie; -use Dflydev\FigCookies\SetCookies; -use Exception; -use Lcobucci\Clock\SystemClock; -use Lcobucci\JWT\Configuration; +use InvalidArgumentException; use Lcobucci\JWT\Encoding\ChainedFormatter; -use Lcobucci\JWT\Signer; +use Lcobucci\JWT\Token; use Lcobucci\JWT\UnencryptedToken; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\StrictValidAt; -use Psr\Clock\ClockInterface; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface; -use PSR7Sessions\Storageless\Session\DefaultSessionData; +use Psr\Http\Server\RequestHandlerInterface; +use PSR7Sessions\Storageless\Http\ClientFingerprint\SameOriginRequest; +use PSR7Sessions\Storageless\Http\Configuration; +use PSR7Sessions\Storageless\Http\SessionMiddleware; +use PSR7Sessions\Storageless\Session\LazySession; use PSR7Sessions\Storageless\Session\SessionInterface; -use stdClass; -use Throwable; -use function array_key_exists; -use function assert; -use function is_string; use function sprintf; final class StoragelessSession implements SessionStorage { - private const DEFAULT_COOKIE = '__Secure-slsession'; - private const SESSION_CLAIM = 'session-data'; - private const DEFAULT_IDLE_TIMEOUT = 300; - - private readonly Cookie|SetCookie $cookie; + public const SESSION_CLAIM = 'session-data'; public function __construct( - private readonly Configuration $configuration, - private readonly int $idleTimeout, - Cookie|SetCookie $cookie, - private readonly ClockInterface $clock, + private readonly Configuration $config, ) { - $this->cookie = clone $cookie; } - public static function fromSymmetricKeyDefaults( - Signer\Key $symmetricKey, - int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT, - Cookie|SetCookie|null $cookie = null, - ClockInterface|null $clock = null, - ): self { - return new self( - Configuration::forSymmetricSigner( - new Signer\Hmac\Sha256(), - $symmetricKey, - ), - $idleTimeout, - $cookie ?? SetCookie::create(self::DEFAULT_COOKIE) - ->withSecure(true) - ->withHttpOnly(true) - ->withSameSite(SameSite::lax()) - ->withPath('/'), - $clock ?? SystemClock::fromUTC(), - ); - } + public function appendSession(SessionInterface $session, ServerRequestInterface $request, Response|null $response = null, RequestHandlerInterface|null $handler = null): Response + { + $sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); + $token = $this->requestToToken($request, $sameOriginRequest); - public static function fromRsaAsymmetricKeyDefaults( - Signer\Key $privateRsaKey, - Signer\Key $publicRsaKey, - int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT, - Cookie|SetCookie|null $cookie = null, - ClockInterface|null $clock = null, - ): self { - return new self( - Configuration::forAsymmetricSigner( - new Signer\Rsa\Sha256(), - $privateRsaKey, - $publicRsaKey, - ), - $idleTimeout, - $cookie ?? SetCookie::create(self::DEFAULT_COOKIE) - ->withSecure(true) - ->withHttpOnly(true) - ->withSameSite(SameSite::lax()) - ->withPath('/'), - $clock ?? SystemClock::fromUTC(), - ); - } + $middleware = new SessionMiddleware($this->config); - public function withSession(ServerRequestInterface|ResponseInterface $message, SessionInterface $session): RequestInterface|ResponseInterface - { - if ($message instanceof ResponseInterface) { - return $this->withResponseSession($message, $session, $this->clock->now()); - } + /** @var RequestHandlerInterface $handler */ + $handler ??= fn (ResponseInterface $response): RequestHandlerInterface => new class ($response) implements RequestHandlerInterface { + public function __construct( + private readonly ResponseInterface $response, + ) { + } - return $this->withRequestSession($message, $session, $this->clock->now()); - } + public function handle(Request $request): ResponseInterface + { + return $this->response; + } + }; - public function get(ServerRequestInterface|ResponseInterface $message): SessionInterface - { - $cookie = $this->getCookieFromMessage($message); + $response ??= $handler->handle($request->withAttribute($this->config->getSessionAttribute(), $session)); + + return $this->appendToken( + $session, + $response, + $token, + $sameOriginRequest, + ); - return $cookie === null - ? DefaultSessionData::newEmptySession() - : $this->cookieToSession($cookie); + return $middleware->process($request, $handler($response)); } - public function getCookieFromMessage(ServerRequestInterface|ResponseInterface $message): SetCookie|Cookie|null + public function getSession(Request $request): SessionInterface { - // TODO: Why we cannot use Cookies::fromRequest() ? - // See: https://github.com/dflydev/dflydev-fig-cookies/issues/57 - if ($message instanceof ServerRequestInterface) { - $cookies = $message->getCookieParams(); + $sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); + $token = $this->requestToToken($request, $sameOriginRequest); - if (! array_key_exists($this->cookie->getName(), $cookies)) { - return null; - } + return LazySession::fromToken($token); + } - $cookieValue = $cookies[$this->cookie->getName()]; - assert(is_string($cookieValue)); + /** + * @throws BadMethodCallException + * @throws InvalidArgumentException + */ + private function appendToken( + SessionInterface $sessionContainer, + Response $response, + Token|null $token, + SameOriginRequest $sameOriginRequest, + ): Response { + $sessionContainerChanged = $sessionContainer->hasChanged(); + + if ($sessionContainerChanged && $sessionContainer->isEmpty()) { + return FigResponseCookies::set($response, $this->getExpirationCookie()); + } - return Cookie::create($this->cookie->getName(), $cookieValue === '' ? null : $cookieValue); + if ($sessionContainerChanged || $this->shouldTokenBeRefreshed($token)) { + return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer, $sameOriginRequest)); } - return SetCookies::fromResponse($message)->get($this->cookie->getName()); + return $response; } - public function cookieToToken(SetCookie|Cookie|null $cookie): UnencryptedToken|null + /** @throws BadMethodCallException */ + private function getTokenCookie(SessionInterface $sessionContainer, SameOriginRequest $sameOriginRequest): SetCookie { - if ($cookie === null) { - return null; - } + $now = $this->config->getClock()->now(); + $expiresAt = $now->add(new DateInterval(sprintf('PT%sS', $this->config->getIdleTimeout()))); + + $jwtConfiguration = $this->config->getJwtConfiguration(); + + $builder = $jwtConfiguration->builder(ChainedFormatter::withUnixTimestampDates()) + ->issuedAt($now) + ->canOnlyBeUsedAfter($now) + ->expiresAt($expiresAt) + ->withClaim(self::SESSION_CLAIM, $sessionContainer); + + $builder = $sameOriginRequest->configure($builder); + + return $this + ->config->getCookie() + ->withValue( + $builder + ->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey()) + ->toString(), + ) + ->withExpires($expiresAt); + } + + public function requestToToken(Request $request, SameOriginRequest|null $sameOriginRequest = null): UnencryptedToken|null + { + $sameOriginRequest ??= new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); - $jwt = $cookie->getValue(); + /** @var array $cookies */ + $cookies = $request->getCookieParams(); + $cookieName = $this->config->getCookie()->getName(); - if ($jwt === null) { + if (! isset($cookies[$cookieName])) { return null; } - if ($jwt === '') { + $cookie = $cookies[$cookieName]; + if ($cookie === '') { return null; } + $jwtConfiguration = $this->config->getJwtConfiguration(); try { - $token = $this->configuration->parser()->parse($jwt); - } catch (Throwable) { + $token = $jwtConfiguration->parser()->parse($cookie); + } catch (InvalidArgumentException) { return null; } @@ -175,92 +169,41 @@ public function cookieToToken(SetCookie|Cookie|null $cookie): UnencryptedToken|n return null; } - $isValid = $this - ->configuration - ->validator() - ->validate( - $token, - new StrictValidAt($this->clock), - new SignedWith($this->configuration->signer(), $this->configuration->verificationKey()), - ); + $constraints = [ + new StrictValidAt($this->config->getClock()), + new SignedWith($jwtConfiguration->signer(), $jwtConfiguration->verificationKey()), + $sameOriginRequest, + ]; - if ($isValid === false) { + if (! $jwtConfiguration->validator()->validate($token, ...$constraints)) { return null; } return $token; } - private function withRequestSession(RequestInterface $request, SessionInterface $session, DateTimeImmutable $now): RequestInterface - { - if ($session->hasChanged() === false) { - return $request; - } - - if (! $this->cookie instanceof Cookie) { - throw new Exception( - 'The default cookie is not a Cookie type.', - ); - } - - return FigRequestCookies::set( - $request, - $this->appendCookieSession($this->cookie, $session, $now), - ); - } - - private function withResponseSession(ResponseInterface $response, SessionInterface $session, DateTimeImmutable $now): ResponseInterface + private function getExpirationCookie(): SetCookie { - if (! $this->cookie instanceof SetCookie) { - throw new Exception( - 'The default cookie is not a SetCookie type.', + return $this + ->config->getCookie() + ->withValue(null) + ->withExpires( + $this->config->getClock() + ->now() + ->modify('-30 days'), ); - } - - if ($session->isEmpty()) { - return FigResponseCookies::set( - $response, - $this->cookie->withExpires($now->modify('-30 days')), - ); - } - - return FigResponseCookies::set( - $response, - $this - ->appendCookieSession( - $this->cookie->withExpires($now->add(new DateInterval(sprintf('PT%sS', $this->idleTimeout)))), - $session, - $now, - ), - ); } - /** @psalm-return ($cookie is SetCookie ? SetCookie : Cookie) */ - private function appendCookieSession(SetCookie|Cookie $cookie, SessionInterface $session, DateTimeImmutable $now): SetCookie|Cookie + private function shouldTokenBeRefreshed(Token|null $token): bool { - $value = $session->isEmpty() - ? null - : $this->configuration->builder(ChainedFormatter::withUnixTimestampDates()) - ->issuedAt($now) - ->canOnlyBeUsedAfter($now) - ->expiresAt($now->add(new DateInterval(sprintf('PT%sS', $this->idleTimeout)))) - ->withClaim(self::SESSION_CLAIM, $session) - ->getToken($this->configuration->signer(), $this->configuration->signingKey()) - ->toString(); - - return $cookie->withValue($value); - } - - private function cookieToSession(SetCookie|Cookie $cookie): SessionInterface - { - $token = $this->cookieToToken($cookie); - if ($token === null) { - return DefaultSessionData::newEmptySession(); + return false; } - return DefaultSessionData::fromDecodedTokenData( - (object) $token->claims()->get(self::SESSION_CLAIM, new stdClass()), + return $token->hasBeenIssuedBefore( + $this->config->getClock() + ->now() + ->sub(new DateInterval(sprintf('PT%sS', $this->config->getRefreshTime()))), ); } } diff --git a/src/Storageless/Session/LazySession.php b/src/Storageless/Session/LazySession.php index 0fb99fd5..f489eddd 100644 --- a/src/Storageless/Session/LazySession.php +++ b/src/Storageless/Session/LazySession.php @@ -20,6 +20,11 @@ namespace PSR7Sessions\Storageless\Session; +use BadMethodCallException; +use Lcobucci\JWT\UnencryptedToken; +use PSR7Sessions\Storageless\Http\SessionMiddleware; +use stdClass; + final class LazySession implements SessionInterface { /** @internal do not access directly: use {@see LazySession::getRealSession} instead */ @@ -47,6 +52,25 @@ public static function fromContainerBuildingCallback(callable $sessionLoader): s return new self($sessionLoader); } + public static function fromToken(UnencryptedToken|null $token): self + { + return self::fromContainerBuildingCallback( + static function () use ($token): SessionInterface { + if (! $token) { + return DefaultSessionData::newEmptySession(); + } + + try { + return DefaultSessionData::fromDecodedTokenData( + (object) $token->claims()->get(SessionMiddleware::SESSION_CLAIM, new stdClass()), + ); + } catch (BadMethodCallException) { + return DefaultSessionData::newEmptySession(); + } + }, + ); + } + /** * {@inheritDoc} */ diff --git a/test/StoragelessTest/Service/StoragelessSessionTest.php b/test/StoragelessTest/Service/StoragelessSessionTest.php deleted file mode 100644 index 561784d8..00000000 --- a/test/StoragelessTest/Service/StoragelessSessionTest.php +++ /dev/null @@ -1,309 +0,0 @@ - */ - public function itThrowsExceptionWhenCookieTypeIsInvalidProvider(): Generator - { - yield [ - static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - 0, - SetCookie::create(SessionMiddleware::DEFAULT_COOKIE), - new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), - ), - new ServerRequest(), - 'The default cookie is not a Cookie type.', - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( - Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), - Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), - 0, - Cookie::create(SessionMiddleware::DEFAULT_COOKIE), - new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), - ), - new Response(), - 'The default cookie is not a SetCookie type.', - ]; - } - - /** - * @param Closure(): SessionStorage $sessionStorageClosure - * - * @dataProvider itThrowsExceptionWhenCookieTypeIsInvalidProvider - */ - public function testItThrowsExceptionWhenCookieTypeIsInvalid( - Closure $sessionStorageClosure, - ServerRequestInterface|ResponseInterface $message, - string $exceptionMessage, - ): void { - $sessionStorage = $sessionStorageClosure(); - - $session = $sessionStorage->get($message); - $session->set('foo', 'bar'); - - $this->expectException(Throwable::class); - $this->expectExceptionMessage($exceptionMessage); - - $sessionStorage->withSession($message, $session); - } - - /** @return Generator */ - public function itCanCustomizeACookieProvider(): Generator - { - yield [ - static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - ), - SessionMiddleware::buildDefaultCookie(), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( - Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), - Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), - ), - SessionMiddleware::buildDefaultCookie(), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - 100, - SessionMiddleware::buildDefaultCookie()->withPath('/foo'), - ), - SessionMiddleware::buildDefaultCookie()->withPath('/foo'), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( - Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), - Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), - 100, - SessionMiddleware::buildDefaultCookie()->withPath('/foo'), - ), - SessionMiddleware::buildDefaultCookie()->withPath('/foo'), - ]; - } - - /** @return Generator */ - public function itCanCustomizeAClockProvider(): Generator - { - yield [ - static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - 0, - ), - SystemClock::fromUTC(), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( - Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), - Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), - 0, - ), - SystemClock::fromUTC(), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - 0, - null, - new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), - ), - new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( - Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), - Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), - 0, - null, - new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), - ), - new FrozenClock(new DateTimeImmutable('2010-05-15 16:00:00')), - ]; - } - - /** - * @param Closure(): SessionStorage $sessionStorageClosure - * - * @dataProvider itCanCustomizeACookieProvider - */ - public function testItCanCustomizeACookie(Closure $sessionStorageClosure, SetCookie|null $cookie): void - { - $sessionStorage = $sessionStorageClosure(); - - $response = new Response(); - - $session = $sessionStorage->get($response); - $session->set('foo', 'bar'); - - $response = $sessionStorage->withSession($response, $session); - - $cookie ??= SessionMiddleware::buildDefaultCookie(); - - $this->assertEquals( - $cookie->getPath(), - $this->getCookie($response)->getPath(), - ); - - $this->assertEquals( - $cookie->getHttpOnly(), - $this->getCookie($response)->getHttpOnly(), - ); - - $this->assertEquals( - $cookie->getSecure(), - $this->getCookie($response)->getSecure(), - ); - } - - /** @return Generator */ - public function itCanCreateACookieWhenItIsNotSetProvider(): Generator - { - yield [ - static fn (): SessionStorage => StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - ), - ]; - - yield [ - static fn (): SessionStorage => StoragelessSession::fromRsaAsymmetricKeyDefaults( - Signer\Key\InMemory::file(__DIR__ . '/../../keys/private_key.pem'), - Signer\Key\InMemory::file(__DIR__ . '/../../keys/public_key.pem'), - ), - ]; - } - - /** - * @param Closure(): SessionStorage $sessionStorageClosure - * - * @dataProvider itCanCreateACookieWhenItIsNotSetProvider - */ - public function testItCanCreateACookieWhenItIsNotSet(Closure $sessionStorageClosure): void - { - $sessionStorage = $sessionStorageClosure(); - - $response = new Response(); - - $session = $sessionStorage->get($response); - $session->set('foo', 'bar'); - - $response = $sessionStorage->withSession($response, $session); - - $this->assertTrue( - $this->getCookie($response)->getSecure(), - ); - - $this->assertTrue( - $this->getCookie($response)->getHttpOnly(), - ); - - $this->assertEquals( - SameSite::lax(), - $this->getCookie($response)->getSameSite(), - ); - - $this->assertEquals( - '/', - $this->getCookie($response)->getPath(), - ); - } - - /** - * @param Closure(): SessionStorage $sessionStorageClosure - * - * @dataProvider itCanCustomizeAClockProvider - */ - public function testItCanUseACustomClock(Closure $sessionStorageClosure, ClockInterface $clock): void - { - $sessionStorage = $sessionStorageClosure(); - - $response = new Response(); - $session = $sessionStorage->get($response); - $session->set('foo', 'bar'); - - $cookie = $this->getCookie($sessionStorage->withSession($response, $session)); - - $this->assertEquals($clock->now()->getTimestamp(), $cookie->getExpires()); - } - - /** @return Generator */ - public function itCanDetectNullOrEmptyJWTProvider(): Generator - { - yield [SessionMiddleware::buildDefaultCookie()->withValue('')]; - - yield [SessionMiddleware::buildDefaultCookie()->withValue(null)]; - } - - /** @dataProvider itCanDetectNullOrEmptyJWTProvider */ - public function testItCanDetectNullOrEmptyJWT(SetCookie $cookie): void - { - $sessionStorage = StoragelessSession::fromSymmetricKeyDefaults( - self::makeRandomSymmetricKey(), - ); - - $this->assertNull($sessionStorage->cookieToToken($cookie)); - } - - private function getCookie(ResponseInterface $response, string $name = SessionMiddleware::DEFAULT_COOKIE): SetCookie - { - return FigResponseCookies::get($response, $name); - } - - private static function makeRandomSymmetricKey(): Signer\Key\InMemory - { - return Signer\Key\InMemory::plainText('test-key_' . base64_encode(random_bytes(128))); - } -} From 646e891ce0d85e2064437b29543b9ebcd28f253c Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 26 Jan 2024 13:58:27 +0100 Subject: [PATCH 4/6] fix: remove cruft --- src/Storageless/Service/StoragelessSession.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Storageless/Service/StoragelessSession.php b/src/Storageless/Service/StoragelessSession.php index 5d2465a4..9798f98a 100644 --- a/src/Storageless/Service/StoragelessSession.php +++ b/src/Storageless/Service/StoragelessSession.php @@ -37,7 +37,6 @@ use Psr\Http\Server\RequestHandlerInterface; use PSR7Sessions\Storageless\Http\ClientFingerprint\SameOriginRequest; use PSR7Sessions\Storageless\Http\Configuration; -use PSR7Sessions\Storageless\Http\SessionMiddleware; use PSR7Sessions\Storageless\Session\LazySession; use PSR7Sessions\Storageless\Session\SessionInterface; @@ -55,11 +54,7 @@ public function __construct( public function appendSession(SessionInterface $session, ServerRequestInterface $request, Response|null $response = null, RequestHandlerInterface|null $handler = null): Response { $sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); - $token = $this->requestToToken($request, $sameOriginRequest); - $middleware = new SessionMiddleware($this->config); - - /** @var RequestHandlerInterface $handler */ $handler ??= fn (ResponseInterface $response): RequestHandlerInterface => new class ($response) implements RequestHandlerInterface { public function __construct( private readonly ResponseInterface $response, @@ -77,19 +72,14 @@ public function handle(Request $request): ResponseInterface return $this->appendToken( $session, $response, - $token, + $this->requestToToken($request, $sameOriginRequest), $sameOriginRequest, ); - - return $middleware->process($request, $handler($response)); } public function getSession(Request $request): SessionInterface { - $sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); - $token = $this->requestToToken($request, $sameOriginRequest); - - return LazySession::fromToken($token); + return LazySession::fromToken($this->requestToToken($request)); } /** From 227aca8d9ee46b289ce4356b1102ec5c1748a267 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 26 Jan 2024 14:15:25 +0100 Subject: [PATCH 5/6] fix: minor meaningless changes --- .../Service/StoragelessSession.php | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/src/Storageless/Service/StoragelessSession.php b/src/Storageless/Service/StoragelessSession.php index 9798f98a..3c67145f 100644 --- a/src/Storageless/Service/StoragelessSession.php +++ b/src/Storageless/Service/StoragelessSession.php @@ -53,7 +53,7 @@ public function __construct( public function appendSession(SessionInterface $session, ServerRequestInterface $request, Response|null $response = null, RequestHandlerInterface|null $handler = null): Response { - $sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); + $sameOriginRequest = $this->getSameOriginRequest($request); $handler ??= fn (ResponseInterface $response): RequestHandlerInterface => new class ($response) implements RequestHandlerInterface { public function __construct( @@ -82,6 +82,42 @@ public function getSession(Request $request): SessionInterface return LazySession::fromToken($this->requestToToken($request)); } + public function requestToToken(Request $request, SameOriginRequest|null $sameOriginRequest = null): UnencryptedToken|null + { + /** @var array $cookies */ + $cookies = $request->getCookieParams(); + $cookieName = $this->config->getCookie()->getName(); + + $cookie = $cookies[$cookieName] ?? ''; + + if ($cookie === '') { + return null; + } + + $jwtConfiguration = $this->config->getJwtConfiguration(); + try { + $token = $jwtConfiguration->parser()->parse($cookie); + } catch (InvalidArgumentException) { + return null; + } + + if (! $token instanceof UnencryptedToken) { + return null; + } + + $constraints = [ + new StrictValidAt($this->config->getClock()), + new SignedWith($jwtConfiguration->signer(), $jwtConfiguration->verificationKey()), + $sameOriginRequest ?? $this->getSameOriginRequest($request), + ]; + + if (! $jwtConfiguration->validator()->validate($token, ...$constraints)) { + return null; + } + + return $token; + } + /** * @throws BadMethodCallException * @throws InvalidArgumentException @@ -95,7 +131,18 @@ private function appendToken( $sessionContainerChanged = $sessionContainer->hasChanged(); if ($sessionContainerChanged && $sessionContainer->isEmpty()) { - return FigResponseCookies::set($response, $this->getExpirationCookie()); + return FigResponseCookies::set( + $response, + $this + ->config + ->getCookie() + ->withValue(null) + ->withExpires( + $this->config->getClock() + ->now() + ->modify('-30 days'), + ), + ); } if ($sessionContainerChanged || $this->shouldTokenBeRefreshed($token)) { @@ -105,6 +152,11 @@ private function appendToken( return $response; } + private function getSameOriginRequest(Request $request): SameOriginRequest + { + return new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); + } + /** @throws BadMethodCallException */ private function getTokenCookie(SessionInterface $sessionContainer, SameOriginRequest $sameOriginRequest): SetCookie { @@ -131,59 +183,6 @@ private function getTokenCookie(SessionInterface $sessionContainer, SameOriginRe ->withExpires($expiresAt); } - public function requestToToken(Request $request, SameOriginRequest|null $sameOriginRequest = null): UnencryptedToken|null - { - $sameOriginRequest ??= new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request); - - /** @var array $cookies */ - $cookies = $request->getCookieParams(); - $cookieName = $this->config->getCookie()->getName(); - - if (! isset($cookies[$cookieName])) { - return null; - } - - $cookie = $cookies[$cookieName]; - if ($cookie === '') { - return null; - } - - $jwtConfiguration = $this->config->getJwtConfiguration(); - try { - $token = $jwtConfiguration->parser()->parse($cookie); - } catch (InvalidArgumentException) { - return null; - } - - if (! $token instanceof UnencryptedToken) { - return null; - } - - $constraints = [ - new StrictValidAt($this->config->getClock()), - new SignedWith($jwtConfiguration->signer(), $jwtConfiguration->verificationKey()), - $sameOriginRequest, - ]; - - if (! $jwtConfiguration->validator()->validate($token, ...$constraints)) { - return null; - } - - return $token; - } - - private function getExpirationCookie(): SetCookie - { - return $this - ->config->getCookie() - ->withValue(null) - ->withExpires( - $this->config->getClock() - ->now() - ->modify('-30 days'), - ); - } - private function shouldTokenBeRefreshed(Token|null $token): bool { if ($token === null) { From e38d233b32dab3008da2b09ed81866f829a6f916 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 26 Jan 2024 14:33:04 +0100 Subject: [PATCH 6/6] fix: minor meaningless changes --- src/Storageless/Service/SessionStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Storageless/Service/SessionStorage.php b/src/Storageless/Service/SessionStorage.php index fb2714bf..b201d809 100644 --- a/src/Storageless/Service/SessionStorage.php +++ b/src/Storageless/Service/SessionStorage.php @@ -27,7 +27,7 @@ interface SessionStorage { - public function appendSession(SessionInterface $session, ServerRequestInterface $request, ResponseInterface|null $response, RequestHandlerInterface|null $handler = null): ResponseInterface; + public function appendSession(SessionInterface $session, ServerRequestInterface $request, ResponseInterface|null $response = null, RequestHandlerInterface|null $handler = null): ResponseInterface; public function getSession(ServerRequestInterface $request): SessionInterface; }