Skip to content

Commit

Permalink
refactor: let SessionMiddleware use StoragelessSession service
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed May 7, 2023
1 parent 48e6e17 commit 5b3c6e1
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 210 deletions.
24 changes: 15 additions & 9 deletions examples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Service\StoragelessSession;
use PSR7Sessions\Storageless\Session\SessionInterface;

require_once __DIR__ . '/../vendor/autoload.php';
Expand All @@ -41,17 +42,22 @@
// simply run `php -S localhost:8888 index.php`
// then point your browser at `http://localhost:8888/`

$clock = SystemClock::fromUTC();
$sessionMiddleware = new SessionMiddleware(
Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText('c9UA8QKLSmDEn4DhNeJIad/4JugZd/HvrjyKrS0jOes='), // // signature key (important: change this to your own)
new StoragelessSession(
Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText('c9UA8QKLSmDEn4DhNeJIad/4JugZd/HvrjyKrS0jOes='), // signature key (important: change this to your own)
),
1200,
SetCookie::create('an-example-cookie-name')
->withSecure(false) // false on purpose, unless you have https locally
->withHttpOnly(true)
->withPath('/'),
$clock,
),
SetCookie::create('an-example-cookie-name')
->withSecure(false) // false on purpose, unless you have https locally
->withHttpOnly(true)
->withPath('/'),
1200, // 20 minutes
new SystemClock(new DateTimeZone(date_default_timezone_get())),
1200, // 20 minutes,
$clock,
);

$myMiddleware = new class implements RequestHandlerInterface {
Expand Down
165 changes: 30 additions & 135 deletions src/Storageless/Http/SessionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,21 @@

use BadMethodCallException;
use DateInterval;
use DateTimeZone;
use Dflydev\FigCookies\FigResponseCookies;
use Dflydev\FigCookies\Modifier\SameSite;
use Dflydev\FigCookies\SetCookie;
use InvalidArgumentException;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
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 OutOfBoundsException;
use Psr\Clock\ClockInterface;
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\Session\DefaultSessionData;
use PSR7Sessions\Storageless\Service\StoragelessSession;
use PSR7Sessions\Storageless\Session\LazySession;
use PSR7Sessions\Storageless\Session\SessionInterface;
use stdClass;

use function date_default_timezone_get;
use function sprintf;

final class SessionMiddleware implements MiddlewareInterface
Expand All @@ -55,35 +45,32 @@ final class SessionMiddleware implements MiddlewareInterface
public const SESSION_ATTRIBUTE = 'session';
public const DEFAULT_COOKIE = '__Secure-slsession';
public const DEFAULT_REFRESH_TIME = 60;
private Configuration $config;
private SetCookie $defaultCookie;

/** @param literal-string $sessionAttribute */
public function __construct(
Configuration $configuration,
SetCookie $defaultCookie,
private int $idleTimeout,
private Clock $clock,
private int $refreshTime = self::DEFAULT_REFRESH_TIME,
private string $sessionAttribute = self::SESSION_ATTRIBUTE,
private readonly StoragelessSession $sessionStorage,
private readonly int $idleTimeout,
private readonly ClockInterface $clock,
private readonly int $refreshTime = self::DEFAULT_REFRESH_TIME,
private readonly string $sessionAttribute = self::SESSION_ATTRIBUTE,
) {
$this->config = $configuration;
$this->defaultCookie = clone $defaultCookie;
}

/**
* This constructor simplifies instantiation when using HTTPS (REQUIRED!) and symmetric key encryption
*/
public static function fromSymmetricKeyDefaults(Signer\Key $symmetricKey, int $idleTimeout): self
{
$clock = SystemClock::fromUTC();

return new self(
Configuration::forSymmetricSigner(
new Signer\Hmac\Sha256(),
StoragelessSession::fromSymmetricKeyDefaults(
$symmetricKey,
$idleTimeout,
self::buildDefaultCookie(),
$clock,
),
self::buildDefaultCookie(),
$idleTimeout,
new SystemClock(new DateTimeZone(date_default_timezone_get())),
$clock,
);
}

Expand All @@ -96,15 +83,18 @@ public static function fromRsaAsymmetricKeyDefaults(
Signer\Key $publicRsaKey,
int $idleTimeout,
): self {
$clock = SystemClock::fromUTC();

return new self(
Configuration::forAsymmetricSigner(
new Signer\Rsa\Sha256(),
StoragelessSession::fromRsaAsymmetricKeyDefaults(
$privateRsaKey,
$publicRsaKey,
$idleTimeout,
self::buildDefaultCookie(),
$clock,
),
self::buildDefaultCookie(),
$idleTimeout,
new SystemClock(new DateTimeZone(date_default_timezone_get())),
$clock,
);
}

Expand All @@ -125,91 +115,28 @@ public static function buildDefaultCookie(): SetCookie
*/
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$token = $this->parseToken($request);
$sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token): SessionInterface {
return $this->extractSessionContainer($token);
});
$session = LazySession::fromContainerBuildingCallback(
fn (): SessionInterface => $this->sessionStorage->get($request)
);

return $this->appendToken(
$sessionContainer,
$handler->handle($request->withAttribute($this->sessionAttribute, $sessionContainer)),
$token,
$session,
$handler->handle($request->withAttribute($this->sessionAttribute, $session)),
$this->sessionStorage->cookieToToken($this->sessionStorage->getCookieFromMessage($request)),
);
}

