Skip to content

Instantly share code, notes, and snippets.

@drupol
Last active April 5, 2023 11:14
Show Gist options
  • Save drupol/a989c34a573c9e514f62dd1de86e7004 to your computer and use it in GitHub Desktop.
Save drupol/a989c34a573c9e514f62dd1de86e7004 to your computer and use it in GitHub Desktop.
Request for comments - Into making a generic service (version 4)
<?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