RFC: PHP Session and PSR-7

psr-7
session

#1

PHP Sessions are used in many web application to store user’s data on the server. For instance, the majority of the PHP applications with a login page (with username and password) use Session.

The usage of PHP functions session_* is not compatible with PSR-7, e.g. calling session_start() will automatically add the Set-Cookie header in the PHP output buffer. Moreover, the usage of global variables, like $_SESSION is not recommended in a middleware architecture. All the data to generate a HTTP Response should be included in the HTTP Request.

The question is, if we need PHP Session in a PSR-7 application, how we can use it?

One possible solution is to use a library like psr7-sessions/storageless. This project works very well using a JWT approach, but it is limited to 400 bytes of data per session and it does not use the PHP Session (ext/session). This can be a limit in many use cases, especially when migrating existing PHP applications to PSR-7 middleware architecture.

I prototyped another possible solution using the PHP Session. The idea is to use a PhpSession class that imports and exports data to PHP Session using $_SESSION.

UPDATE : @matthew has proposed a complete example of a PSR-7 Session Middleware here.

class PhpSession
{
    protected $session;
    protected $id;

    public function __construct(string $id)
    {
        $this->id = $id;
        session_id($id);
        session_start([
            'use_cookies' => false,
            'use_only_cookies' => true
        ]);
        $this->session = $_SESSION;
    }

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

    public function get(string $name)
    {
        return $this->session[$name] ?? null;
    }

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

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

    public function save(): void
    {
        $_SESSION = $this->session;
        session_write_close();
    }
}

This class can be injected in a PSR-7 request using an attribute. If a middleware needs Session it can retrieve as PSR-7 attribute from $request and use it.
In order to disable the automatically output of session_start(), I used the following configuration for Session:

session_start([
    'use_cookies' => false,
    'use_only_cookies' => true
]);

As suggested by Paul M.Jones in PSR-7 and Session Cookies, this configuration tells PHP not to send a cookie when it does session work. Because of that, we need to send the Set-Cookie header manually in the HTTP Response, as requested by the PSR-7 workflow. Moreover, we need to check if a Session Cookie is present in the HTTP Request, getting the Session ID.
We can achieve this using a middleware like the follows:

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;

class SessionMiddleware implements ServerMiddlewareInterface
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $cookies = $request->getCookieParams();
        $id = $cookies[session_name()] ?? bin2hex(random_bytes(16));

        $session = new PhpSession($id);
        $response = $delegate->process($request->withAttribute(PhpSession::class, $session));
        $session->save();

        return isset($cookies[session_name()]) ? $response : $response->withHeader(
            'Set-Cookie',
            sprintf("%s=%s; path=%s", session_name(), $id, ini_get('session.cookie_path'))
        );
    }
}

This SessionMiddleware can be registered in the pipeline before the route dispatch, e.g. using Expressive you can add it in the config/pipeline.php file, as follows:

$app->pipe(SessionMiddleware::class);
// ...
$app->pipeRoutingMiddleware();

You can also add SessionMiddleware only for a specific route. An example is reported below:

// config/routes.php
$app->get('/', [
    SessionMiddleware::class, 
    App\Action\HomePageAction::class
], 'home');

Note: you need to register SessionMiddleware in the service container, e.g. using zend-servicemanger you can register as invokables.

Using the proposed solution, we can use PHP Session as PSR-7 middleware, without the usage of the global variable $_SESSION reusing all the features of ext/session.


#2

We were going to write an ext-session compatible middleware, but it is just
not worth the effort.
95% of apps just need authentication and CSRF, and the rest is extremely
risky to put in sessions anyway, so IMO a general-purpose lib is
sufficiently covered by psr7-sessions/storageless.

What would be interesting is to standardize the request attribute where the
session is located: that would make the lib easily replaceable, if the 5%
scenario is hit :slight_smile:

Marco Pivetta

http://twitter.com/Ocramius

http://ocramius.github.com/


#3

… the rest is extremely risky to put in sessions anyway

Care to elaborate on that statement as to what you are stating is risky in this case? Stateful data inside of the session is not necessarily a bad thing. There are certainly areas where it can be risky (aka storing objects, result sets, etc). The exposure or potential attack vector can increase, however, a session must be secured and the level that someone takes that depends on the application.

If we think about the session component and what may or may not be stored there could be to the degree of:

  • Authorization
  • CSRF
  • Flash messages
  • Breadcrumbs / Navigation paths
  • Workflow state

