mezzio / mezzio-session-ext

ext-session persistence adapter for mezzio-session
https://docs.mezzio.dev/mezzio-session-ext/
BSD 3-Clause "New" or "Revised" License
7 stars 14 forks source link

Sessions without data work in unexpected ways. #5

Open weierophinney opened 4 years ago

weierophinney commented 4 years ago

Treating connections to server themselves on their own merit. Session is not just a variable bag/container to hold some values. It is a way of reasoning the idea of a link between client and server. There does not need to be explicit data persisted between requests to develop workflows focusing around tracking a particular visitor.

That said, it is very easy to work around by setting dummy variable at the beginning of the pipeline. However, if you already know how PHP session works this is unexpected behavior. Therefore, if above notion is rejected, there should be a warning in documentation stating that this library works differently from how people are used to work with PHP. Can spend days debugging, only to find out the framework changed traditional behavior.

Specific usecase example. I have a "go to meeting" style workflow. A financial advisor logs in, initiates a meeting. Needs his client to see in real time how data in form is filled in. Sends one time token link to client who does not have account on intranet system to participate in the meeting. Hash in link is checked to be used one time on a single connection attempt using session_id() . But if follow up background requests during the meeting create new sessions, I cannot rely on session_id() + token combo as one of checks to verify the connection is still coming from the same person.

Expected results

Actual results


Originally posted by @alextech at https://github.com/zendframework/zend-expressive-session-ext/issues/23

weierophinney commented 4 years ago

Hmmm... I see what you mean by tracking with the session_id. By default the PHP session is not started. You need to either start it manually or change an ini setting to autostart it. We could add a check for the session.auto_start ini setting and maybe even add a auto_start config option to mimic this behavior. If auto_start is enabled, we could start a session and cookie even when there is no session data set.


Originally posted by @geerteltink at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-401556401

weierophinney commented 4 years ago

We could add a check for the session.auto_start ini setting

I did not know about that option! Great you mentioned it. If library acts as a compliment to that setting, I think it would help a lot.


Originally posted by @alextech at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-401556738

weierophinney commented 4 years ago

@xtreamwayz @alextech ..or maybe add an extra non-lazy session implementation with a config option for the session middleware to build/load one of them


Originally posted by @pine3ree at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-401785835

weierophinney commented 4 years ago

I ran into the same issue with https://github.com/zendframework/zend-expressive-session-ext/issues/19. A session can exist without data, which agrees with how session_start() behaves.


Originally posted by @rtek at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-411619130

weierophinney commented 4 years ago

Hello @rtek, @xtreamwayz, @alextech, @weierophinney in my opinion we need to add this functionality in the https://github.com/zendframework/zend-expressive-session package, implementing 2 distinct session middlewares (+ their own factories): 1 for lazy-session and 1 for non lazy-session. LazySession starts a session ( for instance a php-session using this package as persistence layer) only when something is read or something is write. A possible non lazy session implementation could be always get started by its own related middleware using a session-id set via cookie or creating and persisting a new one to the browser. Maybe a quick'n'dirty (less-elegant) workaround could be having the lazy-session middleware always making the lazy-session read/write a non-existing session-key to trigger the instantiation of a session object. kind regards, maks.


Originally posted by @pine3ree at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-411864122

weierophinney commented 4 years ago

@pine3ree sounds pretty good. Though I am not sold on adding so much complexity with yet more factories, more decision making at pipline configuration, more paragraphs in documentation to read about lazy vs non-lazy session choice.


Originally posted by @alextech at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-411866722

weierophinney commented 4 years ago

Laziness is not something that works in the middleware approach: the lazy call needs to be "paused" and resumed by the lazy layer, while PSR-15 has no such concept, so two separate middleware would not work.

On Thu, 9 Aug 2018, 22:16 Sasha Alex Romanenko, notifications@github.com wrote:

@pine3ree https://github.com/pine3ree sounds pretty good. Though I am not sold on adding so much complexity with yet more factories, more decision making at pipline configuration, more paragraphs in documentation to read about lazy vs non-lazy session choice.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-411866722, or mute the thread https://github.com/notifications/unsubscribe-auth/AAJakDlSfp7FpmQQbsOMXM7yM0i6ncv4ks5uPIqSgaJpZM4U9_RD .


Originally posted by @Ocramius at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-412001902

weierophinney commented 4 years ago

You can see a reference implementation at https://github.com/psr7-sessions/storageless/blob/5.0.0/src/Storageless/Http/SessionMiddleware.php#L146L148


Originally posted by @Ocramius at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-412002335

weierophinney commented 4 years ago

The LazySession implementation calls initializeSessionFromRequest only when the proxied actual session instance is needed (i.e. when reading/writing data). initializeSessionFromRequest is where the request session-cookie is read, a new ext-session is started and the proxied Session instance is created. If we want to always start an ext-session we should be able to use a non-lazy Session wrapper that initialize the proxied session in its constructor, or having a dedicated middleware that bypass any session wrapper and creates-and-starts a Session instance directly by callling initializeSessionFromRequest. The "lazy" stuff happens inside the LazySession class (using a lazy proxy), maybe we just need a middeware that doesn't create a LazySession but just a Session.

middleware pseudo code


//..
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $session  = $this->persistence->initializeSessionFromRequest($request); // A Session class instance
        $response = $handler->handle($request->withAttribute(self::SESSION_ATTRIBUTE, $session));

        return $this->persistence->persistSession($session, $response);
    }

kind regards.

---

Originally posted by @pine3ree at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-412072658
weierophinney commented 4 years ago

I admit that when I first read this issue I didn't understand it completely, having my mind focused on other aspect of this package.

Now it makes more sense to me: the issue is due to the double purpose and usage of a php-ext-session:

(1) Identify a client using an identifier (the session id) stored in a cookie (this is it, just a cookie with a "known" name no file persistence here) (2) Store data for that client using the retrieved (or a newly generated) identifier (the requested or new session id)

In this package we basically assume that we use a session for (2). So when there is no data (accessed or written) we cannot identify the client in subsequent requests. But there are a cases when we just don't need data, just the session ID. For instance an active visitors counter would require storing a client identifier (read "session-id") in a db table along with a timestamp.

php-session always creates a session-id when session_start is called or just reuses the one from the request cookie. The problem with php-session is its inefficiency: it opens a file even if it doesn't use it: functionalities (1) and (2) are coupled together. This package only starts a php-session when data is involved, solving the inefficiency problem and creating a new one (unidentifiable client without session data)

Looking carefully we can see that:

  1. We just need a cookie (read and sent) to identify the client. We don't need data access (we used to call session_id() for that, $_SESSION was not involved) (...btw we could just do that using another cookie)
  2. The session container is just a data container and it doesn't need to know about the session ID ($_SESSION doesn't know about session_id() value)
  3. The persistence layer must know about the session ID and be able to load/store the container data from/to the session file (only) when needed. The persistence layer use the ID both for identifying the data storage (filename in our case) and to tell the client what its identifier is (at least once per ID)
  4. We need an object that allows us access to the session ID and - LAZILY - to the container data

The current LazySession implementations still couples the ID with the data access proxying the functionality to the internal container and the persitence layer

I believe we should could solve the problem in the following way:

A) we always create a LazySession instance with an internal (not proxied) ID property matching the request session cookie value or generating a new one. B) we remove the ID awareness from the proxied container and we also remove the proxied getID() call from the lazy object (right now just calling getID calls initializeSessionFromRequest) C) Accessors/mutators in the LazySession object are proxied to the internal session container, that is lazily created opening a session file to retrieve the initial data (other optimizations can be added, for instance opening a session file only if (a)there was a session cookie in the request and/or (b)the container data has changed)

In (not-so) short pseudocode:

SESSION MIDDLEWARE START
    lazySession = persistence::initializeFromRequest(request); // we just set the lazy session ID here

    Add lazySession to the request attributes

    INNER MIDDLEWARES START
        lazySession is retrieved as usual from THE request attribute
        lazySession::getID() is available here, without opening a session file (this identify the client)
        lazySession::get('something') => trigger getProxiedSession() => triggers persistence->readFromSessionFile if not done already
        lazySession::set('somethingelse', 'somevalue') => trigger getProxiedSession()->set('somethingelse', 'somevalue') => triggers persistence->readFromSessionFile if not done already 
        lazySession::unset('something') => trigger getProxiedSession() => triggers persistence->readFromSessionFile if not done already
    INNER MIDDLEWARES END

    lazySession->container->data has changed ? => store it into session file  => triggers persistence->saveIntoSessionFile
    add lazySession ID to the response as a session set-cookie header

SESSION MIDDLEWARE END

or even shorter

