File Coverage

blib/lib/Catalyst/Plugin/Session/Store/Cookie.pm
Criterion Covered Total %
statement 40 42 95.2
branch 6 8 75.0
condition 14 29 48.2
subroutine 8 10 80.0
pod 4 5 80.0
total 72 94 76.6


line stmt bran cond sub pod time code
1              
2             use Moose;
3 1     1   3615 use Session::Storage::Secure;
  1         2  
  1         8  
4 1     1   7355 use MRO::Compat;
  1         74750  
  1         40  
5 1     1   7 use Catalyst::Utils;
  1         3  
  1         19  
6 1     1   6  
  1         2  
  1         660  
7             extends 'Catalyst::Plugin::Session::Store';
8             with 'Catalyst::ClassData';
9              
10             our $VERSION = '0.005';
11              
12             __PACKAGE__->mk_classdata($_)
13             for qw/_secure_store _store_cookie_name _store_cookie_expires
14             _store_cookie_secure _store_cookie_httponly _store_cookie_samesite/;
15              
16             my ($self, $key) = @_;
17             $self->_needs_early_session_finalization(1);
18 22     22 1 301976  
19 22         89 # Don't decode if we've decoded this context already.
20             return $self->{__cookie_session_store_cache__}->{$key} if
21             exists($self->{__cookie_session_store_cache__}) &&
22             exists($self->{__cookie_session_store_cache__}->{$key});
23              
24 22 100 100     6052 my $cookie = $self->req->cookie($self->_store_cookie_name);
25             $self->{__cookie_session_store_cache__} = defined($cookie) ?
26 6         23 $self->_decode_secure_store($cookie, $key) : +{};
27 6 100       744  
28             return $self->{__cookie_session_store_cache__}->{$key};
29             }
30 6         24  
31             my ($self, $cookie, $key) = @_;
32             my $decoded = eval {
33             $self->_secure_store->decode($cookie->value);
34 4     4   13 } || do {
35             $self->log->error("Issue decoding cookie for key '$key': $@");
36             return +{};
37 4   33     12 };
38             return $decoded;
39             }
40              
41 4         4174 my ($self, $key, $data) = @_;
42              
43             $self->{__cookie_session_store_cache__} = +{
44             %{$self->{__cookie_session_store_cache__}},
45 3     3 1 2640 $key => $data};
46              
47             my $cookie = {
48 3         6 value => $self->_secure_store->encode($self->{__cookie_session_store_cache__}),
  3         18  
49             expires => $self->_store_cookie_expires,
50             };
51              
52 3         24 # copied from Catalyst::Plugin::Session::State::Cookie
53             my $sec = $self->_store_cookie_secure;
54             $cookie->{secure} = 1 unless ( ($sec==0) || ($sec==2) );
55             $cookie->{secure} = 1 if ( ($sec==2) && $self->req->secure );
56             $cookie->{httponly} = $self->_store_cookie_httponly;
57 3         8836 $cookie->{samesite} = $self->_store_cookie_samesite;
58 3 50 33     63  
59 3 50 33     10 return $self->res->cookies->{$self->_store_cookie_name} = $cookie;
60 3         13 }
61 3         69  
62             my ($self, $key) = @_;
63 3         69 delete $self->{__cookie_session_store_cache__}->{$key};
64             }
65              
66             # Docs say 'this may be used in the future', like 10 years ago...
67 0     0 1 0  
68 0         0 my $class = shift;
69             my $cfg = $class->_session_plugin_config;
70             $class->_store_cookie_name($cfg->{storage_cookie_name} || Catalyst::Utils::appprefix($class) . '_store');
71             $class->_store_cookie_expires($cfg->{storage_cookie_expires} || '+1d');
72       0 1   $class->_secure_store(
73             Session::Storage::Secure->new(
74             secret_key => ($cfg->{storage_secret_key} ||
75 1     1 0 381187 die "storage_secret_key' configuration param for 'Catalyst::Plugin::Session::Store::Cookie' is missing!"),
76 1         4 sereal_encoder_options => ($cfg->{sereal_encoder_options} || +{ snappy => 1, stringify_unknown => 1 }),
77 1   33     77 sereal_decoder_options => ($cfg->{sereal_decoder_options} || +{ validate_utf8 => 1 })
78 1   50     53 )
79             );
80             $class->_store_cookie_secure($cfg->{storage_cookie_secure} || 0);
81             $class->_store_cookie_httponly($cfg->{storage_cookie_httponly} || 1);
82             $class->_store_cookie_samesite($cfg->{storage_cookie_samesite} || 'Lax');
83              
84 1   50     39 return $class->maybe::next::method(@_);
      50        
      50        
