Skip to content

Commit

Permalink
refactor: (wip) change almost everything
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed Jan 26, 2024
1 parent b65b060 commit 1d54d46
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 646 deletions.
1 change: 0 additions & 1 deletion examples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
use Psr\Http\Server\RequestHandlerInterface;
use PSR7Sessions\Storageless\Http\Configuration;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Service\StoragelessSession;
use PSR7Sessions\Storageless\Session\SessionInterface;

require_once __DIR__ . '/../vendor/autoload.php';
Expand Down
167 changes: 9 additions & 158 deletions src/Storageless/Http/SessionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,26 @@
namespace PSR7Sessions\Storageless\Http;

use BadMethodCallException;
use DateInterval;
use Dflydev\FigCookies\FigResponseCookies;
use Dflydev\FigCookies\SetCookie;
use InvalidArgumentException;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use OutOfBoundsException;
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\Http\ClientFingerprint\SameOriginRequest;
use PSR7Sessions\Storageless\Session\DefaultSessionData;
use PSR7Sessions\Storageless\Service\StoragelessSession;
use PSR7Sessions\Storageless\Session\LazySession;
use PSR7Sessions\Storageless\Session\SessionInterface;
use stdClass;

use function sprintf;

/** @immutable */
final class SessionMiddleware implements MiddlewareInterface
{
public const SESSION_CLAIM = 'session-data';
public const SESSION_ATTRIBUTE = 'session';

private readonly StoragelessSession $service;

public function __construct(
private readonly Configuration $config,
) {
$this->service = new StoragelessSession($config);
}

/**
Expand All @@ -62,149 +51,11 @@ public function __construct(
*/
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$sameOriginRequest = new SameOriginRequest($this->config->getClientFingerprintConfiguration(), $request);
$token = $this->parseToken($request, $sameOriginRequest);
$sessionContainer = LazySession::fromContainerBuildingCallback(function () use ($token): SessionInterface {
return $this->extractSessionContainer($token);
});

return $this->appendToken(
$sessionContainer,
$handler->handle($request->withAttribute($this->config->getSessionAttribute(), $sessionContainer)),
$token,
$sameOriginRequest,
return $this->service->appendSession(
LazySession::fromToken($this->service->requestToToken($request)),
$request,
null,
$handler,
);
}

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

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

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

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

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

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

if (! $jwtConfiguration->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,
SameOriginRequest $sameOriginRequest,
): Response {
$sessionContainerChanged = $sessionContainer->hasChanged();

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

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

return $response;
}

private function shouldTokenBeRefreshed(Token|null $token): bool
{
if ($token === null) {
return false;
}

return $token->hasBeenIssuedBefore(
$this->config->getClock()
->now()
->sub(new DateInterval(sprintf('PT%sS', $this->config->getRefreshTime()))),
);
}

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

$jwtConfiguration = $this->config->getJwtConfiguration();

$builder = $jwtConfiguration->builder(ChainedFormatter::withUnixTimestampDates())
->issuedAt($now)
->canOnlyBeUsedAfter($now)
->expiresAt($expiresAt)
->withClaim(self::SESSION_CLAIM, $sessionContainer);

$builder = $sameOriginRequest->configure($builder);

return $this
->config->getCookie()
->withValue(
$builder
->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey())
->toString(),
)
->withExpires($expiresAt);
}

private function getExpirationCookie(): SetCookie
{
return $this
->config->getCookie()
->withValue(null)
->withExpires(
$this->config->getClock()
->now()
->modify('-30 days'),
);
}
}
7 changes: 3 additions & 4 deletions src/Storageless/Service/SessionStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@

namespace PSR7Sessions\Storageless\Service;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use PSR7Sessions\Storageless\Session\SessionInterface;

interface SessionStorage
{
/** @psalm-return ($message is ResponseInterface ? ResponseInterface : RequestInterface) */
public function withSession(ServerRequestInterface|ResponseInterface $message, SessionInterface $session): RequestInterface|ResponseInterface;
public function appendSession(SessionInterface $session, ServerRequestInterface $request, ResponseInterface|null $response, RequestHandlerInterface|null $handler = null): ResponseInterface;

public function get(ServerRequestInterface|ResponseInterface $message): SessionInterface;
public function getSession(ServerRequestInterface $request): SessionInterface;
}
Loading

0 comments on commit 1d54d46

Please sign in to comment.