Skip to content

Commit

Permalink
Merge pull request #570 from Slamdunk/config
Browse files Browse the repository at this point in the history
Add Config object, drop factory methods
  • Loading branch information
Slamdunk authored Jul 24, 2023
2 parents a6404a3 + 029d039 commit 8eb29d2
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 503 deletions.
23 changes: 12 additions & 11 deletions examples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
use Laminas\Diactoros\Response;
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;
use Psr\Http\Server\RequestHandlerInterface;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\SessionMiddlewareConfiguration;
use PSR7Sessions\Storageless\Session\SessionInterface;

require_once __DIR__ . '/../vendor/autoload.php';
Expand All @@ -42,16 +42,17 @@
// 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)
),
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())),
(new SessionMiddlewareConfiguration(
Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText('c9UA8QKLSmDEn4DhNeJIad/4JugZd/HvrjyKrS0jOes='), // // signature key (important: change this to your own)
),
))->withCookie(
SetCookie::create('an-example-cookie-name')
->withSecure(false) // false on purpose, unless you have https locally
->withHttpOnly(true)
->withPath('/'),
)->withIdleTimeout(1200), // 20 minutes
);

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

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;
Expand All @@ -46,75 +40,17 @@
use PSR7Sessions\Storageless\Session\SessionInterface;
use stdClass;

use function date_default_timezone_get;
use function sprintf;

/** @immutable */
final class SessionMiddleware implements MiddlewareInterface
{
public const SESSION_CLAIM = 'session-data';
public const SESSION_ATTRIBUTE = 'session';
public const DEFAULT_COOKIE = '__Secure-slsession';
public const DEFAULT_REFRESH_TIME = 60;
private Configuration $config;
private SetCookie $defaultCookie;
public const SESSION_CLAIM = 'session-data';
public const SESSION_ATTRIBUTE = 'session';

/** @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 SessionMiddlewareConfiguration $config,
) {
$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
{
return new self(
Configuration::forSymmetricSigner(
new Signer\Hmac\Sha256(),
$symmetricKey,
),
self::buildDefaultCookie(),
$idleTimeout,
new SystemClock(new DateTimeZone(date_default_timezone_get())),
);
}

/**
* This constructor simplifies instantiation when using HTTPS (REQUIRED!) and asymmetric key encryption
* based on RSA keys
*/
public static function fromRsaAsymmetricKeyDefaults(
Signer\Key $privateRsaKey,
Signer\Key $publicRsaKey,
int $idleTimeout,
): self {
return new self(
Configuration::forAsymmetricSigner(
new Signer\Rsa\Sha256(),
$privateRsaKey,
$publicRsaKey,
),
self::buildDefaultCookie(),
$idleTimeout,
new SystemClock(new DateTimeZone(date_default_timezone_get())),
);
}

public static function buildDefaultCookie(): SetCookie
{
return SetCookie::create(self::DEFAULT_COOKIE)
->withSecure(true)
->withHttpOnly(true)
->withSameSite(SameSite::lax())
->withPath('/');
}

/**
Expand All @@ -132,7 +68,7 @@ public function process(Request $request, RequestHandlerInterface $handler): Res

return $this->appendToken(
$sessionContainer,
$handler->handle($request->withAttribute($this->sessionAttribute, $sessionContainer)),
$handler->handle($request->withAttribute($this->config->getSessionAttribute(), $sessionContainer)),
$token,
);
}
Expand All @@ -144,7 +80,7 @@ private function parseToken(Request $request): UnencryptedToken|null
{
/** @var array<string, string> $cookies */
$cookies = $request->getCookieParams();
$cookieName = $this->defaultCookie->getName();
$cookieName = $this->config->getCookie()->getName();

if (! isset($cookies[$cookieName])) {
return null;
Expand All @@ -155,8 +91,9 @@ private function parseToken(Request $request): UnencryptedToken|null
return null;
}

$jwtConfiguration = $this->config->getJwtConfiguration();
try {
$token = $this->config->parser()->parse($cookie);
$token = $jwtConfiguration->parser()->parse($cookie);
} catch (InvalidArgumentException) {
return null;
}
Expand All @@ -166,11 +103,11 @@ private function parseToken(Request $request): UnencryptedToken|null
}

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