85             }
86              
87 1   50     5890 __PACKAGE__->meta->make_immutable;
88 1   50     28  
89 1   50     26 =head1 NAME
90              
91 1         23 Catalyst::Plugin::Session::Store::Cookie - Store session data in the cookie
92              
93             =head1 SYNOPSIS
94              
95             package MyApp;
96              
97             use Catalyst qw/
98             Session
99             Session::State::Cookie
100             Session::Store::Cookie
101             /;
102              
103             __PACKAGE__->config(
104             'Plugin::Session' => {
105             storage_cookie_name => ...,
106             storage_cookie_expires => ...,
107             storage_secret_key => ...,
108             storage_cookie_secure => ...,
109             storage_cookie_httponly => ...,
110             storage_cookie_samesite => ...,
111             },
112             ## More configuration
113             );
114              
115             __PACKAGE__->setup;
116              
117             =head1 DESCRIPTION
118              
119             What's old is new again...
120              
121             Store session data in the client cookie, like in 1995. Handy when you don't
122             want to setup yet another storage system just for supporting sessions and
123             authentication. Can be very fast since you avoid the overhead of requesting and
124             deserializing session information from whatever you are using to store it.
125             Since Sessions in L<Catalyst> are global you can use this to reduce per request
126             overhead. On the other hand you may just use this for early prototying and
127             then move onto something else for production. I'm sure you'll do the right
128             thing ;)
129              
130             The downsides are that you can really only count on about 4Kb of storage space
131             on the cookie. Also, that cookie data becomes part of every request so that
132             will increase overhead on the request side of the network. In other words a big
133             cookie means more data over the wire (maybe you are paying by the byte...?)
134              
135             Also there are some questions as to the security of this approach. We encrypt
136             information with L<Session::Storage::Secure> so you should review that and the
137             notes that it includes. Using this without SSL/HTTPS is not recommended. Buyer
138             beware.
139              
140             In any case if all you are putting in the session is a user id and a few basic
141             things this will probably be totally fine and likely a lot more sane that using
142             something non persistant like memcached. On the other hand if you like to dump
143             a bunch of stuff into the user session, this will likely not work out.
144              
145             B<NOTE> Since we need to store all the session info in the cookie, the session
146             state will be set at ->finalize_headers stage (rather than at ->finalize_body
147             which is the default for session storage plugins). What this means is that if
148             you use the streaming or socket interfaces ($c->response->write, $c->response->write_fh
149             and $c->req->io_fh) your session state will get saved early. For example you
150             cannot do this:
151              
152             $c->res->write("some stuff");
153             $c->session->{key} = "value";
154              
155             That key 'key' will not be recalled when the session is recovered for the following
156             request. In general this should be an easy issue to work around, but you need
157             to be aware of it.
158              
159             =head1 CONFIGURATION
160              
161             This plugin supports the following configuration settings, which are stored as
162             a hash ref under the configuration key 'Plugin::Session::Store::Cookie'. See
163             L</SYNOPSIS> for example.
164              
165             =head2 storage_cookie_name
166              
167             The name of the cookie that stores your session data on the client. Defaults
168             to '${$myapp}_sstore' (where $myappp is the lowercased version of your application
169             subclass). You may wish something less obvious.
170              
171             =head2 storage_cookie_expires
172              
173             How long before the cookie that is storing the session info expires. defaults
174             to '+1d'. Lower is more secure but bigger hassle for your user. You choose the
175             right balance.
176              
177             =head2 storage_secret_key
178              
179             Used to fill the 'secret_key' initialization parameter for L<Session::Storage::Secure>.
180             Don't let this be something you can guess or something that escapes into the
181             wild...
182              
183             There is no default for this, you need to supply.
184              
185             =head2 storage_cookie_secure
186              
187             If this attribute B<set to 0> the cookie will not have the secure flag.
188              
189             If this attribute B<set to 1> the cookie sent by the server to the client
190             will get the secure flag that tells the browser to send this cookie back to
191             the server only via HTTPS.
192              
193             If this attribute B<set to 2> then the cookie will get the secure flag only if
194             the request that caused cookie generation was sent over https (this option is
195             not good if you are mixing https and http in your application).
196              
197             Default value is 0.
198              
199             =head2 storage_cookie_httponly
200              
201             If this attribute B<set to 0>, the cookie will not have HTTPOnly flag.
202              
203             If this attribute B<set to 1>, the cookie will got HTTPOnly flag that should
204             prevent client side Javascript accessing the cookie value - this makes some
205             sort of session hijacking attacks significantly harder. Unfortunately not all
206             browsers support this flag (MSIE 6 SP1+, Firefox 3.0.0.6+, Opera 9.5+); if
207             a browser is not aware of HTTPOnly the flag will be ignored.
208              
209             Default value is 1.
210              
211             Note1: Many people are confused by the name "HTTPOnly" - it B<does not mean>
212             that this cookie works only over HTTP and not over HTTPS.
213              
214             Note2: This parameter requires Catalyst::Runtime 5.80005 otherwise is skipped.
215              
216             =head2 storage_cookie_samesite
217              
218             This attribute configures the value of the
219             L<SameSite|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite>
220             flag.
221              
222             If set to None, the cookie will be sent when making cross origin requests,
223             including following links from other origins. This requires the
224             L</cookie_secure> flag to be set.
225              
226             If set to Lax, the cookie will not be included when embedded in or fetched from
227             other origins, but will be included when following cross origin links.
228              
229             If set to Strict, the cookie will not be included for any cross origin requests,
230             including links from different origins.
231              
232             Default value is C<Lax>. This is the default modern browsers use.
233              
234             Note: This parameter requires Catalyst::Runtime 5.90125 otherwise is skipped.
235              
236             =head2 sereal_decoder_options
237              
238             =head2 sereal_encoder_options
239              
240             This should be a hashref of options passed to init args of same name in
241             L<Session::Storage::Secure>. Defaults to:
242              
243             sereal_encoder_options => +{ snappy => 1, stringify_unknown => 1 },
244             sereal_decoder_options => +{ validate_utf8 => 1 },
245              
246             Please note the default B<allows> object serealization. You may wish to
247             not allow this for production setups.
248              
249             =head1 AUTHOR
250              
251             John Napiorkowski L<email:jjnapiork@cpan.org>
252             Alexander Hartmaier L<email:abraxxa@cpan.org>
253              
254             =head1 SEE ALSO
255              
256             L<Catalyst>, L<Catalyst::Plugin::Session>, L<Session::Storage::Secure>
257              
258             =head1 COPYRIGHT & LICENSE
259              
260             Copyright 2022, John Napiorkowski L<email:jjnapiork@cpan.org>
261              
262             This library is free software; you can redistribute it and/or modify it under
263             the same terms as Perl itself.
264              
265             =cut
266