/**
* Extract the token from the given request object
*/
private function parseToken(Request $request): UnencryptedToken|null
{
/** @var array<string, string> $cookies */
$cookies = $request->getCookieParams();
$cookieName = $this->defaultCookie->getName();

if (! isset($cookies[$cookieName])) {
return null;
}

$cookie = $cookies[$cookieName];
if ($cookie === '') {
return null;
}

try {
$token = $this->config->parser()->parse($cookie);
} catch (InvalidArgumentException) {
return null;
}

if (! $token instanceof UnencryptedToken) {
return null;
}

$constraints = [
new StrictValidAt($this->clock),
new SignedWith($this->config->signer(), $this->config->verificationKey()),
];

if (! $this->config->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): Response
{
$sessionContainerChanged = $sessionContainer->hasChanged();

if ($sessionContainerChanged && $sessionContainer->isEmpty()) {
return FigResponseCookies::set($response, $this->getExpirationCookie());
if ($sessionContainer->hasChanged() === false && $this->shouldTokenBeRefreshed($token) === false) {
return $response;
}

if ($sessionContainerChanged || $this->shouldTokenBeRefreshed($token)) {
return FigResponseCookies::set($response, $this->getTokenCookie($sessionContainer));
}

return $response;
return $this->sessionStorage->withSession($response, $sessionContainer);
}

private function shouldTokenBeRefreshed(Token|null $token): bool
Expand All @@ -224,36 +151,4 @@ private function shouldTokenBeRefreshed(Token|null $token): bool
->sub(new DateInterval(sprintf('PT%sS', $this->refreshTime))),
);
}

/** @throws BadMethodCallException */
private function getTokenCookie(SessionInterface $sessionContainer): SetCookie
{
$now = $this->clock->now();
$expiresAt = $now->add(new DateInterval(sprintf('PT%sS', $this->idleTimeout)));

return $this
->defaultCookie
->withValue(
$this->config->builder(ChainedFormatter::withUnixTimestampDates())
->issuedAt($now)
->canOnlyBeUsedAfter($now)
->expiresAt($expiresAt)
->withClaim(self::SESSION_CLAIM, $sessionContainer)
->getToken($this->config->signer(), $this->config->signingKey())
->toString(),
)
->withExpires($expiresAt);
}

private function getExpirationCookie(): SetCookie
{
return $this
->defaultCookie
->withValue(null)
->withExpires(
$this->clock
->now()
->modify('-30 days'),
);
}
}
Loading

0 comments on commit 5b3c6e1

Please sign in to comment.