add SameSite support
[catagits/Catalyst-Plugin-Session-State-Cookie.git] / lib / Catalyst / Plugin / Session / State / Cookie.pm
1 package Catalyst::Plugin::Session::State::Cookie;
2 use Moose;
3 use namespace::autoclean;
4
5 extends 'Catalyst::Plugin::Session::State';
6
7 use MRO::Compat;
8 use Catalyst::Utils ();
9
10 our $VERSION = "0.17";
11
12 has _deleted_session_id => ( is => 'rw' );
13
14 sub setup_session {
15     my $c = shift;
16
17     $c->maybe::next::method(@_);
18
19     $c->_session_plugin_config->{cookie_name}
20         ||= Catalyst::Utils::appprefix($c) . '_session';
21 }
22
23 sub extend_session_id {
24     my ( $c, $sid, $expires ) = @_;
25
26     if ( my $cookie = $c->get_session_cookie ) {
27         $c->update_session_cookie( $c->make_session_cookie( $sid ) );
28     }
29
30     $c->maybe::next::method( $sid, $expires );
31 }
32
33 sub set_session_id {
34     my ( $c, $sid ) = @_;
35
36     $c->update_session_cookie( $c->make_session_cookie( $sid ) );
37
38     return $c->maybe::next::method($sid);
39 }
40
41 sub update_session_cookie {
42     my ( $c, $updated ) = @_;
43
44     unless ( $c->cookie_is_rejecting( $updated ) ) {
45         my $cookie_name = $c->_session_plugin_config->{cookie_name};
46         $c->response->cookies->{$cookie_name} = $updated;
47     }
48 }
49
50 sub cookie_is_rejecting {
51     my ( $c, $cookie ) = @_;
52
53     if ( $cookie->{path} ) {
54         return 1 if index '/'.$c->request->path, $cookie->{path};
55     }
56
57     return 0;
58 }
59
60 sub make_session_cookie {
61     my ( $c, $sid, %attrs ) = @_;
62
63     my $cfg    = $c->_session_plugin_config;
64     my $cookie = {
65         value => $sid,
66         ( $cfg->{cookie_domain} ? ( domain => $cfg->{cookie_domain} ) : () ),
67         ( $cfg->{cookie_path} ? ( path => $cfg->{cookie_path} ) : () ),
68         %attrs,
69     };
70
71     unless ( exists $cookie->{expires} ) {
72         $cookie->{expires} = $c->calculate_session_cookie_expires();
73     }
74
75     #beware: we have to accept also the old syntax "cookie_secure = true"
76     my $sec = $cfg->{cookie_secure} || 0; # default = 0 (not set)
77     $cookie->{secure} = 1 unless ( ($sec==0) || ($sec==2) );
78     $cookie->{secure} = 1 if ( ($sec==2) && $c->req->secure );
79
80     $cookie->{httponly} = $cfg->{cookie_httponly};
81     $cookie->{httponly} = 1
82         unless defined $cookie->{httponly}; # default = 1 (set httponly)
83
84     $cookie->{samesite} = $cfg->{cookie_samesite};
85     $cookie->{samesite} = "Lax"
86         unless defined $cookie->{ samesite}; # default = Lax
87
88     return $cookie;
89 }
90
91 sub calc_expiry { # compat
92     my $c = shift;
93     $c->maybe::next::method( @_ ) || $c->calculate_session_cookie_expires( @_ );
94 }
95
96 sub calculate_session_cookie_expires {
97     my $c   = shift;
98     my $cfg = $c->_session_plugin_config;
99
100     my $value = $c->maybe::next::method(@_);
101     return $value if $value;
102
103     if ( exists $cfg->{cookie_expires} ) {
104         if ( $cfg->{cookie_expires} > 0 ) {
105             return time() + $cfg->{cookie_expires};
106         }
107         else {
108             return undef;
109         }
110     }
111     else {
112         return $c->session_expires;
113     }
114 }
115
116 sub get_session_cookie {
117     my $c = shift;
118
119     my $cookie_name = $c->_session_plugin_config->{cookie_name};
120
121     return $c->request->cookies->{$cookie_name};
122 }
123
124 sub get_session_id {
125     my $c = shift;
126
127     if ( !$c->_deleted_session_id and my $cookie = $c->get_session_cookie ) {
128         my $sid = $cookie->value;
129         $c->log->debug(qq/Found sessionid "$sid" in cookie/) if $c->debug;
130         return $sid if $sid;
131     }
132
133     $c->maybe::next::method(@_);
134 }
135
136 sub delete_session_id {
137     my ( $c, $sid ) = @_;
138
139     $c->_deleted_session_id(1); # to prevent get_session_id from returning it
140
141     $c->update_session_cookie( $c->make_session_cookie( $sid, expires => 0 ) );
142
143     $c->maybe::next::method($sid);
144 }
145
146 1;
147 __END__
148
149 =head1 NAME
150
151 Catalyst::Plugin::Session::State::Cookie - Maintain session IDs using cookies.
152
153 =head1 SYNOPSIS
154
155     use Catalyst qw/Session Session::State::Cookie Session::Store::Foo/;
156
157 =head1 DESCRIPTION
158
159 In order for L<Catalyst::Plugin::Session> to work the session ID needs to be
160 stored on the client, and the session data needs to be stored on the server.
161
162 This plugin stores the session ID on the client using the cookie mechanism.
163
164 =head1 METHODS
165
166 =over 4
167
168 =item make_session_cookie
169
170 Returns a hash reference with the default values for new cookies.
171
172 =item update_session_cookie $hash_ref
173
174 Sets the cookie based on C<cookie_name> in the response object.
175
176 =item calc_expiry
177
178 =item calculate_session_cookie_expires
179
180 =item cookie_is_rejecting
181
182 =item delete_session_id
183
184 =item extend_session_id
185
186 =item get_session_cookie
187
188 =item get_session_id
189
190 =item set_session_id
191
192 =back
193
194 =head1 EXTENDED METHODS
195
196 =over 4
197
198 =item prepare_cookies
199
200 Will restore if an appropriate cookie is found.
201
202 =item finalize_cookies
203
204 Will set a cookie called C<session> if it doesn't exist or if its value is not
205 the current session id.
206
207 =item setup_session
208
209 Will set the C<cookie_name> parameter to its default value if it isn't set.
210
211 =back
212
213 =head1 CONFIGURATION
214
215 =over 4
216
217 =item cookie_name
218
219 The name of the cookie to store (defaults to C<Catalyst::Utils::apprefix($c) . '_session'>).
220
221 =item cookie_domain
222
223 The name of the domain to store in the cookie (defaults to current host)
224
225 =item cookie_expires
226
227 Number of seconds from now you want to elapse before cookie will expire.
228 Set to 0 to create a session cookie, ie one which will die when the
229 user's browser is shut down.
230
231 =item cookie_secure
232
233 If this attribute B<set to 0> the cookie will not have the secure flag.
234
235 If this attribute B<set to 1> (or true for backward compatibility) - the cookie
236 sent by the server to the client will get the secure flag that tells the browser
237 to send this cookie back to the server only via HTTPS.
238
239 If this attribute B<set to 2> then the cookie will get the secure flag only if
240 the request that caused cookie generation was sent over https (this option is
241 not good if you are mixing https and http in your application).
242
243 Default value is 0.
244
245 =item cookie_httponly
246
247 If this attribute B<set to 0>, the cookie will not have HTTPOnly flag.
248
249 If this attribute B<set to 1>, the cookie will got HTTPOnly flag that should
250 prevent client side Javascript accessing the cookie value - this makes some
251 sort of session hijacking attacks significantly harder. Unfortunately not all
252 browsers support this flag (MSIE 6 SP1+, Firefox 3.0.0.6+, Opera 9.5+); if
253 a browser is not aware of HTTPOnly the flag will be ignored.
254
255 Default value is 1.
256
257 Note1: Many people are confused by the name "HTTPOnly" - it B<does not mean>
258 that this cookie works only over HTTP and not over HTTPS.
259
260 Note2: This parameter requires Catalyst::Runtime 5.80005 otherwise is skipped.
261
262 =item cookie_samesite
263
264 This attribute configures the value of the
265 L<SameSite|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite>
266 flag.
267
268 If set to None, the cookie will be sent when making cross origin requests,
269 including following links from other origins. This requires the
270 L</cookie_secure> flag to be set.
271
272 If set to Lax, the cookie will not be included when embedded in or fetched from
273 other origins, but will be included when following cross origin links.
274
275 If set to Strict, the cookie will not be included for any cross origin requests,
276 including links from different origins.
277
278 Default value is C<Lax>. This is the default modern browsers use.
279
280 Note: This parameter requires Catalyst::Runtime 5.90125 otherwise is skipped.
281
282 =item cookie_path
283
284 The path of the request url where cookie should be baked.
285
286 =back
287
288 For example, you could stick this in MyApp.pm:
289
290     __PACKAGE__->config( 'Plugin::Session' => {
291         cookie_domain  => '.mydomain.com',
292     });
293
294 =head1 CAVEATS
295
296 Sessions have to be created before the first write to be saved. For example:
297
298     sub action : Local {
299         my ( $self, $c ) = @_;
300         $c->res->write("foo");
301         $c->session( ... );
302         ...
303     }
304
305 Will cause a session ID to not be set, because by the time a session is
306 actually created the headers have already been sent to the client.
307
308 =head1 SEE ALSO
309
310 L<Catalyst>, L<Catalyst::Plugin::Session>.
311
312 =head1 AUTHORS
313
314 Yuval Kogman <nothingmuch@woobling.org>
315
316 =head1 CONTRIBUTORS
317
318 This module is derived from L<Catalyst::Plugin::Session::FastMmap> code, and
319 has been heavily modified since.
320
321 Andrew Ford
322
323 Andy Grundman
324
325 Christian Hansen
326
327 Marcus Ramberg
328
329 Jonathan Rockway <jrockway@cpan.org>
330
331 Sebastian Riedel
332
333 Florian Ragwitz
334
335 =head1 COPYRIGHT
336
337 Copyright (c) 2005 - 2009
338 the Catalyst::Plugin::Session::State::Cookie L</AUTHORS> and L</CONTRIBUTORS>
339 as listed above.
340
341 =head1 LICENSE
342
343 This program is free software, you can redistribute it and/or modify it
344 under the same terms as Perl itself.
345
346 =cut