Skip to content

jamielsharief/psr-session

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 

Repository files navigation

PSR-7/PSR-15 Sessions

Problems

  1. Currently PHP sessions are not compatible with PSR-7
  2. The session object is widely used but there is no standard. A session spec would actually help solve a number of problems, including:
    • standardizing between frameworks
    • using other peoples implementations easily
    • swapping out types of implementations so if you want to use Swoole, you can move away from the PHP sessions and switch to a Redis handler
    • Lots of companies like to move their controller logic into reusable services which would be much easier with a session spec.
  3. Another important part of this PSR, I believe, is for a standard way to set and get the Session object on the ServerRequestInterface object, without this feature, I think it would be incomplete.

Security

  • Security is an issue and a potential complexity. I believe by also standardizing the hooks into the session lifecycle, such as startup and shutdown this will bring enough flexibility into the session spec, so developers are able to deal with security related issues such as validating the session ID perhaps or deleting previous data etc.
  • Take into consideration OWASP recommendations, including:
    • the default session ID name to be id to prevent Session ID Name Fingerprinting
    • the session ID should be at least 128 bits (16 bytes)
    • and other recommendations such as cookie settings etc.

Objectives

  • User can set, get, check, clear or destroy sessions completely
  • Middleware needs to start, close sessions, set the id, and get the id once the storage creates the ID or regenerates the ID so that it can write to the cookie
  • Middleware also needs to know if the session was destroyed so it can delete the cookie
  • Hooks into the session lifecycle for security purposes, needs to be handled
  • It needs to work with various types of storage, e.g PHP Sessions, Redis, JWT tokens and single long-running processes e.g. Swoole.

Initial Concept

interface SessionInterface
{
    /**
     * Sets a value in the session
     *
     * @param string $key
     * @param mixed $value
     * @return void
     */
    public function set(string $key, $value) : void;

    /**
     * Gets a value from the session
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public function get(string $key, $default = null);

    /**
     * Removes a value from the session
     *
     * @param string $key
     * @return void
     */
    public function unset(string $key): void;

    /**
     * Checks if the session has the key
     *
     * @param string $key
     * @return boolean
     */
    public function has(string $key): bool;

    /**
     * Clears the contents of the session
     *
     * @return void
     */
    public function clear(): void;

    /**
     * Destroy the current session
     *
     * @return void
     */
    public function destroy(): void;

    /**
     * Starts a session and loads data from storage
     *
     * @param string|null $sessionId
     * @return boolean
     */
    public function start(?string $sessionId): bool;

    /**
     * Closes a session and writes to storage
     *
     * @return boolean
     */
    public function close(): bool;

    /**
     * Get the session Id, null means session was destroyed (or not started).
     *
     * @param string|null $sessionId
     * @return string|null
     */
    public function getId(): ?string;

    /**
     * Informs the session object to regenerate the session ID for the existing session data
     *
     * @return boolean
     */
    public function regenerateId(): bool;
}

With regards to standardizing the setting and getting the session object on ServerRequestInterface object

interface ServerRequestSessionInterface 
{
    public function setSession(SessionInterface $session): void;
    public function getSession(): SessionInterface;
}
  • not sure about the naming though if this interface is used
  • hopefully this allows to extend and not break anything
  • A potential issue here is if the session was not added, do you throw an exception, return null etc?

Example Middleware

class SessionMiddleware implements MiddlewareInterface
{
    private SessionInterface $session;
    private string $cookieName = 'id';
    private int $timeout = 900; // 15 minutes
    private string $sameSite = 'lax';
    private string $cookiePath = '/';

    public function __construct(SessionInterface $session)
    {
        $this->session = $session;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $sessionId = $this->getSessionId($request); // Get value from cookie, session id or perhaps JWT token?

        $this->session->start($sessionId);

        $response = $handler->handle($request->withAttribute('session', $this->session));

        $this->session->close(); // close session if still open, user may have destroyed or closed manually

        return $this->addCookieToResponse($request, $response);
    }

    /**
     * If the session was destroyed, there will be no id, so delete delete delete. If the session ID was regenerated, then
     * the cookie needs to be updated.
     */
    private function addCookieToResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $wasDestroyed = is_null($this->session->getId());
        $cookieValue = $wasDestroyed ? '' : $this->session->getId();
        $cookieExpires = $wasDestroyed ? time() - 3600 : time() + $this->timeout;

        return $response->withAddedHeader(
             'Set-Cookie', $this->createCookieString($cookieValue, $cookieExpires, $request)
         );
    }

    private function getSessionId(ServerRequestInterface $request): ?string
    {
        $cookies = $request->getCookieParams();

        return $cookies[$this->cookieName] ?? null;
    }

    private function createCookieString(string $sessionId, int $expires, ServerRequestInterface $request): string
    {
        return sprintf(
            '%s=%s; expires=%s; path=%s; samesite=%s;%s httponly',
            $this->cookieName,
            $sessionId,
            gmdate(\DateTime::COOKIE, $expires),
            $this->cookiePath,
            $this->sameSite,
            $request->getUri()->getScheme() === 'https' ? ' secure;' : null
        ) ;
    }
}

Here is a simple example to demonstrate the concept

class PhpSession implements SessionInterface
{
    private ?string $id = null;
    private array $session = [];
    private bool $isRegenerated = false;
    private bool $isStarted = false;

    public function start(?string $id): bool
    {
        if ($this->isStarted) {
            return false;
        }

        $this->id = $id ?: $this->generateId();

        session_id($this->id);

        // Disable the PHP cookie features, credit to @pmjones for this
        $this->isStarted = session_start([
            'use_cookies' => false,
            'use_only_cookies' => false,
            'use_trans_sid' => false
        ]);

        $this->session = $_SESSION ?? [];

        return $this->isStarted;
    }

    public function set(string $key, $value): void
    {
        $this->session[$key] = $value;
    }

    public function get(string $key, $default = null)
    {
        return $this->session[$key] ?? $default;
    }

    public function unset(string $key): void
    {
        unset($this->session[$key]);
    }

    public function has(string $key): bool
    {
        return array_key_exists($key, $this->session);
    }

    public function clear(): void
    {
        $this->session = [];
    }

    public function destroy(): void
    {
        $this->session = [];
        $this->close();
        $this->id = null;
    }

    public function getId(): ?string
    {
        return $this->id;
    }

    public function regenerateId(): bool
    {
        $this->id = $this->generateId();
        $this->isRegenerated = true;

        return true;
    }

    private function generateId(): string
    {
        return bin2hex(random_bytes(16)); // OWASP recommendation
    }

    public function close(): bool
    {
        if ($this->isStarted === false) {
            return false;
        }

        // I think there were some issues with overwriting the $_SESSION variable directly
        $removed = array_diff(array_keys($_SESSION), array_keys($this->session));
        foreach ($this->session as $key => $value) {
            $_SESSION[$key] = $value;
        }

        foreach ($removed as  $key) {
            unset($_SESSION[$key]);
        }
        $closed = session_write_close();

        $this->isStarted = $closed === false;

        return $this->isRegenerated ? $this->regenerateSessionData() : $closed;
    }

    /**
     * Copy session data to new ID
     *
     * @return boolean
     */
    private function regenerateSessionData(): bool
    {
        $this->isRegenerated = false;
        $session = $_SESSION; // data still seems to be here
        $this->start($this->id); // start session with new session ID
        $this->session = $session; // kansas city shuffle

        return $this->close(); // save
    }
}

Resources

About

PSR-7/PSR-15 sessions

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages