setting session expire time, secure state, etc.

82 views
Skip to first unread message

Brian

unread,
Nov 3, 2016, 1:55:06 AM11/3/16
to Perl Kelp
Using session with store=>File, it creates a cookie "plack_session."  This is working fine except when I want to create a "remember me" checkbox which expires at some point in the distant future instead of when the browser window is closed.  In addition, I want to make sure the session cookies are only passed on secure pages but by default it looks like the session cookie is created non-secure which will send it on both secure and non secure requests.

Interestingly, there are methods available on a couple of objects that look like they will allow me to control this:

http://search.cpan.org/~miyagawa/Plack-Middleware-Session-0.30/lib/Plack/Session/State/Cookie.pm

This one has "expires" and "secure" methods.  But how do I access this object to call these methods?  It looks like the only interface to the session that I have is env->session (or $self->req->session) which returns a hashref, not an instance of Plack::Middleware::Session, Plack::Session, Plack:Session::State::Cookie, or anything else that lets me call methods on it.

I have also found information on setting options like this:

http://stackoverflow.com/questions/29187503/how-to-set-the-expiration-time-for-a-cookie-managed-by-plackmiddlewaresessio

However, it doesn't seem to be working for me.  During the login, I set those options, but the cookie always gets set to expire when the browser window is closed.

Please forgive me as I'm trying to wrap my head around how Plack and PSGI and all this works.  I'm having a cognitive disconnect because historically if I want an object I just "use" the class and then instantiate the object with a new() method.  All the examples around Plack show an interface like "builder" methods which seem more like global state as opposed to objects on a per-request basis.  In addition, frameworks like Kelp and Dancer have me use configuration to enable middleware but I don't have access to where the objects are actually instantiated which makes trying to decipher the documentation more difficult.

Thanks again,
Brian

Stefan Geneshky

unread,
Nov 3, 2016, 11:07:36 PM11/3/16
to perl...@googlegroups.com
Brian,

The Plack session has two properties: store and state. Store controls the session storage medium, which in your case is File. State contains a pointer within the store, which in your case is a cookie containing the filename of the session file.

Both can be specified in the Plack builder. For example, to use a secure cookie state, add this to your app:

builder {
    enable 'Session', state => Plack::Session::State::Cookie->new( secure => 1 );
    $app;
};


To add a file store: (example from https://metacpan.org/pod/Plack::Session::Store::File )

builder {
    enable 'Session',
        store => Plack::Session::Store::File->new(
            dir          => '/path/to/sessions',
            # YAML takes it's args the opposite order
            serializer   => sub { YAML::DumpFile( reverse @_ ) },
            deserializer => sub { YAML::LoadFile( @_ ) },
        );
    $app;
};

To add both:

builder {
    enable 'Session',
state => Plack::Session::State::Cookie->new( secure => 1 ),
        store => Plack::Session::Store::File->new(
            dir          => '/path/to/sessions',
            # YAML takes it's args the opposite order
            serializer   => sub { YAML::DumpFile( reverse @_ ) },
            deserializer => sub { YAML::LoadFile( @_ ) },
        );
    $app;
};

The above code goes in your app.psgi. Kelp also allows you to control all middleware in your config files.
This is good if you use different stores in your different environments, for example you could use Redis in your
production server, while using a simple file storage in development.


I hope that helps.

Stefan

Brian E. Lozier

unread,
Nov 3, 2016, 11:45:51 PM11/3/16
to perl...@googlegroups.com
Thank you for your reply.  It helps a lot but I'm still stuck.  Declaring the session state in builder{} did ensure that my cookies are now sent on secure connections only.  The problem I am having at this point is that I need access to that state object within the context of a request.  Like, within the route handler function.  If a user checked the "remember me" box, I need to call the expires() method on the Plack::Session::State::Cookie object so that I can make the cookie expire at some point in the far future instead of when the browser is closed.  That way, when the user comes back, he/she will still be logged in.  Even when I use the builder{} function as you suggested the actual behavior of the $self->req->session() call is the same -- I still get a hashref and not an object.

I've seen others asking the exact same question and the answer (as of 2 years ago) was to set the session objects in the $env variable directly; that doesn't seem to work for me.

If there's no way to reach into that object or call methods on it from within the app, it sounds like I should stop using plack sessions via middleware and handle them myself within the app.  Does that sound like a reasonable solution?  If so, do you have any pointers to any docs that would help out?

I appreciate your patience and your time.

Brian E. Lozier

unread,
Nov 4, 2016, 12:17:09 AM11/4/16
to perl...@googlegroups.com
Using the psgix.session.options as outlined in my original post actually appears to be attempting to set the cookie expire time.  Here are the headers from the Live HTTP Headers extension on firefox:

HTTP/1.1 302 Found
Server: nginx/1.10.0 (Ubuntu)
Date: Fri, 04 Nov 2016 04:02:33 GMT
Content-Length: 0
Connection: keep-alive
Location: https://localhost:444/login_success/
X-Framework: Perl Kelp
Set-Cookie: plack_session=49808b909cd219fe323efa26ff6570feee0a6161; path=/; expires=Sun, 04-Dec-2016 04:02:33 GMT
Set-Cookie: plack_session=aa758952ff022a2aabc11539e94618108b93e080; path=/; expires=Sun, 04-Dec-2016 04:02:33 GMT; secure
Strict-Transport-Security: max-age=63072000; includeSubdomains
X-Frame-Options: DENY
x-content-type-options: nosniff

So the expires is actually being set.  But the client is ignoring it.  After some research it turns out this is actually by design :(  An RFC states:

   3.3.2  Rejecting Cookies  To prevent possible security or privacy
   violations, a user agent rejects a cookie according to rules below.
   The goal of the rules is to try to limit the set of servers for which
   a cookie is valid, based on the values of the Path, Domain, and Port
   attributes and the request-URI, request-host and request-port.

   A user agent rejects (SHALL NOT store its information) if the Version
   attribute is missing.  Moreover, a user agent rejects (SHALL NOT
   store its information) if any of the following is true of the
   attributes explicitly present in the Set-Cookie2 response header:

      *  The value for the Path attribute is not a prefix of the
         request-URI.

      *  The value for the Domain attribute contains no embedded dots,
         and the value is not .local.

      *  The effective host name that derives from the request-host does
         not domain-match the Domain attribute.

      *  The request-host is a HDN (not IP address) and has the form HD,
         where D is the value of the Domain attribute, and H is a string
         that contains one or more dots.

      *  The Port attribute has a "port-list", and the request-port was
         not in the list.

So... sorry for the noise in this one :(  I'm not exactly happy with the interface; setting hash values in a global hashref just seems so wrong; but oh well!

It's also interesting to me that I see two set-cookie headers, one non-secure and one secure.  That is weird.  I'll look into it more later.

Thanks again for your help, I appreciate it.

Stefan Geneshky

unread,
Nov 4, 2016, 12:22:40 AM11/4/16
to perl...@googlegroups.com
I would suggest that you don't tamper with the cookie, but instead control the session itself.
You could set a permanent cookie for everyone, but clear the session with:
$self->req->session( {} );
for those who your code deems expired.

See https://metacpan.org/pod/Kelp::Request#session for examples on how to handle sessions.

On Thu, Nov 3, 2016 at 8:45 PM, Brian E. Lozier <saber...@gmail.com> wrote:

Brian E. Lozier

unread,
Nov 4, 2016, 12:47:41 AM11/4/16
to perl...@googlegroups.com
Just some further information.  Setting the cookie "expires" (via the psgix.session.options) actually works, but on subsequent requests the Plack session middleware or someone resends the cookie without the extended expire time, so it overwrites the more long-lasting cookie that was previously added.  This makes a sort of sense because it's not recording on a per-session basis how far in the future the cookie should expire.  And as far as I can tell, I have no easy way to get into the code which is setting the cookies (I could examine the values in the session to determine how far in the future to set the cookie if I did have that access).  Further, using change_id=1 confuses the issue because it causes plack (or somebody) to emit multiple plack_session cookies with the same cookie name but different values.

This is way more complicated than it should be; I've done this type of thing dozens of times under CGI, php, etc.  My gut feeling is that using the magic middleware isn't a solution I should pursue further.  Rather, I should try to handle the sessions and the cookies myself because I can't get the granular control I need using the middleware.

However, I appreciate your suggestion regarding setting a permanent cookie for everyone.  I actually do need to keep track (server side) of how long the user has been logged in and force a re-login at reasonable intervals anyway.  I'll pursue this before I go nuclear.  The fact that cookies are giving me so many problems makes me want to go back to raw mod_perl handlers :)

Thanks again,
Brian

Brian E. Lozier

unread,
Nov 4, 2016, 1:37:53 AM11/4/16
to perl...@googlegroups.com
Sorry for the noise on the list.  I ended up solving it in a different way.  The issue that was killing me was the fact that the Plack::Session::State::Cookie module's "finalize" method was constantly setting the session cookie even if it'd already been sent.  That meant that on subsequent requests to any page my more long-lasting cookie was being overwritten with a new expire time (end of session).  Instead of trying to handle everything myself I just created a new module that inherited from Plack::Session::State::Cookie and used that instead.  In my module I overrode the finalize() method.  In there, I look into the actual session and try to find an _expires key (set by me in the session elsewhere; when the user logs in).  If this key is there, I use it to set the expire time of the cookie instead of the default (0 or undefined, which makes the cookie expire when the browser is closed).  This way, I only have persistent cookies for those users that have chosen "remember me" and everyone else gets the transient cookies.  I'll add a check later to ensure that if the expire time has passed the session is invalidated.  This still constantly re-sends the cookie, which I think is silly, but I'm done worrying about it for now.

app.psgi

use lib 'lib';

use Plack::Builder;
use Plack::Middleware::Session;
use Plack::Session::Store::File;

use Fan::Session::State::Cookie;

use Fan::Web;

my $app = Fan::Web->new();

builder {
    enable 'Session',
        state => Fan::Session::State::Cookie->new(
            secure      => 1,
            session_key => 'fan_session',

        ),
        store => Plack::Session::Store::File->new(
            dir => '/tmp',
        );
    $app->run;
};


Fan::Session::State::Cookie

package Fan::Session::State::Cookie;

use strict;
use warnings;

use parent 'Plack::Session::State::Cookie';

use Cookie::Baker;
use Plack::Util;

use Plack::Util::Accessor qw[
    env
];

sub get_session_id {
    my ($self, $env) = @_;

    $self->env($env);

    $self->SUPER::get_session_id($env);
}

sub finalize {
    my ($self, $id, $res, $options) = @_;

    # Look at the session, find a _expires key, and if present, use that as the
    # expire time for the cookie
    my $env = $self->env();

    if(my $expires = $env->{'psgix.session'}->{'_expires'}) {
        $options->{'expires'} = $expires;
    }

    my %opts = $self->merge_options(%$options);

    $self->_set_cookie($id, $res, %opts);
}

1;


Reply all
Reply to author
Forward
0 new messages