Created
April 4, 2023 12:13
-
-
Save drupol/345059c878981e13f47339ae970e36ca to your computer and use it in GitHub Desktop.
Request for comments - Into making a generic service - version 2
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 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 Psr\Clock\ClockInterface; | |
use Psr\Http\Message\RequestInterface; | |
use Psr\Http\Message\ResponseInterface; | |
use PSR7Sessions\Storageless\Session\DefaultSessionData; | |
use PSR7Sessions\Storageless\Session\SessionInterface; | |
final class StoragelessManager implements SessionStorage | |
{ | |
private const SESSION_CLAIM = 'data'; | |
private const DEFAULT_COOKIE = '__Secure-slsession'; | |
public function __construct( | |
private readonly Configuration $configuration, | |
private readonly int $idleTimeout, | |
private readonly ClockInterface $clock, | |
) {} | |
public static function fromSymmetricKeyDefaults(Key $symmetricKey, int $idleTimeout, ?ClockInterface $clock = null): self { | |
return new self( | |
Configuration::forSymmetricSigner( | |
new Sha256(), | |
$symmetricKey, | |
), | |
$idleTimeout, | |
$clock ?? SystemClock::fromSystemTimezone(), | |
); | |
} | |
public static function fromRsaAsymmetricKeyDefaults( | |
Key $privateRsaKey, | |
Key $publicRsaKey, | |
int $idleTimeout, | |
?ClockInterface $clock = null | |
): self { | |
return new self( | |
Configuration::forAsymmetricSigner( | |
new Sha256(), | |
$privateRsaKey, | |
$publicRsaKey, | |
), | |
$idleTimeout, | |
$clock ?? SystemClock::fromSystemTimezone(), | |
); | |
} | |
public function store(ResponseInterface $request, SessionInterface $session): ResponseInterface { | |
return FigResponseCookies::set($request, $this->sessionToCookie($session)); | |
} | |
public function get(RequestInterface $request): SessionInterface { | |
$cookie = Cookies::fromRequest($request)->get(self::DEFAULT_COOKIE); | |
if (null === $cookie) { | |
return DefaultSessionData::newEmptySession(); | |
} | |
try { | |
$session = $this->cookieToSession($cookie); | |
} catch (\Throwable $exception) { | |
$session = DefaultSessionData::newEmptySession(); | |
} | |
return $session; | |
} | |
private function cookieToSession(SetCookie|Cookie $cookie): SessionInterface { | |
$token = $this->configuration->parser()->parse($cookie->getValue()); | |
if (! $token instanceof UnencryptedToken) { | |
return DefaultSessionData::newEmptySession(); | |
} | |
$constraints = [ | |
new StrictValidAt($this->clock), | |
new SignedWith($this->configuration->signer(), $this->configuration->verificationKey()), | |
]; | |
if (false === $this->configuration->validator()->validate($token, ...$constraints)) { | |
return DefaultSessionData::newEmptySession(); | |
} | |
try { | |
$session = DefaultSessionData::fromDecodedTokenData( | |
(object) $token->claims()->get(self::SESSION_CLAIM, new \stdClass()), | |
); | |
} catch (\Throwable) { | |
$session = DefaultSessionData::newEmptySession(); | |
} | |
return $session; | |
} | |
private function sessionToCookie(SessionInterface $session): SetCookie | |
{ | |
$now = $this->clock->now(); | |
$expiresAt = $now->add(new \DateInterval(sprintf('PT%sS', $this->idleTimeout))); | |
return SetCookie::create(self::DEFAULT_COOKIE) | |
->withSecure(true) | |
->withHttpOnly(true) | |
->withSameSite(SameSite::lax()) | |
->withPath('/') | |
->withExpires($expiresAt) | |
->withValue( | |
$this->configuration->builder(ChainedFormatter::withUnixTimestampDates()) | |
->issuedAt($now) | |
->canOnlyBeUsedAfter($now) | |
->expiresAt($expiresAt) | |
->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