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