package Session; use Modern::Perl; use signatures; require Mojo::Cookie::Response; require JSON; require Digest::SHA; require Clone; require Test::Deep; require Compress::Zlib; my $SESSION_NAME = 'session'; my $SESSION_HMAC_NAME = 'session_hmac'; my $SESSION_HMAC_KEY = '091u23npu0298u1emo2i12s'; # session hashes our %session; # what other modules use our %received_session; # what we use to find out if anything changed during the request by deep-comparison with %session # deserializes session data from the cookie, doing an integrity and authenticity check sub load_session( $context ) { %session = (); %received_session = (); # if we didn't receive any cookies, leave the session empty my $cookie_ref = $context->req->cookies; return if not $cookie_ref; # extract the session and HMAC cookies my %cookies = map { $_->name => $_->value } @$cookie_ref; my $encoded_session = $cookies{ $SESSION_NAME }; my $expected_session_hmac = $cookies{ $SESSION_HMAC_NAME }; return if not $encoded_session; # decode the session cookie and take an HMAC of it my $compressed_session = MIME::Base64::decode( $encoded_session ); my $serialized_session = Compress::Zlib::memGunzip( $compressed_session ); my $actual_session_hmac = Digest::SHA::hmac_sha256_base64( $serialized_session, $SESSION_HMAC_KEY ); # check the session cookie wasn't manipulated by the client return if $actual_session_hmac ne $expected_session_hmac; # populate our session %session = %{ JSON::decode_json( $serialized_session ) }; %received_session = %{ Clone::clone( \%session ) }; } # serializes session data to cookie, adding an HMAC sub save_session( $context ) { # we only send back a new session cookie if any session variables actually changed during this request return if Test::Deep::eq_deeply( \%session, \%received_session ); # serialize, compress and BASE64 encode our session my $serialized_session = JSON::encode_json( \%session ); my $compressed_session = Compress::Zlib::memGzip( $serialized_session ); my $encoded_session = MIME::Base64::encode( $compressed_session, '' ); # we do an HMAC of the non-compressed/encoded version to leave less leeway for envelope manipulation my $session_hmac = Digest::SHA::hmac_sha256_base64( $serialized_session, $SESSION_HMAC_KEY ); # create the session and HMAC cookies my $encoded_session_cookie = Mojo::Cookie::Response->new( name => $SESSION_NAME, value => $encoded_session, path => '/' ); my $session_hmac_cookie = Mojo::Cookie::Response->new( name => $SESSION_HMAC_NAME, value => $session_hmac, path => '/' ); # warning: this replaces -all- existing cookies $context->res->cookies( ( $encoded_session_cookie, $session_hmac_cookie ) ); } 1;