3 package Catalyst::Plugin::Session;
7 use Catalyst::Exception ();
10 use Object::Signature ();
13 use namespace::clean -except => 'meta';
15 our $VERSION = '0.25';
17 # used in delete_session
18 my @session_data_accessors = qw/
22 _extended_session_expires
27 _tried_loading_session_id
28 _tried_loading_session_data
29 _tried_loading_session_expires
30 _tried_loading_flash_data
33 has '_session_delete_reason' => ( is => 'rw');
34 foreach (@session_data_accessors) {
35 has $_ => ( is => 'rw');
39 before 'setup_finalize' => sub {
42 $c->check_session_plugin_requirements;
48 sub check_session_plugin_requirements {
51 unless ( $c->isa("Catalyst::Plugin::Session::State")
52 && $c->isa("Catalyst::Plugin::Session::Store") )
55 ( "The Session plugin requires both Session::State "
56 . "and Session::Store plugins to be used as well." );
59 Catalyst::Exception->throw($err);
66 my $cfg = ( $c->config->{session} ||= {} );
71 verify_user_agent => 0,
75 $c->maybe::next::method();
78 before 'prepare_action' => sub {
81 if ( $c->config->{session}{flash_to_stash}
83 and my $flash_data = $c->flash )
85 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
90 before 'finalize_headers' => sub {
93 # fix cookie before we send headers
94 $c->_save_session_expires;
98 before 'finalize_body' => sub {
101 # We have to finalize our session *before* $c->engine->finalize_xxx is called,
102 # because we do not want to send the HTTP response before the session is stored/committed to
103 # the session database (or whatever Session::Store you use).
104 $c->finalize_session;
108 sub finalize_session {
111 $c->maybe::next::method(@_);
113 $c->_save_session_id;
117 $c->_clear_session_instance_data;
120 sub _save_session_id {
123 # we already called set when allocating
124 # no need to tell the state plugins anything new
127 sub _save_session_expires {
130 if ( defined($c->_session_expires) ) {
131 my $expires = $c->session_expires; # force extension
133 my $sid = $c->sessionid;
134 $c->store_session_data( "expires:$sid" => $expires );
141 if ( my $session_data = $c->_session ) {
143 no warnings 'uninitialized';
144 if ( Object::Signature::signature($session_data) ne
145 $c->_session_data_sig )
147 $session_data->{__updated} = time();
148 my $sid = $c->sessionid;
149 $c->store_session_data( "session:$sid" => $session_data );
157 if ( my $flash_data = $c->_flash ) {
159 my $hashes = $c->_flash_key_hashes || {};
160 my $keep = $c->_flash_keep_keys || {};
161 foreach my $key ( keys %$hashes ) {
162 if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
163 delete $flash_data->{$key};
167 my $sid = $c->sessionid;
169 my $session_data = $c->_session;
171 $session_data->{__flash} = $flash_data;
174 delete $session_data->{__flash};
176 $c->_session($session_data);
181 sub _load_session_expires {
183 return $c->_session_expires if $c->_tried_loading_session_expires;
184 $c->_tried_loading_session_expires(1);
186 if ( my $sid = $c->sessionid ) {
187 my $expires = $c->get_session_data("expires:$sid") || 0;
189 if ( $expires >= time() ) {
190 $c->_session_expires( $expires );
193 $c->delete_session( "session expired" );
203 return $c->_session if $c->_tried_loading_session_data;
204 $c->_tried_loading_session_data(1);
206 if ( my $sid = $c->sessionid ) {
207 if ( $c->_load_session_expires ) { # > 0
209 my $session_data = $c->get_session_data("session:$sid") || return;
210 $c->_session($session_data);
212 no warnings 'uninitialized'; # ne __address
213 if ( $c->config->{session}{verify_address}
214 && $session_data->{__address} ne $c->request->address )
217 "Deleting session $sid due to address mismatch ("
218 . $session_data->{__address} . " != "
219 . $c->request->address . ")"
221 $c->delete_session("address mismatch");
224 if ( $c->config->{session}{verify_user_agent}
225 && $session_data->{__user_agent} ne $c->request->user_agent )
228 "Deleting session $sid due to user agent mismatch ("
229 . $session_data->{__user_agent} . " != "
230 . $c->request->user_agent . ")"
232 $c->delete_session("user agent mismatch");
236 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
237 $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
238 $c->_expire_session_keys;
240 return $session_data;
249 return $c->_flash if $c->_tried_loading_flash_data;
250 $c->_tried_loading_flash_data(1);
252 if ( my $sid = $c->sessionid ) {
254 my $session_data = $c->session;
255 $c->_flash($session_data->{__flash});
257 if ( my $flash_data = $c->_flash )
259 $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
268 sub _expire_session_keys {
269 my ( $c, $data ) = @_;
273 my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
274 foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
275 delete $c->_session->{$key};
276 delete $expire_times->{$key};
280 sub _clear_session_instance_data {
282 $c->$_(undef) for @session_data_accessors;
283 $c->maybe::next::method(@_); # allow other plugins to hook in on this
286 sub change_session_id {
289 my $sessiondata = $c->session;
290 my $oldsid = $c->sessionid;
291 my $newsid = $c->create_session_id;
294 $c->log->debug(qq/change_sessid: deleting session data from "$oldsid"/) if $c->debug;
295 $c->delete_session_data("${_}:${oldsid}") for qw/session expires flash/;
298 $c->log->debug(qq/change_sessid: storing session data to "$newsid"/) if $c->debug;
299 $c->store_session_data( "session:$newsid" => $sessiondata );
305 my ( $c, $msg ) = @_;
307 $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
309 # delete the session data
310 if ( my $sid = $c->sessionid ) {
311 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
312 $c->delete_session_id($sid);
315 # reset the values in the context object
316 # see the BEGIN block
317 $c->_clear_session_instance_data;
319 $c->_session_delete_reason($msg);
322 sub session_delete_reason {
325 $c->session_is_valid; # check that it was loaded
327 $c->_session_delete_reason(@_);
330 sub session_expires {
333 if ( defined( my $expires = $c->_extended_session_expires ) ) {
335 } elsif ( defined( $expires = $c->_load_session_expires ) ) {
336 return $c->extend_session_expires( $expires );
342 sub extend_session_expires {
343 my ( $c, $expires ) = @_;
344 $c->_extended_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
345 $c->extend_session_id( $c->sessionid, $updated );
349 sub calculate_initial_session_expires {
351 return ( time() + $c->config->{session}{expires} );
354 sub calculate_extended_session_expires {
355 my ( $c, $prev ) = @_;
356 $c->calculate_initial_session_expires;
359 sub reset_session_expires {
360 my ( $c, $sid ) = @_;
362 my $exp = $c->calculate_initial_session_expires;
363 $c->_session_expires( $exp );
364 $c->_extended_session_expires( $exp );
371 return $c->_sessionid || $c->_load_sessionid;
374 sub _load_sessionid {
376 return if $c->_tried_loading_session_id;
377 $c->_tried_loading_session_id(1);
379 if ( defined( my $sid = $c->get_session_id ) ) {
380 if ( $c->validate_session_id($sid) ) {
381 # temporarily set the inner key, so that validation will work
382 $c->_sessionid($sid);
385 my $err = "Tried to set invalid session ID '$sid'";
386 $c->log->error($err);
387 Catalyst::Exception->throw($err);
394 sub session_is_valid {
397 # force a check for expiry, but also __address, etc
398 if ( $c->_load_session ) {
405 sub validate_session_id {
406 my ( $c, $sid ) = @_;
408 $sid and $sid =~ /^[a-f\d]+$/i;
414 $c->_session || $c->_load_session || do {
415 $c->create_session_id_if_needed;
416 $c->initialize_session_data;
421 my ( $c, @keys ) = @_;
422 my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
423 (@{$href}{@keys}) = ((undef) x @keys);
428 $c->_flash || $c->_load_flash || do {
429 $c->create_session_id_if_needed;
437 my $items = @_ > 1 ? {@_} : $_[0];
438 croak('flash takes a hash or hashref') unless ref $items;
439 @{ $c->_flash }{ keys %$items } = values %$items;
453 #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
454 $c->_flash_key_hashes({});
455 $c->_flash_keep_keys({});
459 sub session_expire_key {
460 my ( $c, %keys ) = @_;
463 @{ $c->session->{__expire_keys} }{ keys %keys } =
464 map { $now + $_ } values %keys;
467 sub initialize_session_data {
478 $c->config->{session}{verify_address}
479 ? ( __address => $c->request->address||'' )
483 $c->config->{session}{verify_user_agent}
484 ? ( __user_agent => $c->request->user_agent||'' )
491 sub generate_session_id {
494 my $digest = $c->_find_digest();
495 $digest->add( $c->session_hash_seed() );
496 return $digest->hexdigest;
499 sub create_session_id_if_needed {
501 $c->create_session_id unless $c->sessionid;
504 sub create_session_id {
507 my $sid = $c->generate_session_id;
509 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
511 $c->_sessionid($sid);
512 $c->reset_session_expires;
513 $c->set_session_id($sid);
520 sub session_hash_seed {
523 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
528 sub _find_digest () {
530 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
531 if ( eval { Digest->new($alg) } ) {
536 Catalyst::Exception->throw(
537 "Could not find a suitable Digest module. Please install "
538 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
542 return Digest->new($usable);
549 $c->maybe::next::method(),
552 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
558 sub get_session_id { shift->maybe::next::method(@_) }
559 sub set_session_id { shift->maybe::next::method(@_) }
560 sub delete_session_id { shift->maybe::next::method(@_) }
561 sub extend_session_id { shift->maybe::next::method(@_) }
571 Catalyst::Plugin::Session - Generic Session plugin - ties together server side storage and client side state required to maintain session data.
575 # To get sessions to "just work", all you need to do is use these plugins:
579 Session::Store::FastMmap
580 Session::State::Cookie
583 # you can replace Store::FastMmap with Store::File - both have sensible
584 # default configurations (see their docs for details)
586 # more complicated backends are available for other scenarios (DBI storage,
590 # after you've loaded the plugins you can save session data
591 # For example, if you are writing a shopping cart, it could be implemented
594 sub add_item : Local {
595 my ( $self, $c ) = @_;
597 my $item_id = $c->req->param("item");
599 # $c->session is a hash ref, a bit like $c->stash
600 # the difference is that it' preserved across requests
602 push @{ $c->session->{items} }, $item_id;
604 $c->forward("MyView");
607 sub display_items : Local {
608 my ( $self, $c ) = @_;
610 # values in $c->session are restored
611 $c->stash->{items_to_display} =
612 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
614 $c->forward("MyView");
619 The Session plugin is the base of two related parts of functionality required
620 for session management in web applications.
622 The first part, the State, is getting the browser to repeat back a session key,
623 so that the web application can identify the client and logically string
624 several requests together into a session.
626 The second part, the Store, deals with the actual storage of information about
627 the client. This data is stored so that the it may be revived for every request
628 made by the same client.
630 This plugin links the two pieces together.
632 =head1 RECOMENDED BACKENDS
636 =item Session::State::Cookie
638 The only really sane way to do state is using cookies.
640 =item Session::Store::File
642 A portable backend, based on Cache::File.
644 =item Session::Store::FastMmap
646 A fast and flexible backend, based on Cache::FastMmap.
656 An accessor for the session ID value.
660 Returns a hash reference that might contain unserialized values from previous
661 requests in the same session, and whose modified value will be saved for future
664 This method will automatically create a new session and session ID if none
667 =item session_expires
669 =item session_expires $reset
671 This method returns the time when the current session will expire, or 0 if
672 there is no current session. If there is a session and it already expired, it
673 will delete the session and return 0 as well.
675 If the C<$reset> parameter is true, and there is a session ID the expiry time
676 will be reset to the current time plus the time to live (see
677 L</CONFIGURATION>). This is used when creating a new session.
681 This is like Ruby on Rails' flash data structure. Think of it as a stash that
682 lasts for longer than one request, letting you redirect instead of forward.
684 The flash data will be cleaned up only on requests on which actually use
685 $c->flash (thus allowing multiple redirections), and the policy is to delete
686 all the keys which haven't changed since the flash data was loaded at the end
690 my ( $self, $c ) = @_;
692 $c->flash->{beans} = 10;
693 $c->response->redirect( $c->uri_for("foo") );
697 my ( $self, $c ) = @_;
699 my $value = $c->flash->{beans};
703 $c->response->redirect( $c->uri_for("bar") );
707 my ( $self, $c ) = @_;
709 if ( exists $c->flash->{beans} ) { # false
716 Zap all the keys in the flash regardless of their current state.
718 =item keep_flash @keys
720 If you want to keep a flash key for the next request too, even if it hasn't
721 changed, call C<keep_flash> and pass in the keys as arguments.
723 =item delete_session REASON
725 This method is used to invalidate a session. It takes an optional parameter
726 which will be saved in C<session_delete_reason> if provided.
728 NOTE: This method will B<also> delete your flash data.
730 =item session_delete_reason
732 This accessor contains a string with the reason a session was deleted. Possible
747 =item session_expire_key $key, $ttl
749 Mark a key to expire at a certain time (only useful when shorter than the
750 expiry time for the whole session).
754 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
758 $c->session_expire_key( __user => 3600 );
760 Will make the session data survive, but the user will still be logged out after
763 Note that these values are not auto extended.
765 =item change_session_id
767 By calling this method you can force a session id change while keeping all
768 session data. This method might come handy when you are paranoid about some
769 advanced variations of session fixation attack.
771 If you want to prevent this session fixation scenario:
773 0) let us have WebApp with anonymous and authenticated parts
774 1) a hacker goes to vulnerable WebApp and gets a real sessionid,
775 just by browsing anonymous part of WebApp
776 2) the hacker inserts (somehow) this values into a cookie in victim's browser
777 3) after the victim logs into WebApp the hacker can enter his/her session
779 you should call change_session_id in your login controller like this:
781 if ($c->authenticate( { username => $user, password => $pass } )) {
783 $c->change_session_id;
792 =head1 INTERNAL METHODS
798 This method is extended to also make calls to
799 C<check_session_plugin_requirements> and C<setup_session>.
801 =item check_session_plugin_requirements
803 This method ensures that a State and a Store plugin are also in use by the
808 This method populates C<< $c->config->{session} >> with the default values
809 listed in L</CONFIGURATION>.
813 This method is extended.
815 Its only effect is if the (off by default) C<flash_to_stash> configuration
816 parameter is on - then it will copy the contents of the flash to the stash at
819 =item finalize_headers
821 This method is extended and will extend the expiry time before sending
826 This method is extended and will call finalize_session before the other
827 finalize_body methods run. Here we persist the session data if a session exists.
829 =item initialize_session_data
831 This method will initialize the internal structure of the session, and is
832 called by the C<session> method if appropriate.
834 =item create_session_id
836 Creates a new session ID using C<generate_session_id> if there is no session ID
839 =item validate_session_id SID
841 Make sure a session ID is of the right format.
843 This currently ensures that the session ID string is any amount of case
844 insensitive hexadecimal characters.
846 =item generate_session_id
848 This method will return a string that can be used as a session ID. It is
849 supposed to be a reasonably random string with enough bits to prevent
850 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
851 MD5 or SHA-256, depending on the availability of these modules.
853 =item session_hash_seed
855 This method is actually rather internal to generate_session_id, but should be
856 overridable in case you want to provide more random data.
858 Currently it returns a concatenated string which contains:
864 =item * The current time
866 =item * One value from C<rand>.
868 =item * The stringified value of a newly allocated hash reference
870 =item * The stringified value of the Catalyst context object
874 in the hopes that those combined values are entropic enough for most uses. If
875 this is not the case you can replace C<session_hash_seed> with e.g.
877 sub session_hash_seed {
878 open my $fh, "<", "/dev/random";
879 read $fh, my $bytes, 20;
884 Or even more directly, replace C<generate_session_id>:
886 sub generate_session_id {
887 open my $fh, "<", "/dev/random";
888 read $fh, my $bytes, 20;
890 return unpack("H*", $bytes);
893 Also have a look at L<Crypt::Random> and the various openssl bindings - these
894 modules provide APIs for cryptographically secure random data.
896 =item finalize_session
898 Clean up the session during C<finalize>.
900 This clears the various accessors after saving to the store.
904 See L<Catalyst/dump_these> - ammends the session data structure to the list of
905 dumped objects if session ID is defined.
908 =item calculate_extended_session_expires
910 =item calculate_initial_session_expires
912 =item create_session_id_if_needed
914 =item delete_session_id
916 =item extend_session_expires
918 =item extend_session_id
922 =item reset_session_expires
924 =item session_is_valid
930 =head1 USING SESSIONS DURING PREPARE
932 The earliest point in time at which you may use the session data is after
933 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
935 State plugins must set $c->session ID before C<prepare_action>, and during
936 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
942 # don't touch $c->session yet!
944 $c->NEXT::prepare_action( @_ );
946 $c->session; # this is OK
947 $c->sessionid; # this is also OK
952 $c->config->{session} = {
956 All configuation parameters are provided in a hash reference under the
957 C<session> key in the configuration hash.
963 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
968 When true, C<<$c->request->address>> will be checked at prepare time. If it is
969 not the same as the address that initiated the session, the session is deleted.
973 =item verify_user_agent
975 When true, C<<$c->request->user_agent>> will be checked at prepare time. If it
976 is not the same as the user agent that initiated the session, the session is
983 This option makes it easier to have actions behave the same whether they were
984 forwarded to or redirected to. On prepare time it copies the contents of
985 C<flash> (if any) to the stash.
991 The hash reference returned by C<< $c->session >> contains several keys which
992 are automatically set:
998 This key no longer exists. Use C<session_expires> instead.
1002 The last time a session was saved to the store.
1006 The time when the session was first created.
1010 The value of C<< $c->request->address >> at the time the session was created.
1011 This value is only populated if C<verify_address> is true in the configuration.
1015 The value of C<< $c->request->user_agent >> at the time the session was created.
1016 This value is only populated if C<verify_user_agent> is true in the configuration.
1022 =head2 Round the Robin Proxies
1024 C<verify_address> could make your site inaccessible to users who are behind
1025 load balanced proxies. Some ISPs may give a different IP to each request by the
1026 same client due to this type of proxying. If addresses are verified these
1027 users' sessions cannot persist.
1029 To let these users access your site you can either disable address verification
1030 as a whole, or provide a checkbox in the login dialog that tells the server
1031 that it's OK for the address of the client to change. When the server sees that
1032 this box is checked it should delete the C<__address> special key from the
1033 session hash when the hash is first created.
1035 =head2 Race Conditions
1037 In this day and age where cleaning detergents and Dutch football (not the
1038 American kind) teams roam the plains in great numbers, requests may happen
1039 simultaneously. This means that there is some risk of session data being
1040 overwritten, like this:
1046 request a starts, request b starts, with the same session ID
1050 session data is loaded in request a
1054 session data is loaded in request b
1058 session data is changed in request a
1062 request a finishes, session data is updated and written to store
1066 request b finishes, session data is updated and written to store, overwriting
1067 changes by request a
1071 If this is a concern in your application, a soon-to-be-developed locking
1072 solution is the only safe way to go. This will have a bigger overhead.
1074 For applications where any given user is only making one request at a time this
1075 plugin should be safe enough.
1083 Yuval Kogman, C<nothingmuch@woobling.org>
1087 Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1093 And countless other contributers from #catalyst. Thanks guys!
1095 =head1 COPYRIGHT & LICENSE
1097 Copyright (c) 2005 the aforementioned authors. All rights
1098 reserved. This program is free software; you can redistribute
1099 it and/or modify it under the same terms as Perl itself.