if (! $this->config->validator()->validate($token, ...$constraints)) {
if (! $jwtConfiguration->validator()->validate($token, ...$constraints)) {
return null;
}

Expand Down Expand Up @@ -219,27 +156,29 @@ private function shouldTokenBeRefreshed(Token|null $token): bool
}

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

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

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

return $this
->defaultCookie
->config->getCookie()
->withValue(
$this->config->builder(ChainedFormatter::withUnixTimestampDates())
$jwtConfiguration->builder(ChainedFormatter::withUnixTimestampDates())
->issuedAt($now)
->canOnlyBeUsedAfter($now)
->expiresAt($expiresAt)
->withClaim(self::SESSION_CLAIM, $sessionContainer)
->getToken($this->config->signer(), $this->config->signingKey())
->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey())
->toString(),
)
->withExpires($expiresAt);
Expand All @@ -248,10 +187,10 @@ private function getTokenCookie(SessionInterface $sessionContainer): SetCookie
private function getExpirationCookie(): SetCookie
{
return $this
->defaultCookie
->config->getCookie()
->withValue(null)
->withExpires(
$this->clock
$this->config->getClock()
->now()
->modify('-30 days'),
);
Expand Down
138 changes: 138 additions & 0 deletions src/Storageless/Http/SessionMiddlewareConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license.
*/

declare(strict_types=1);

namespace PSR7Sessions\Storageless\Http;

use Dflydev\FigCookies\Modifier\SameSite;
use Dflydev\FigCookies\SetCookie;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;

/** @immutable */
final class SessionMiddlewareConfiguration
{
private Configuration $jwtConfiguration;
private Clock $clock;
private SetCookie $cookie;
/** @var positive-int */
private int $idleTimeout = 43200;
/** @var positive-int */
private int $refreshTime = 60;
/** @var literal-string */
private string $sessionAttribute = SessionMiddleware::SESSION_ATTRIBUTE;

public function __construct(
Configuration $jwtConfiguration,
) {
$this->jwtConfiguration = clone $jwtConfiguration;

$this->clock = SystemClock::fromSystemTimezone();
$this->cookie = SetCookie::create('__Secure-slsession')
->withSecure(true)
->withHttpOnly(true)
->withSameSite(SameSite::lax())
->withPath('/');
}

public function getJwtConfiguration(): Configuration
{
return $this->jwtConfiguration;
}

public function getClock(): Clock
{
return $this->clock;
}

public function getCookie(): SetCookie
{
return $this->cookie;
}

/** @return positive-int */
public function getIdleTimeout(): int
{
return $this->idleTimeout;
}

/** @return positive-int */
public function getRefreshTime(): int
{
return $this->refreshTime;
}

/** @return literal-string */
public function getSessionAttribute(): string
{
return $this->sessionAttribute;
}

public function withJwtConfiguration(Configuration $jwtConfiguration): self
{
$new = clone $this;
$new->jwtConfiguration = clone $jwtConfiguration;

return $new;
}

public function withClock(Clock $clock): self
{
$new = clone $this;
$new->clock = clone $clock;

return $new;
}

public function withCookie(SetCookie $cookie): self
{
$new = clone $this;
$new->cookie = clone $cookie;

return $new;
}

/** @param positive-int $idleTimeout */
public function withIdleTimeout(int $idleTimeout): self
{
$new = clone $this;
$new->idleTimeout = $idleTimeout;

return $new;
}

/** @param positive-int $refreshTime */
public function withRefreshTime(int $refreshTime): self
{
$new = clone $this;
$new->refreshTime = $refreshTime;

return $new;
}

/** @param literal-string $sessionAttribute */
public function withSessionAttribute(string $sessionAttribute): self
{
$new = clone $this;
$new->sessionAttribute = $sessionAttribute;

return $new;
}
}
Loading

0 comments on commit 8eb29d2

Please sign in to comment.