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 5, 2023
1 parent 17e86f3 commit 3c68095
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 193 deletions.
13 changes: 4 additions & 9 deletions examples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand All @@ -41,17 +39,14 @@
// simply run `php -S localhost:8888 index.php`
// then point your browser at `http://localhost:8888/`

$sessionMiddleware = new SessionMiddleware(
Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText('c9UA8QKLSmDEn4DhNeJIad/4JugZd/HvrjyKrS0jOes='), // // signature key (important: change this to your own)
),
$sessionMiddleware = SessionMiddleware::fromSymmetricKeyDefaults(
InMemory::plainText('c9UA8QKLSmDEn4DhNeJIad/4JugZd/HvrjyKrS0jOes='), // // signature key (important: change this to your own)
1200, // 20 minutes
SystemClock::fromUTC(),
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())),
);

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

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 +46,37 @@ 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 Configuration $config,
private readonly int $idleTimeout,
private readonly int $refreshTime,
private readonly string $sessionAttribute,
private readonly ClockInterface $clock,
) {
$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
public static function fromSymmetricKeyDefaults(Signer\Key $symmetricKey, int $idleTimeout, ClockInterface|null $clock = null, SetCookie|null $cookie = null): self
{
return new self(
StoragelessSession::fromSymmetricKeyDefaults(
$symmetricKey,
$idleTimeout,
$cookie ?? self::buildDefaultCookie(),
$clock ?? SystemClock::fromUTC(),
),
Configuration::forSymmetricSigner(
new Signer\Hmac\Sha256(),
$symmetricKey,
),
self::buildDefaultCookie(),
$idleTimeout,
new SystemClock(new DateTimeZone(date_default_timezone_get())),
self::DEFAULT_REFRESH_TIME,
self::SESSION_ATTRIBUTE,
$clock ?? SystemClock::fromUTC(),
);
}

Expand All @@ -95,16 +88,26 @@ public static function fromRsaAsymmetricKeyDefaults(
Signer\Key $privateRsaKey,
Signer\Key $publicRsaKey,
int $idleTimeout,
ClockInterface|null $clock = null,
SetCookie|null $cookie = null,
): self {
return new self(
StoragelessSession::fromRsaAsymmetricKeyDefaults(
$privateRsaKey,
$publicRsaKey,
$idleTimeout,
$cookie ?? self::buildDefaultCookie(),
$clock ?? SystemClock::fromUTC(),
),
Configuration::forAsymmetricSigner(
new Signer\Rsa\Sha256(),
$privateRsaKey,
$publicRsaKey,
),
self::buildDefaultCookie(),
$idleTimeout,
new SystemClock(new DateTimeZone(date_default_timezone_get())),
self::DEFAULT_REFRESH_TIME,
self::SESSION_ATTRIBUTE,
$clock ?? SystemClock::fromUTC(),
);
}

Expand All @@ -125,91 +128,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 +164,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 3c68095

Please sign in to comment.