The largest key of something like going with JWT / storageless sessions is that you do want to limit the amount of information exposure in the event of a possible breach inside of the encrypted data. For instance, personally identifiable information generally is not stored but looked up server side whereas in a typical server stored session you may consider additional data points.

Just some thoughts from my point of view.


#4

Hey Mike,

The ext-session component is a shared storage that may be abused, which
makes it unfit for:

  • sensitive information
  • data to be (un-)serialized (RCE)

That said, all scenarios mentioned above fit in the 400 bytes… Or more:
we put a limit on psr7-sessions/storageless, but it is only in the
documentation, and every browser on the market supports much larger strings.

Even then, (de-)serializing large data from storage is not suggested even
with “traditional” sessions.

For the state machines, I’d say that moving to server-managed “real
aggregates” is required anyway to preserve everybody’s sanity.

The scenario that JWT-based tokens do not cover is logging out remote
sessions for single users: that is a known limitation, but TBH, not even my
bank does that, and they passed some idiotic PCI compliance audit to do
their business.


#5

@enrico

Personally I really like the idea and believe it’s a very important topic for psr7 / expressive adoption. Good you came up with it.

Some questions came to my mind

  • How could we set cookie lifetime, domain or cache-limiter and cache-expire in the RFC, pmjones seems to have made it here ?
  • How to destroy session ?
  • Does the RFC intend to provide a kind of SessionInterface or just a PHPSession class ?

@ocramius

As a side note, after reading @enrico zend-authentication blog post blog post few month ago. I was wondering if it could be possible to implement a Zend\Session\Storage\StorageInterface JWT storage. I started with ‘psr7-sessions/storageless’ but couldn’t make it work with expressive 2, so I decided to wait a bit. But It’s still in my mind. (see also here for an implementation I found on github, didn’t try it).


#6

@enrico sorry didn’t read well… PHPSession is just the storage… The SessionMiddeware will have to take care of headers (lifetime, domain, path, cache_limiter, cache_expire… eventually remove some ini defaults) ?


#7

I’ve seen this blog post passed around a lot as a polemic against JWT. However, the argumentation in that post feels rather weak and the premise seems more clickbait then anything. Basically just says that JWT aren’t better than cookies, but never really says how they are objectively worse - especially if you are being stateful.

FWIW, I use a solution very similar to @ocramius’s lib, but actually backed with an in-memory session store - most importantly so I can logout individual sessions for users. Every session has it’s own unique signature key, and I like being able to use the session data as JSON on my frontend easily from sessionStorage. It also makes it pretty easy to do rolling session inactivity logouts on both server (session collection has a TTL for each record) and client side (check exp once a minute).


#8

Hi @belgattitude, the PHP Session configuration is done using the usual session_* functions of PHP.
The goal of this RFC is to continue to use PHP Session in a PSR-7 scenario, without many changes.


#9

@moderndeveloperllc, @ocramius The goal of this RFC is about PHP Session and PSR-7. This RFC does not want to compare PHP Session vs. JWT. We know the differences of these approaches, and we agree that JWT can be used to replace Session ID in a more secure way.

That said, if a PSR7 middleware application needs to use PHP Session we should provide some code/suggestions to accomplish this. The PHP Session is used in many web applications in the market and we cannot just say: please use JWT instead.


#10

@enrico well this is blessed by simplicity;) Just one comment: can we have a setId() so it can be regenerated on login / per-request / whatever?

And is there a repro yet?


#11

Hey Enrico,

Sorry, got a bit too defensive, because I have this argument very often and
also with stubborn infosec people that simply don’t get it.

That said, all I’d want is some sort of interoperable “session” object that
we can slam into the request attributes.

What we need:

  • A simple ArrayAccess-alike session abstraction
  • Session attribute name pseudo-standardisation

#12

@ocramius thanks for your clarification. The idea of using PhpSession class is to offer an object oriented repository for PHP Session to be shared between different middlewares using attribute. I agree that we should propose a simple session abstraction. I’ll try to propose something more ArrayAccess oriented.


#13

@kynx I’ll add the setId() function, it’s sound a good idea. At the moment, I didn’t create a repository for this RFC but I’ll do shortly. Thanks!


#14

@matthew has proposed a complete example of a PSR-7 Session Middleware here.
It incorporates some of the @ocramius suggestions with some additional ideas using session segments.