A) initialize the lazy-session id from the request B) read data from the session file only on data access (other than id) (optimization: and only if request session id cookie was present) C) persist the (last used/generated) session id to the response and the container data to session file if data has changed (or id regenerated)

right now A) and B) are one operation

kind regards ...poking @alextech @xtreamwayz @weierophinney @Ocramius gosh...sorry I still cannot write shorter comments in english...


Originally posted by @pine3ree at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-468445771

weierophinney commented 4 years ago

(1) Identify a client using an identifier (the session id) stored in a cookie (this is it, just a cookie with a "known" name no file persistence here)

I guess I don't understand what this particular use case is, exactly — what purpose does a session identifier serve without associated data? The identifier is literally only useful in this case for locating existing persisted session data. An empty session means there is no data for this particular identifier, and the identifier thus has no meaning for subsequent requests.

Can you maybe elaborate on what problem a session identifier without associated data solves? What problems could you solve in such a situation? Could that be solved by setting a regular cookie instead?


Originally posted by @weierophinney at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-469857556

weierophinney commented 4 years ago

Can you maybe elaborate on what problem a session identifier without associated data solves? What problems could you solve in such a situation? Could that be solved by setting a regular cookie instead?

Yes a regular cookie would be just fine.


Poking @alextech to check if this is what he referred to:

The use case that I remembered (visitor counters) can be implemented with another cookie (remember to GDPR-document it!!!!), but i remember I always used the session-id.

What I believe @alextech referred to is that the word "session" could mean just a data-less interaction between the browser and the server, where the server software is able to recognize a previous client by its assigned id (the session id).

So I believe the discussion here is only about what the "session" means for this package.

php-session allows to do that (identify without involving session data access) because it always send a cookie when session_start is called (if not already sent in a previous request). The problem with php-session is that it opens a file even if you don't read/store data from/into it.

PS I managed to achieve this double nature locally in this (pseudo code) way:

1) $lazySession = $persistence->initializeSessionFromRequest($request); 2) inner middleware 3) $persistence->persistSession($lazySession, $response);

comments:

1) $lazySession (which composes a lazy-data-container) it is always created with an existing or new session id property that will be sent (if new) with the response (no dependency on persistence layer and/or request....the lazy part here is the data container, created - if and when needed - by a factory passed in by initializeSessionFromRequest to the lazy-session constructor)

3) persistence may only set a response session-id cookie without opening or closing any session file

just to give an idea, I left out the details...

kind regards


Originally posted by @pine3ree at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-469872514

weierophinney commented 4 years ago

The identifier is literally only useful in this case for locating existing persisted session data

Disagree. That is one of two use-cases for a session. Session is also a conceptual connection of a distributed operation between multiple computers working together (even in real time).

This also breaks ajax/fetch calls. The problem of session not being persistent unless it has data, is once page is loaded through browser's GET, follow up background requests (ajax/websockets) are seen as new unique connections. This is of course false as they come from same page view. It can be a problem if there is some kind of "firewall" that checks if new connections are coming from trusted origins or same place as the main "entrance" pageload.

Could that be solved by setting a regular cookie instead?

Yes, which is what _sessionstart() does by setting session ID for me to keep track of said connection! :) But that ability is taken away so I have to put in dummy values into the session storage to fool zend-expressive into persisting it.

What I believe @alextech referred to is that the word "session" could mean just a data-less interaction between the browser and the server, where the server software is able to recognize a previous client by its assigned id

That is correct. @pine3ree 's unique visitor counter is the simplest to think of usecase. It can be extrapolated to more advanced, like the intranet tool I described in my original post. Once a permission link is generated, the data transferred between clients is then taken over by web sockets, or ajax, or simply if accidentally refreshing page and still having access to that work session (where "work session" is literally a real world human real time live meeting session, for lack of better word). Once the meeting is done, the connection to network nodes should not be trusted for future connection until new permission URL is created. Of course extra connections into that collaborative meeting are no good, hence why there is a "firewall" preventing new unique connections past the ones originally established. But there may not be data to store yet until end of the meeting so every background request is currently seen as new unique connection.


Originally posted by @alextech at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-469880650

weierophinney commented 4 years ago

Hello @alextech , I uploaded some PoC code here (https://github.com/pine3ree/app-session) for state-less session supports. I havent't had much time to test it, check it out if you like. The README file highlights a few of the changes from the original zend-expressive package. kind regards


Originally posted by @pine3ree at https://github.com/zendframework/zend-expressive-session-ext/issues/23#issuecomment-471360942