Added a failing test for a situation that occurs when you have a cookie for a session...
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
CommitLineData
9e447f9d 1#!/usr/bin/perl
2
3package Catalyst::Plugin::Session;
9e447f9d 4
fff59d60 5use Moose;
6with 'MooseX::Emulate::Class::Accessor::Fast';
fe0d5ebe 7use MRO::Compat;
9e447f9d 8use Catalyst::Exception ();
9a9252c2 9use Digest ();
10use overload ();
d44bc687 11use Object::Signature ();
0466f6c7 12use Carp;
9e447f9d 13
c0c50a2c 14use namespace::clean -except => 'meta';
15
836b0a11 16our $VERSION = '0.28';
9a50355f 17$VERSION = eval $VERSION;
37160715 18
ab634fee 19my @session_data_accessors; # used in delete_session
fff59d60 20
21__PACKAGE__->mk_accessors(
ab634fee 22 "_session_delete_reason",
23 @session_data_accessors = qw/
4207ce8d 24 _sessionid
25 _session
26 _session_expires
260b14c4 27 _extended_session_expires
4207ce8d 28 _session_data_sig
4207ce8d 29 _flash
f4d79f85 30 _flash_keep_keys
31 _flash_key_hashes
1dbf4cd5 32 _tried_loading_session_id
33 _tried_loading_session_data
34 _tried_loading_session_expires
35 _tried_loading_flash_data
4207ce8d 36 /
fff59d60 37);
38
064c3709 39sub _session_plugin_config {
40 my $c = shift;
41 # FIXME - Start warning once all the state/store modules have also been updated.
42 #$c->log->warn("Deprecated 'session' config key used, please use the key 'Plugin::Session' instead")
43 # if exists $c->config->{session}
44 #$c->config->{'Plugin::Session'} ||= delete($c->config->{session}) || {};
45 $c->config->{'Plugin::Session'} ||= $c->config->{session} || {};
46}
9e447f9d 47
48sub setup {
9a9252c2 49 my $c = shift;
50
fe0d5ebe 51 $c->maybe::next::method(@_);
9a9252c2 52
53 $c->check_session_plugin_requirements;
54 $c->setup_session;
55
56 return $c;
9e447f9d 57}
58
59sub check_session_plugin_requirements {
9a9252c2 60 my $c = shift;
9e447f9d 61
9a9252c2 62 unless ( $c->isa("Catalyst::Plugin::Session::State")
63 && $c->isa("Catalyst::Plugin::Session::Store") )
64 {
65 my $err =
66 ( "The Session plugin requires both Session::State "
67 . "and Session::Store plugins to be used as well." );
9e447f9d 68
9a9252c2 69 $c->log->fatal($err);
70 Catalyst::Exception->throw($err);
71 }
9e447f9d 72}
73
74sub setup_session {
9a9252c2 75 my $c = shift;
9e447f9d 76
064c3709 77 my $cfg = $c->_session_plugin_config;
9e447f9d 78
9a9252c2 79 %$cfg = (
80 expires => 7200,
33641c1d 81 verify_address => 0,
06c621b5 82 verify_user_agent => 0,
9a9252c2 83 %$cfg,
84 );
9e447f9d 85
fe0d5ebe 86 $c->maybe::next::method();
9e447f9d 87}
88
19c130c2 89sub prepare_action {
90 my $c = shift;
91
3e2a53a8 92 $c->maybe::next::method(@_);
93
064c3709 94 if ( $c->_session_plugin_config->{flash_to_stash}
e5b2372a 95 and $c->sessionid
4207ce8d 96 and my $flash_data = $c->flash )
97 {
19c130c2 98 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
99 }
19c130c2 100}
101
ccc77553 102sub finalize_headers {
9a9252c2 103 my $c = shift;
9e447f9d 104
d3c97126 105 # fix cookie before we send headers
106 $c->_save_session_expires;
2a45c03b 107
fe0d5ebe 108 return $c->maybe::next::method(@_);
8f236527 109}
110
92eaec32 111sub finalize_body {
d3c97126 112 my $c = shift;
d3c97126 113
92eaec32 114 # We have to finalize our session *before* $c->engine->finalize_xxx is called,
115 # because we do not want to send the HTTP response before the session is stored/committed to
116 # the session database (or whatever Session::Store you use).
d3c97126 117 $c->finalize_session;
92eaec32 118
fe0d5ebe 119 return $c->maybe::next::method(@_);
d3c97126 120}
121
8f236527 122sub finalize_session {
123 my $c = shift;
124
fe0d5ebe 125 $c->maybe::next::method(@_);
49727697 126
9760b704 127 $c->_save_session_id;
9b0fa2a6 128 $c->_save_session;
129 $c->_save_flash;
130
260b14c4 131 $c->_clear_session_instance_data;
9b0fa2a6 132}
133
b92ce604 134sub _save_session_id {
135 my $c = shift;
23a2bf16 136
137 # we already called set when allocating
138 # no need to tell the state plugins anything new
b92ce604 139}
140
1dbf4cd5 141sub _save_session_expires {
9b0fa2a6 142 my $c = shift;
4207ce8d 143
260b14c4 144 if ( defined($c->_session_expires) ) {
145 my $expires = $c->session_expires; # force extension
146
1dbf4cd5 147 my $sid = $c->sessionid;
148 $c->store_session_data( "expires:$sid" => $expires );
1dbf4cd5 149 }
150}
6687905d 151
1dbf4cd5 152sub _save_session {
153 my $c = shift;
7a02371f 154
1dbf4cd5 155 if ( my $session_data = $c->_session ) {
d44bc687 156
1dbf4cd5 157 no warnings 'uninitialized';
158 if ( Object::Signature::signature($session_data) ne
159 $c->_session_data_sig )
160 {
161 $session_data->{__updated} = time();
162 my $sid = $c->sessionid;
163 $c->store_session_data( "session:$sid" => $session_data );
ea972e9a 164 }
9a9252c2 165 }
9b0fa2a6 166}
9a9252c2 167
9b0fa2a6 168sub _save_flash {
169 my $c = shift;
170
1dbf4cd5 171 if ( my $flash_data = $c->_flash ) {
4207ce8d 172
1dbf4cd5 173 my $hashes = $c->_flash_key_hashes || {};
174 my $keep = $c->_flash_keep_keys || {};
175 foreach my $key ( keys %$hashes ) {
176 if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
177 delete $flash_data->{$key};
23fbca00 178 }
ea972e9a 179 }
af1e4bc8 180
1dbf4cd5 181 my $sid = $c->sessionid;
182
eb250519 183 my $session_data = $c->_session;
1dbf4cd5 184 if (%$flash_data) {
eb250519 185 $session_data->{__flash} = $flash_data;
1dbf4cd5 186 }
187 else {
eb250519 188 delete $session_data->{__flash};
1dbf4cd5 189 }
eb250519 190 $c->_session($session_data);
191 $c->_save_session;
9b0fa2a6 192 }
9e447f9d 193}
194
e5b2372a 195sub _load_session_expires {
196 my $c = shift;
b92ce604 197 return $c->_session_expires if $c->_tried_loading_session_expires;
1dbf4cd5 198 $c->_tried_loading_session_expires(1);
e5b2372a 199
200 if ( my $sid = $c->sessionid ) {
201 my $expires = $c->get_session_data("expires:$sid") || 0;
202
203 if ( $expires >= time() ) {
260b14c4 204 $c->_session_expires( $expires );
205 return $expires;
e5b2372a 206 } else {
207 $c->delete_session( "session expired" );
208 return 0;
209 }
e5b2372a 210 }
1dbf4cd5 211
212 return;
e5b2372a 213}
214
b7acf64e 215sub _load_session {
216 my $c = shift;
b92ce604 217 return $c->_session if $c->_tried_loading_session_data;
1dbf4cd5 218 $c->_tried_loading_session_data(1);
b7acf64e 219
e5b2372a 220 if ( my $sid = $c->sessionid ) {
260b14c4 221 if ( $c->_load_session_expires ) { # > 0
0974ac06 222
2e412459 223 my $session_data = $c->get_session_data("session:$sid") || return;
6687905d 224 $c->_session($session_data);
3f182468 225
6687905d 226 no warnings 'uninitialized'; # ne __address
064c3709 227 if ( $c->_session_plugin_config->{verify_address}
6687905d 228 && $session_data->{__address} ne $c->request->address )
229 {
230 $c->log->warn(
231 "Deleting session $sid due to address mismatch ("
232 . $session_data->{__address} . " != "
47ca362e 233 . $c->request->address . ")"
6687905d 234 );
235 $c->delete_session("address mismatch");
236 return;
237 }
064c3709 238 if ( $c->_session_plugin_config->{verify_user_agent}
06c621b5 239 && $session_data->{__user_agent} ne $c->request->user_agent )
240 {
241 $c->log->warn(
242 "Deleting session $sid due to user agent mismatch ("
243 . $session_data->{__user_agent} . " != "
244 . $c->request->user_agent . ")"
245 );
246 $c->delete_session("user agent mismatch");
247 return;
248 }
4207ce8d 249
6687905d 250 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
168d6819 251 $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
6687905d 252 $c->_expire_session_keys;
4207ce8d 253
6687905d 254 return $session_data;
3f182468 255 }
9a9252c2 256 }
29d15411 257
4207ce8d 258 return;
b7acf64e 259}
9a9252c2 260
9b0fa2a6 261sub _load_flash {
262 my $c = shift;
b92ce604 263 return $c->_flash if $c->_tried_loading_flash_data;
1dbf4cd5 264 $c->_tried_loading_flash_data(1);
9b0fa2a6 265
e5b2372a 266 if ( my $sid = $c->sessionid ) {
eb250519 267
268 my $session_data = $c->session;
269 $c->_flash($session_data->{__flash});
270
271 if ( my $flash_data = $c->_flash )
4207ce8d 272 {
f4d79f85 273 $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
af1e4bc8 274
9b0fa2a6 275 return $flash_data;
276 }
277 }
278
1dbf4cd5 279 return;
9b0fa2a6 280}
281
d44bc687 282sub _expire_session_keys {
b7acf64e 283 my ( $c, $data ) = @_;
284
285 my $now = time;
286
e5b2372a 287 my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
288 foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
b7acf64e 289 delete $c->_session->{$key};
e5b2372a 290 delete $expire_times->{$key};
b7acf64e 291 }
9e447f9d 292}
293
260b14c4 294sub _clear_session_instance_data {
295 my $c = shift;
296 $c->$_(undef) for @session_data_accessors;
fe0d5ebe 297 $c->maybe::next::method(@_); # allow other plugins to hook in on this
260b14c4 298}
299
0ade68bd 300sub change_session_id {
301 my $c = shift;
302
303 my $sessiondata = $c->session;
304 my $oldsid = $c->sessionid;
305 my $newsid = $c->create_session_id;
af1e4bc8 306
0ade68bd 307 if ($oldsid) {
308 $c->log->debug(qq/change_sessid: deleting session data from "$oldsid"/) if $c->debug;
309 $c->delete_session_data("${_}:${oldsid}") for qw/session expires flash/;
310 }
311
312 $c->log->debug(qq/change_sessid: storing session data to "$newsid"/) if $c->debug;
313 $c->store_session_data( "session:$newsid" => $sessiondata );
314
af1e4bc8 315 return $newsid;
0ade68bd 316}
317
9e447f9d 318sub delete_session {
9a9252c2 319 my ( $c, $msg ) = @_;
9e447f9d 320
49727697 321 $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
b92ce604 322
9a9252c2 323 # delete the session data
b92ce604 324 if ( my $sid = $c->sessionid ) {
325 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
c6eaa93a 326 $c->delete_session_id($sid);
b92ce604 327 }
e5b2372a 328
9a9252c2 329 # reset the values in the context object
ab634fee 330 # see the BEGIN block
260b14c4 331 $c->_clear_session_instance_data;
6687905d 332
29d15411 333 $c->_session_delete_reason($msg);
334}
335
336sub session_delete_reason {
337 my $c = shift;
338
e5b2372a 339 $c->session_is_valid; # check that it was loaded
29d15411 340
4207ce8d 341 $c->_session_delete_reason(@_);
9e447f9d 342}
343
6687905d 344sub session_expires {
e5b2372a 345 my $c = shift;
6687905d 346
260b14c4 347 if ( defined( my $expires = $c->_extended_session_expires ) ) {
e5b2372a 348 return $expires;
1dbf4cd5 349 } elsif ( defined( $expires = $c->_load_session_expires ) ) {
260b14c4 350 return $c->extend_session_expires( $expires );
e5b2372a 351 } else {
1dbf4cd5 352 return 0;
e5b2372a 353 }
354}
6687905d 355
e5b2372a 356sub extend_session_expires {
357 my ( $c, $expires ) = @_;
260b14c4 358 $c->_extended_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
4c79feb6 359 $c->extend_session_id( $c->sessionid, $updated );
e5b2372a 360 return $updated;
361}
6687905d 362
e5b2372a 363sub calculate_initial_session_expires {
364 my $c = shift;
064c3709 365 return ( time() + $c->_session_plugin_config->{expires} );
e5b2372a 366}
367
368sub calculate_extended_session_expires {
369 my ( $c, $prev ) = @_;
370 $c->calculate_initial_session_expires;
371}
372
373sub reset_session_expires {
374 my ( $c, $sid ) = @_;
af1e4bc8 375
260b14c4 376 my $exp = $c->calculate_initial_session_expires;
377 $c->_session_expires( $exp );
378 $c->_extended_session_expires( $exp );
e5b2372a 379 $exp;
6687905d 380}
381
0974ac06 382sub sessionid {
4207ce8d 383 my $c = shift;
af1e4bc8 384
1dbf4cd5 385 return $c->_sessionid || $c->_load_sessionid;
386}
387
388sub _load_sessionid {
389 my $c = shift;
390 return if $c->_tried_loading_session_id;
391 $c->_tried_loading_session_id(1);
392
393 if ( defined( my $sid = $c->get_session_id ) ) {
394 if ( $c->validate_session_id($sid) ) {
ec299c02 395 # temporarily set the inner key, so that validation will work
396 $c->_sessionid($sid);
1dbf4cd5 397 return $sid;
398 } else {
399 my $err = "Tried to set invalid session ID '$sid'";
400 $c->log->error($err);
401 Catalyst::Exception->throw($err);
4207ce8d 402 }
403 }
1dbf4cd5 404
405 return;
e5b2372a 406}
407
408sub session_is_valid {
409 my $c = shift;
4207ce8d 410
b92ce604 411 # force a check for expiry, but also __address, etc
412 if ( $c->_load_session ) {
e5b2372a 413 return 1;
414 } else {
415 return;
416 }
0974ac06 417}
418
419sub validate_session_id {
4207ce8d 420 my ( $c, $sid ) = @_;
0974ac06 421
4207ce8d 422 $sid and $sid =~ /^[a-f\d]+$/i;
0974ac06 423}
424
9e447f9d 425sub session {
9a9252c2 426 my $c = shift;
9e447f9d 427
29d15411 428 $c->_session || $c->_load_session || do {
1dbf4cd5 429 $c->create_session_id_if_needed;
29d15411 430 $c->initialize_session_data;
4207ce8d 431 };
9e447f9d 432}
433
f4d79f85 434sub keep_flash {
435 my ( $c, @keys ) = @_;
2e412459 436 my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
437 (@{$href}{@keys}) = ((undef) x @keys);
f4d79f85 438}
439
af1e4bc8 440sub _flash_data {
873f7011 441 my $c = shift;
78476ce0 442 $c->_flash || $c->_load_flash || do {
1dbf4cd5 443 $c->create_session_id_if_needed;
78476ce0 444 $c->_flash( {} );
c9396824 445 };
446}
447
448sub _set_flash {
449 my $c = shift;
450 if (@_) {
451 my $items = @_ > 1 ? {@_} : $_[0];
452 croak('flash takes a hash or hashref') unless ref $items;
453 @{ $c->_flash }{ keys %$items } = values %$items;
1dbf4cd5 454 }
873f7011 455}
456
c9396824 457sub flash {
458 my $c = shift;
459 $c->_flash_data;
f6009cac 460 $c->_set_flash(@_);
461 return $c->_flash;
c9396824 462}
463
49727697 464sub clear_flash {
465 my $c = shift;
af1e4bc8 466
5a1f6ed4 467 #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
49727697 468 $c->_flash_key_hashes({});
5a1f6ed4 469 $c->_flash_keep_keys({});
49727697 470 $c->_flash({});
471}
472
b7acf64e 473sub session_expire_key {
474 my ( $c, %keys ) = @_;
475
476 my $now = time;
4207ce8d 477 @{ $c->session->{__expire_keys} }{ keys %keys } =
478 map { $now + $_ } values %keys;
b7acf64e 479}
480
9e447f9d 481sub initialize_session_data {
9a9252c2 482 my $c = shift;
9e447f9d 483
9a9252c2 484 my $now = time;
9e447f9d 485
4207ce8d 486 return $c->_session(
487 {
488 __created => $now,
489 __updated => $now,
490
491 (
064c3709 492 $c->_session_plugin_config->{verify_address}
f8f81744 493 ? ( __address => $c->request->address||'' )
4207ce8d 494 : ()
495 ),
06c621b5 496 (
064c3709 497 $c->_session_plugin_config->{verify_user_agent}
f8f81744 498 ? ( __user_agent => $c->request->user_agent||'' )
06c621b5 499 : ()
500 ),
4207ce8d 501 }
502 );
9e447f9d 503}
504
9e447f9d 505sub generate_session_id {
506 my $c = shift;
507
508 my $digest = $c->_find_digest();
509 $digest->add( $c->session_hash_seed() );
510 return $digest->hexdigest;
511}
512
1dbf4cd5 513sub create_session_id_if_needed {
78476ce0 514 my $c = shift;
1dbf4cd5 515 $c->create_session_id unless $c->sessionid;
516}
78476ce0 517
1dbf4cd5 518sub create_session_id {
519 my $c = shift;
af1e4bc8 520
e5b2372a 521 my $sid = $c->generate_session_id;
78476ce0 522
e5b2372a 523 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
78476ce0 524
e5b2372a 525 $c->_sessionid($sid);
526 $c->reset_session_expires;
527 $c->set_session_id($sid);
528
529 return $sid;
78476ce0 530}
531
9e447f9d 532my $counter;
9a9252c2 533
9e447f9d 534sub session_hash_seed {
9a9252c2 535 my $c = shift;
536
537 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 538}
539
540my $usable;
9a9252c2 541
9e447f9d 542sub _find_digest () {
9a9252c2 543 unless ($usable) {
d44bc687 544 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
4207ce8d 545 if ( eval { Digest->new($alg) } ) {
5faaa4b0 546 $usable = $alg;
547 last;
548 }
7d139eeb 549 }
4207ce8d 550 Catalyst::Exception->throw(
9a9252c2 551 "Could not find a suitable Digest module. Please install "
4207ce8d 552 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
553 unless $usable;
9a9252c2 554 }
9e447f9d 555
556 return Digest->new($usable);
557}
558
99b2191e 559sub dump_these {
560 my $c = shift;
561
562 (
fe0d5ebe 563 $c->maybe::next::method(),
99b2191e 564
565 $c->sessionid
566 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
567 : ()
568 );
569}
570
e5b2372a 571
fe0d5ebe 572sub get_session_id { shift->maybe::next::method(@_) }
573sub set_session_id { shift->maybe::next::method(@_) }
574sub delete_session_id { shift->maybe::next::method(@_) }
575sub extend_session_id { shift->maybe::next::method(@_) }
e5b2372a 576
9e447f9d 577__PACKAGE__;
578
579__END__
580
581=pod
582
583=head1 NAME
584
7048c24e 585Catalyst::Plugin::Session - Generic Session plugin - ties together server side storage and client side state required to maintain session data.
9e447f9d 586
587=head1 SYNOPSIS
588
8f0b4c16 589 # To get sessions to "just work", all you need to do is use these plugins:
590
591 use Catalyst qw/
592 Session
593 Session::Store::FastMmap
594 Session::State::Cookie
595 /;
596
7048c24e 597 # you can replace Store::FastMmap with Store::File - both have sensible
598 # default configurations (see their docs for details)
8f0b4c16 599
7048c24e 600 # more complicated backends are available for other scenarios (DBI storage,
601 # etc)
8f0b4c16 602
603
604 # after you've loaded the plugins you can save session data
605 # For example, if you are writing a shopping cart, it could be implemented
606 # like this:
9e447f9d 607
229a5b53 608 sub add_item : Local {
609 my ( $self, $c ) = @_;
610
611 my $item_id = $c->req->param("item");
612
8f0b4c16 613 # $c->session is a hash ref, a bit like $c->stash
614 # the difference is that it' preserved across requests
229a5b53 615
616 push @{ $c->session->{items} }, $item_id;
617
618 $c->forward("MyView");
619 }
620
621 sub display_items : Local {
622 my ( $self, $c ) = @_;
623
624 # values in $c->session are restored
625 $c->stash->{items_to_display} =
8f0b4c16 626 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
229a5b53 627
628 $c->forward("MyView");
629 }
630
9e447f9d 631=head1 DESCRIPTION
632
633The Session plugin is the base of two related parts of functionality required
634for session management in web applications.
635
636The first part, the State, is getting the browser to repeat back a session key,
637so that the web application can identify the client and logically string
638several requests together into a session.
639
640The second part, the Store, deals with the actual storage of information about
641the client. This data is stored so that the it may be revived for every request
642made by the same client.
643
644This plugin links the two pieces together.
645
2a323f6f 646=head1 RECOMENDED BACKENDS
8f0b4c16 647
648=over 4
649
650=item Session::State::Cookie
651
652The only really sane way to do state is using cookies.
653
654=item Session::Store::File
655
656A portable backend, based on Cache::File.
657
658=item Session::Store::FastMmap
659
660A fast and flexible backend, based on Cache::FastMmap.
661
662=back
663
9e447f9d 664=head1 METHODS
665
666=over 4
667
668=item sessionid
669
670An accessor for the session ID value.
671
672=item session
673
674Returns a hash reference that might contain unserialized values from previous
675requests in the same session, and whose modified value will be saved for future
676requests.
677
678This method will automatically create a new session and session ID if none
679exists.
680
ab634fee 681=item session_expires
682
683=item session_expires $reset
684
685This method returns the time when the current session will expire, or 0 if
686there is no current session. If there is a session and it already expired, it
687will delete the session and return 0 as well.
688
689If the C<$reset> parameter is true, and there is a session ID the expiry time
690will be reset to the current time plus the time to live (see
691L</CONFIGURATION>). This is used when creating a new session.
692
07e714d2 693=item flash
694
695This is like Ruby on Rails' flash data structure. Think of it as a stash that
44ab6d1c 696lasts for longer than one request, letting you redirect instead of forward.
697
698The flash data will be cleaned up only on requests on which actually use
699$c->flash (thus allowing multiple redirections), and the policy is to delete
bf6bd311 700all the keys which haven't changed since the flash data was loaded at the end
701of every request.
07e714d2 702
703 sub moose : Local {
704 my ( $self, $c ) = @_;
705
706 $c->flash->{beans} = 10;
707 $c->response->redirect( $c->uri_for("foo") );
708 }
709
710 sub foo : Local {
711 my ( $self, $c ) = @_;
712
713 my $value = $c->flash->{beans};
714
715 # ...
716
717 $c->response->redirect( $c->uri_for("bar") );
718 }
719
720 sub bar : Local {
721 my ( $self, $c ) = @_;
722
723 if ( exists $c->flash->{beans} ) { # false
af1e4bc8 724
07e714d2 725 }
726 }
727
49727697 728=item clear_flash
729
730Zap all the keys in the flash regardless of their current state.
731
bf6bd311 732=item keep_flash @keys
733
1961343c 734If you want to keep a flash key for the next request too, even if it hasn't
bf6bd311 735changed, call C<keep_flash> and pass in the keys as arguments.
736
fffeb18f 737=item delete_session REASON
738
739This method is used to invalidate a session. It takes an optional parameter
740which will be saved in C<session_delete_reason> if provided.
741
eb250519 742NOTE: This method will B<also> delete your flash data.
743
9e447f9d 744=item session_delete_reason
745
746This accessor contains a string with the reason a session was deleted. Possible
747values include:
748
749=over 4
750
751=item *
752
753C<address mismatch>
754
755=item *
756
757C<session expired>
758
759=back
760
b7acf64e 761=item session_expire_key $key, $ttl
762
763Mark a key to expire at a certain time (only useful when shorter than the
764expiry time for the whole session).
765
766For example:
767
064c3709 768 __PACKAGE__->config('Plugin::Session' => { expires => 1000000000000 }); # forever
b7acf64e 769
770 # later
771
772 $c->session_expire_key( __user => 3600 );
773
774Will make the session data survive, but the user will still be logged out after
775an hour.
776
777Note that these values are not auto extended.
778
0ade68bd 779=item change_session_id
780
781By calling this method you can force a session id change while keeping all
782session data. This method might come handy when you are paranoid about some
783advanced variations of session fixation attack.
784
785If you want to prevent this session fixation scenario:
786
787 0) let us have WebApp with anonymous and authenticated parts
af1e4bc8 788 1) a hacker goes to vulnerable WebApp and gets a real sessionid,
0ade68bd 789 just by browsing anonymous part of WebApp
790 2) the hacker inserts (somehow) this values into a cookie in victim's browser
791 3) after the victim logs into WebApp the hacker can enter his/her session
792
793you should call change_session_id in your login controller like this:
794
795 if ($c->authenticate( { username => $user, password => $pass } )) {
796 # login OK
797 $c->change_session_id;
798 ...
799 } else {
800 # login FAILED
801 ...
802 }
803
8f0b4c16 804=back
805
10c72079 806=head1 INTERNAL METHODS
8f0b4c16 807
808=over 4
809
9e447f9d 810=item setup
811
812This method is extended to also make calls to
813C<check_session_plugin_requirements> and C<setup_session>.
814
815=item check_session_plugin_requirements
816
817This method ensures that a State and a Store plugin are also in use by the
818application.
819
820=item setup_session
821
064c3709 822This method populates C<< $c->config('Plugin::Session') >> with the default values
9e447f9d 823listed in L</CONFIGURATION>.
824
825=item prepare_action
826
bfa4f9cc 827This method is extended.
68fd02ae 828
bfa4f9cc 829Its only effect is if the (off by default) C<flash_to_stash> configuration
68fd02ae 830parameter is on - then it will copy the contents of the flash to the stash at
831prepare time.
9e447f9d 832
ccc77553 833=item finalize_headers
9e447f9d 834
d3c97126 835This method is extended and will extend the expiry time before sending
836the response.
837
92eaec32 838=item finalize_body
d3c97126 839
92eaec32 840This method is extended and will call finalize_session before the other
841finalize_body methods run. Here we persist the session data if a session exists.
9e447f9d 842
9e447f9d 843=item initialize_session_data
844
845This method will initialize the internal structure of the session, and is
846called by the C<session> method if appropriate.
847
68fd02ae 848=item create_session_id
849
bfa4f9cc 850Creates a new session ID using C<generate_session_id> if there is no session ID
68fd02ae 851yet.
852
ab634fee 853=item validate_session_id SID
854
855Make sure a session ID is of the right format.
856
857This currently ensures that the session ID string is any amount of case
858insensitive hexadecimal characters.
859
229a5b53 860=item generate_session_id
861
862This method will return a string that can be used as a session ID. It is
863supposed to be a reasonably random string with enough bits to prevent
864collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
bfa4f9cc 865MD5 or SHA-256, depending on the availability of these modules.
229a5b53 866
867=item session_hash_seed
868
869This method is actually rather internal to generate_session_id, but should be
870overridable in case you want to provide more random data.
871
872Currently it returns a concatenated string which contains:
873
874=over 4
875
7048c24e 876=item * A counter
229a5b53 877
7048c24e 878=item * The current time
229a5b53 879
7048c24e 880=item * One value from C<rand>.
229a5b53 881
7048c24e 882=item * The stringified value of a newly allocated hash reference
229a5b53 883
7048c24e 884=item * The stringified value of the Catalyst context object
229a5b53 885
886=back
887
bfa4f9cc 888in the hopes that those combined values are entropic enough for most uses. If
229a5b53 889this is not the case you can replace C<session_hash_seed> with e.g.
890
891 sub session_hash_seed {
892 open my $fh, "<", "/dev/random";
893 read $fh, my $bytes, 20;
894 close $fh;
895 return $bytes;
896 }
897
898Or even more directly, replace C<generate_session_id>:
899
900 sub generate_session_id {
901 open my $fh, "<", "/dev/random";
902 read $fh, my $bytes, 20;
903 close $fh;
904 return unpack("H*", $bytes);
905 }
906
907Also have a look at L<Crypt::Random> and the various openssl bindings - these
908modules provide APIs for cryptographically secure random data.
909
8f236527 910=item finalize_session
911
912Clean up the session during C<finalize>.
913
914This clears the various accessors after saving to the store.
915
99b2191e 916=item dump_these
917
918See L<Catalyst/dump_these> - ammends the session data structure to the list of
919dumped objects if session ID is defined.
920
d3c97126 921
922=item calculate_extended_session_expires
923
924=item calculate_initial_session_expires
925
926=item create_session_id_if_needed
927
928=item delete_session_id
929
930=item extend_session_expires
931
932=item extend_session_id
933
934=item get_session_id
935
936=item reset_session_expires
937
938=item session_is_valid
939
940=item set_session_id
941
9e447f9d 942=back
943
a92c8aeb 944=head1 USING SESSIONS DURING PREPARE
945
946The earliest point in time at which you may use the session data is after
947L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
948
949State plugins must set $c->session ID before C<prepare_action>, and during
950C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
951the store.
952
7048c24e 953 sub prepare_action {
954 my $c = shift;
a92c8aeb 955
7048c24e 956 # don't touch $c->session yet!
b1cd7d77 957
7048c24e 958 $c->NEXT::prepare_action( @_ );
a92c8aeb 959
7048c24e 960 $c->session; # this is OK
961 $c->sessionid; # this is also OK
962 }
a92c8aeb 963
9e447f9d 964=head1 CONFIGURATION
965
064c3709 966 $c->config('Plugin::Session' => {
229a5b53 967 expires => 1234,
064c3709 968 });
9e447f9d 969
970All configuation parameters are provided in a hash reference under the
064c3709 971C<Plugin::Session> key in the configuration hash.
9e447f9d 972
973=over 4
974
975=item expires
976
977The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
978hours).
979
980=item verify_address
981
8c7e922c 982When true, C<<$c->request->address>> will be checked at prepare time. If it is
983not the same as the address that initiated the session, the session is deleted.
9e447f9d 984
33641c1d 985Defaults to false.
986
06c621b5 987=item verify_user_agent
988
989When true, C<<$c->request->user_agent>> will be checked at prepare time. If it
af1e4bc8 990is not the same as the user agent that initiated the session, the session is
06c621b5 991deleted.
992
993Defaults to false.
994
68fd02ae 995=item flash_to_stash
996
997This option makes it easier to have actions behave the same whether they were
998forwarded to or redirected to. On prepare time it copies the contents of
999C<flash> (if any) to the stash.
1000
9e447f9d 1001=back
1002
1003=head1 SPECIAL KEYS
1004
1005The hash reference returned by C<< $c->session >> contains several keys which
1006are automatically set:
1007
1008=over 4
1009
1010=item __expires
1011
ab634fee 1012This key no longer exists. Use C<session_expires> instead.
9e447f9d 1013
1014=item __updated
1015
d44bc687 1016The last time a session was saved to the store.
9e447f9d 1017
1018=item __created
1019
1020The time when the session was first created.
1021
1022=item __address
1023
1024The value of C<< $c->request->address >> at the time the session was created.
8c7e922c 1025This value is only populated if C<verify_address> is true in the configuration.
9e447f9d 1026
06c621b5 1027=item __user_agent
1028
e79a686c 1029The value of C<< $c->request->user_agent >> at the time the session was created.
06c621b5 1030This value is only populated if C<verify_user_agent> is true in the configuration.
1031
9e447f9d 1032=back
1033
c80e9f04 1034=head1 CAVEATS
1035
a552e4b5 1036=head2 Round the Robin Proxies
1037
c80e9f04 1038C<verify_address> could make your site inaccessible to users who are behind
1039load balanced proxies. Some ISPs may give a different IP to each request by the
1040same client due to this type of proxying. If addresses are verified these
1041users' sessions cannot persist.
1042
1043To let these users access your site you can either disable address verification
1044as a whole, or provide a checkbox in the login dialog that tells the server
1045that it's OK for the address of the client to change. When the server sees that
bfa4f9cc 1046this box is checked it should delete the C<__address> special key from the
c80e9f04 1047session hash when the hash is first created.
1048
a552e4b5 1049=head2 Race Conditions
1050
bfa4f9cc 1051In this day and age where cleaning detergents and Dutch football (not the
1052American kind) teams roam the plains in great numbers, requests may happen
a552e4b5 1053simultaneously. This means that there is some risk of session data being
1054overwritten, like this:
1055
1056=over 4
1057
1058=item 1.
1059
bfa4f9cc 1060request a starts, request b starts, with the same session ID
a552e4b5 1061
1062=item 2.
1063
1064session data is loaded in request a
1065
1066=item 3.
1067
1068session data is loaded in request b
1069
1070=item 4.
1071
1072session data is changed in request a
1073
1074=item 5.
1075
1076request a finishes, session data is updated and written to store
1077
1078=item 6.
1079
1080request b finishes, session data is updated and written to store, overwriting
1081changes by request a
1082
1083=back
1084
bfa4f9cc 1085If this is a concern in your application, a soon-to-be-developed locking
a552e4b5 1086solution is the only safe way to go. This will have a bigger overhead.
1087
1088For applications where any given user is only making one request at a time this
1089plugin should be safe enough.
1090
d45028d6 1091=head1 AUTHORS
1092
7048c24e 1093Andy Grundman
baa9db9c 1094
7048c24e 1095Christian Hansen
baa9db9c 1096
2842d938 1097Yuval Kogman, C<nothingmuch@woobling.org>
baa9db9c 1098
7048c24e 1099Sebastian Riedel
baa9db9c 1100
2842d938 1101Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1102
1103Sergio Salvi
1104
af1e4bc8 1105kmx C<kmx@volny.cz>
1106
836b0a11 1107Florian Ragwitz (rafl) C<rafl@debian.org>
1108
1109Kent Fredric (kentnl)
1110
baa9db9c 1111And countless other contributers from #catalyst. Thanks guys!
d45028d6 1112
cc40ae4b 1113=head1 COPYRIGHT & LICENSE
d45028d6 1114
7048c24e 1115 Copyright (c) 2005 the aforementioned authors. All rights
1116 reserved. This program is free software; you can redistribute
1117 it and/or modify it under the same terms as Perl itself.
d45028d6 1118
9e447f9d 1119=cut
1120
1121