Skip to content

Instantly share code, notes, and snippets.

@drupol
Created April 4, 2023 12:13
Show Gist options
  • Save drupol/345059c878981e13f47339ae970e36ca to your computer and use it in GitHub Desktop.
Save drupol/345059c878981e13f47339ae970e36ca to your computer and use it in GitHub Desktop.
Request for comments - Into making a generic service - version 2
<?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