Bug 19160: CAS Single logout
authorChris Cormack <chrisc@catalyst.net.nz>
Wed, 7 Jun 2017 21:38:30 +0000 (09:38 +1200)
committerJonathan Druart <jonathan.druart@bugs.koha-community.org>
Fri, 23 Mar 2018 14:45:37 +0000 (11:45 -0300)
CAS supports single logout, where if you logout of one application it
logs you out of all of them.

This bug implements this

You will need a CAS server (with single logout configure),
and at least 2 applications (one being Koha)

1/ In Koha login via CAS
2/ Login to the other application via CAS
3/ Logout of the other application
4/ Notice you are still logged into Koha
5/ Log out of Koha
6/ Apply patch
7/ Login to Koha via CAS, login to other app via CAS
8/ Log out of other app
9/ Notice you are logged out of Koha

If you dont have CAS, this patch should be a no op, you could test that
1/ Login and logout normally
2/ Apply patch
3/ Login and logout still work fine

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Patch works as described, local login still works correctly.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

C4/Auth.pm
C4/Auth_with_cas.pm
opac/opac-user.pl

index c3ad351..5df6f2e 100644 (file)
@@ -748,7 +748,6 @@ sub _timeout_syspref {
 sub checkauth {
     my $query = shift;
     $debug and warn "Checking Auth";
-
     # $authnotrequired will be set for scripts which will run without authentication
     my $authnotrequired = shift;
     my $flagsrequired   = shift;
@@ -768,7 +767,7 @@ sub checkauth {
     my $logout = $query->param('logout.x');
 
     my $anon_search_history;
-
+    my $cas_ticket = '';
     # This parameter is the name of the CAS server we want to authenticate against,
     # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
     my $casparam = $query->param('cas');
@@ -906,8 +905,45 @@ sub checkauth {
             }
         }
     }
+    elsif ($logout && $cas) {
+        # We got a cas single logout request from a cas server;
+        my $ticket = $query->param('cas_ticket');
+        # We've been called as part of the single logout destroy the session associated with the cas ticket
+        my $storage_method = C4::Context->preference('SessionStorage');
+        my $dsn;
+        my $dsn_options;
+        # shift this to a function make get_session use the function too
+        my $dbh            = C4::Context->dbh;
+        if ( $storage_method eq 'mysql' ) {
+            $dsn = "driver:MySQL;serializer:yaml;id:md5";
+            $dsn_options = { Handle => $dbh };
+        }
+        elsif (  $storage_method eq 'Pg' ) {
+            $dsn = "driver:PostgreSQL;serializer:yaml;id:md5";
+            $dsn_options = { Handle => $dbh };
+        }
+        elsif ( $storage_method eq 'memcached' && Koha::Caches->get_instance->memcached_cache ) {
+            $dsn = "driver:memcached;serializer:yaml;id:md5";
+            my $memcached = Koha::Caches->get_instance()->memcached_cache;
+            $dsn_options =  { Memcached => $memcached };
+        }
+        else {
+            $dsn = "driver:File;serializer:yaml;id:md5";
+            my $dir = File::Spec->tmpdir;
+            my $instance = C4::Context->config( 'database' ); #actually for packages not exactly the instance name, but generally safer to leave it as it is
+            $dsn_options =  { Directory => "$dir/cgisess_$instance" };
+        }
+        my $success =  CGI::Session->find( $dsn, sub {delete_cas_session(@_, $ticket)}, $dsn_options );
+        sub delete_cas_session {
+            my $session = shift;
+            my $ticket = shift;
+            if ($session->param('cas_ticket') && $session->param('cas_ticket') eq $ticket ) {
+                $session->delete;
+                $session->flush;
+            }
+        }
+    }
     unless ( $userid || $sessionID ) {
-
         #we initiate a session prior to checking for a username to allow for anonymous sessions...
         my $session = get_session("") or die "Auth ERROR: Cannot get_session()";
 
@@ -938,7 +974,6 @@ sub checkauth {
         {
             my $password    = $query->param('password');
             my $shibSuccess = 0;
-
             my ( $return, $cardnumber );
 
             # If shib is enabled and we have a shib login, does the login match a valid koha user
@@ -956,7 +991,7 @@ sub checkauth {
             unless ($shibSuccess) {
                 if ( $cas && $query->param('ticket') ) {
                     my $retuserid;
-                    ( $return, $cardnumber, $retuserid ) =
+                    ( $return, $cardnumber, $retuserid, $cas_ticket ) =
                       checkpw( $dbh, $userid, $password, $query, $type );
                     $userid = $retuserid;
                     $info{'invalidCasLogin'} = 1 unless ($return);
@@ -1016,7 +1051,7 @@ sub checkauth {
                 }
                 else {
                     my $retuserid;
-                    ( $return, $cardnumber, $retuserid ) =
+                    ( $return, $cardnumber, $retuserid, $cas_ticket ) =
                       checkpw( $dbh, $q_userid, $password, $query, $type );
                     $userid = $retuserid if ($retuserid);
                     $info{'invalid_username_or_password'} = 1 unless ($return);
@@ -1142,6 +1177,7 @@ sub checkauth {
                     $session->param( 'ip',           $session->remote_addr() );
                     $session->param( 'lasttime',     time() );
                 }
+                $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
                 C4::Context->set_userenv(
                     $session->param('number'),       $session->param('id'),
                     $session->param('cardnumber'),   $session->param('firstname'),
@@ -1374,9 +1410,9 @@ Possible return values in C<$status> are:
 =cut
 
 sub check_api_auth {
+
     my $query         = shift;
     my $flagsrequired = shift;
-
     my $dbh     = C4::Context->dbh;
     my $timeout = _timeout_syspref();
 
@@ -1470,7 +1506,7 @@ sub check_api_auth {
         # new login
         my $userid   = $query->param('userid');
         my $password = $query->param('password');
-        my ( $return, $cardnumber );
+        my ( $return, $cardnumber, $cas_ticket );
 
         # Proxy CAS auth
         if ( $cas && $query->param('PT') ) {
@@ -1479,7 +1515,7 @@ sub check_api_auth {
 
             # In case of a CAS authentication, we use the ticket instead of the password
             my $PT = $query->param('PT');
-            ( $return, $cardnumber, $userid ) = check_api_auth_cas( $dbh, $PT, $query );    # EXTERNAL AUTH
+            ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $dbh, $PT, $query );    # EXTERNAL AUTH
         } else {
 
             # User / password auth
@@ -1488,7 +1524,8 @@ sub check_api_auth {
                 # caller did something wrong, fail the authenticateion
                 return ( "failed", undef, undef );
             }
-            ( $return, $cardnumber ) = checkpw( $dbh, $userid, $password, $query );
+            my $newuserid;
+            ( $return, $cardnumber, $newuserid, $cas_ticket ) = checkpw( $dbh, $userid, $password, $query );
         }
 
         if ( $return and haspermission( $userid, $flagsrequired ) ) {
@@ -1586,6 +1623,7 @@ sub check_api_auth {
                 $session->param( 'ip',           $session->remote_addr() );
                 $session->param( 'lasttime',     time() );
             }
+            $session->param( 'cas_ticket', $cas_ticket);
             C4::Context->set_userenv(
                 $session->param('number'),       $session->param('id'),
                 $session->param('cardnumber'),   $session->param('firstname'),
@@ -1792,9 +1830,11 @@ sub checkpw {
         # In case of a CAS authentication, we use the ticket instead of the password
         my $ticket = $query->param('ticket');
         $query->delete('ticket');                                   # remove ticket to come back to original URL
-        my ( $retval, $retcard, $retuserid ) = checkpw_cas( $dbh, $ticket, $query, $type );    # EXTERNAL AUTH
+        my ( $retval, $retcard, $retuserid, $cas_ticket ) = checkpw_cas( $dbh, $ticket, $query, $type );    # EXTERNAL AUTH
         if ( $retval ) {
-            @return = ( $retval, $retcard, $retuserid );
+            @return = ( $retval, $retcard, $retuserid, $cas_ticket );
+        } else {
+            @return = (0);
         }
         $passwd_ok = $retval;
     }
index 6e7bb93..ba683cd 100644 (file)
@@ -105,18 +105,20 @@ sub checkpw_cas {
             my $userid = $val->user();
             $debug and warn "User CAS authenticated as: $userid";
 
+            # we should store the CAS ticekt too, we need this for single logout https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#233-single-logout
+
             # Does it match one of our users ?
             my $sth = $dbh->prepare("select cardnumber from borrowers where userid=?");
             $sth->execute($userid);
             if ( $sth->rows ) {
                 $retnumber = $sth->fetchrow;
-                return ( 1, $retnumber, $userid );
+                return ( 1, $retnumber, $userid, $ticket );
             }
             $sth = $dbh->prepare("select userid from borrowers where cardnumber=?");
             $sth->execute($userid);
             if ( $sth->rows ) {
                 $retnumber = $sth->fetchrow;
-                return ( 1, $retnumber, $userid );
+                return ( 1, $retnumber, $userid, $ticket );
             }
 
             # If we reach this point, then the user is a valid CAS user, but not a Koha user
@@ -154,19 +156,21 @@ sub check_api_auth_cas {
 
             my $userid = $r->user;
 
+            # we should store the CAS ticket too, we need this for single logout https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#233-single-logout
+
             # Does it match one of our users ?
             my $sth = $dbh->prepare("select cardnumber from borrowers where userid=?");
             $sth->execute($userid);
             if ( $sth->rows ) {
                 $retnumber = $sth->fetchrow;
-                return ( 1, $retnumber, $userid );
+                return ( 1, $retnumber, $userid, $PT );
             }
             $sth = $dbh->prepare("select userid from borrowers where cardnumber=?");
             return $r->user;
             $sth->execute($userid);
             if ( $sth->rows ) {
                 $retnumber = $sth->fetchrow;
-                return ( 1, $retnumber, $userid );
+                return ( 1, $retnumber, $userid, $PT );
             }
 
             # If we reach this point, then the user is a valid CAS user, but not a Koha user
index ac91d4f..c5d6ab7 100755 (executable)
@@ -61,6 +61,22 @@ BEGIN {
         import C4::External::BakerTaylor qw(&image_url &link_url);
     }
 }
+my $logout='';
+# CAS Single Sign Out
+if (C4::Context->preference('casAuthentication')){
+    # Check we havent been hit by a logout call
+    my $xml = $query->param('logoutRequest');
+    if ($xml) {
+        my $dom = XML::LibXML->load_xml(string => $xml);
+        my $ticket;
+        foreach my $node ($dom->findnodes('/samlp:LogoutRequest')){
+            $ticket = $node->findvalue('./samlp:SessionIndex');
+        }
+        $query->param(-name =>'logout.x', -value => 1);
+        $query->param(-name =>'cas_ticket', -value => $ticket);
+        $logout=1;
+    }
+}
 
 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
     {
@@ -72,6 +88,12 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
     }
 );
 
+if ($logout){
+    print $query->header;
+    exit;
+}
+
+
 my %renewed = map { $_ => 1 } split( ':', $query->param('renewed') );
 
 my $show_priority;