Last active
April 5, 2023 11:14
-
-
Save drupol/a989c34a573c9e514f62dd1de86e7004 to your computer and use it in GitHub Desktop.
Request for comments - Into making a generic service (version 4)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace PSR7Sessions\Storageless\Service; | |
use Closure; | |
use Dflydev\FigCookies\Cookie; | |
use Dflydev\FigCookies\Cookies; | |
use Dflydev\FigCookies\FigResponseCookies; | |
use Dflydev\FigCookies\Modifier\SameSite; | |
use Dflydev\FigCookies\SetCookie; | |
use Lcobucci\Clock\SystemClock; | |
use Lcobucci\JWT\Configuration; | |
use Lcobucci\JWT\Encoding\ChainedFormatter; | |
use Lcobucci\JWT\Signer\Key; | |
use Lcobucci\JWT\Signer\Hmac\Sha256; | |
use Lcobucci\JWT\UnencryptedToken; | |
use Lcobucci\JWT\Validation\Constraint\SignedWith; | |
use Lcobucci\JWT\Validation\Constraint\StrictValidAt; | |
use PSR7Sessions\Storageless\Http\ClientFingerprint\SameOriginRequest; | |
use Psr\Clock\ClockInterface; | |
use Psr\Http\Message\RequestInterface; | |
use Psr\Http\Message\ResponseInterface; | |
use PSR7Sessions\Storageless\Session\DefaultSessionData; | |
use PSR7Sessions\Storageless\Session\SessionInterface; | |
use PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration as FingerprintConfig; | |
final class StoragelessManager implements WithSession | |
{ | |
private const DEFAULT_COOKIE = '__Secure-slsession'; | |
private const SESSION_CLAIM = 'data'; | |
public function __construct( | |
private readonly Configuration $configuration, | |
private readonly int $idleTimeout, | |
private readonly SetCookie $cookie, | |
private readonly FingerprintConfig $fingerprintConfig, | |
private readonly SessionInterface $session, | |
private readonly ClockInterface $clock | |
) {} | |
public static function fromSymmetricKeyDefaults( | |
Key $symmetricKey, | |
int $idleTimeout = 300, | |
?SetCookie $cookie = null, | |
?FingerprintConfig $fingerprintConfig = null, | |
?SessionInterface $session = null, | |
?ClockInterface $clock = null | |
): self { | |
return new self( | |
Configuration::forSymmetricSigner( | |
new Sha256(), | |
$symmetricKey, | |
), | |
$idleTimeout, | |
$cookie ?? SetCookie::create(self::DEFAULT_COOKIE) | |
->withSecure(true) | |
->withHttpOnly(true) | |
->withSameSite(SameSite::lax()) | |
->withPath('/'), | |
$fingerprintConfig ?? FingerprintConfig::forIpAndUserAgent(), | |
$session ?? DefaultSessionData::newEmptySession(), | |
$clock ?? SystemClock::fromUTC(), | |
); | |
} | |
public static function fromRsaAsymmetricKeyDefaults( | |
Key $privateRsaKey, | |
Key $publicRsaKey, | |
int $idleTimeout = 300, | |
?SetCookie $cookie = null, | |
?ClockInterface $clock = null, | |
?FingerprintConfig $fingerprintConfig = null, | |
?SessionInterface $session = null | |
): self { | |
return new self( | |
Configuration::forAsymmetricSigner( | |
new Sha256(), | |
$privateRsaKey, | |
$publicRsaKey, | |
), | |
$idleTimeout, | |
$cookie ?? SetCookie::create(self::DEFAULT_COOKIE) | |
->withSecure(true) | |
->withHttpOnly(true) | |
->withSameSite(SameSite::lax()) | |
->withPath('/'), | |
$fingerprintConfig ?? FingerprintConfig::forIpAndUserAgent(), | |
$session ?? DefaultSessionData::newEmptySession(), | |
$clock ?? SystemClock::fromUTC(), | |
); | |
} | |
public function handle( | |
RequestInterface $request, | |
ResponseInterface $response, | |
Closure $callback | |
): ResponseInterface { | |
$session = $this->getFromRequest($request); | |
// This callback is supposed to alter the $session, thus, it is | |
// not supposed to return anything. On top of that, SessionInterface is | |
// stateful, so, it's not needed at all. | |
$callback($session); | |
return $this->withResponse($response, $request, $session); | |
} | |
private function withResponse( | |
ResponseInterface $response, | |
RequestInterface $request, | |
SessionInterface $session | |
): ResponseInterface { | |
return FigResponseCookies::set( | |
$response, | |
$this->sessionToSetCookie($request, $response, $session) | |
); | |
} | |
private function getFromRequest(RequestInterface $request): SessionInterface { | |
$cookie = Cookies::fromRequest($request)->get(self::DEFAULT_COOKIE); | |
if (null === $cookie) { | |
return $this->session; | |
} | |
try { | |
$session = $this->cookieToSession($cookie, $request); | |
} catch (\Throwable) { | |
$session = $this->session; | |
} | |
return $session; | |
} | |
private function cookieToSession( | |
Cookie $cookie, | |
RequestInterface $request | |
): SessionInterface { | |
$token = $this->configuration->parser()->parse($cookie->getValue()); | |
if (! $token instanceof UnencryptedToken) { | |
return $this->session; | |
} | |
$isValidated = $this | |
->configuration | |
->validator() | |
->validate( | |
$token, | |
new StrictValidAt($this->clock), | |
new SignedWith( | |
$this->configuration->signer(), | |
$this->configuration->verificationKey() | |
), | |
new SameOriginRequest($this->fingerprintConfig, $request) | |
); | |
if (false === $isValidated) { | |
return $this->session; | |
} | |
try { | |
$session = DefaultSessionData::fromDecodedTokenData( | |
(object) $token->claims()->get(self::SESSION_CLAIM, new \stdClass()), | |
); | |
} catch (\Throwable) { | |
$session = $this->session; | |
} | |
return $session; | |
} | |
private function sessionToSetCookie( | |
RequestInterface $request, | |
ResponseInterface $response, | |
SessionInterface $session | |
): SetCookie { | |
$now = $this->clock->now(); | |
$expiresAt = $now->add(new \DateInterval(sprintf('PT%sS', $this->idleTimeout))); | |
return $this | |
->cookie | |
->withExpires($expiresAt) | |
->withValue( | |
(new SameOriginRequest($this->fingerprintConfig, $request)) | |
->configure( | |
$this | |
->configuration | |
->builder(ChainedFormatter::withUnixTimestampDates()) | |
->canOnlyBeUsedAfter($now) | |
->expiresAt($expiresAt) | |
->issuedAt($now) | |
->withClaim(self::SESSION_CLAIM, $session) | |
) | |
->getToken($this->configuration->signer(), $this->configuration->signingKey()) | |
->toString(), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment