From ea71778316a4aecce59f678f7f0657dacb22a35b Mon Sep 17 00:00:00 2001 From: Chris Cormack Date: Thu, 8 Jun 2017 09:38:30 +1200 Subject: [PATCH] Bug 19160 CAS Single logout 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 Patch works as described, local login still works correctly. Signed-off-by: Kyle M Hall --- C4/Auth.pm | 64 +++++++++++++++++++++++++++++++++++++++++++---------- C4/Auth_with_cas.pm | 12 ++++++---- opac/opac-user.pl | 22 ++++++++++++++++++ 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/C4/Auth.pm b/C4/Auth.pm index 67bdd7e..a09f6b8 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -746,7 +746,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; @@ -766,7 +765,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'); @@ -904,8 +903,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()"; @@ -936,7 +972,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 @@ -954,7 +989,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); @@ -1014,7 +1049,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); @@ -1140,6 +1175,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'), @@ -1371,9 +1407,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(); @@ -1467,7 +1503,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') ) { @@ -1476,7 +1512,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 @@ -1485,7 +1521,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 ) ) { @@ -1583,6 +1620,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'), @@ -1789,9 +1827,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; } diff --git a/C4/Auth_with_cas.pm b/C4/Auth_with_cas.pm index 6e7bb93..ba683cd 100644 --- a/C4/Auth_with_cas.pm +++ b/C4/Auth_with_cas.pm @@ -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 diff --git a/opac/opac-user.pl b/opac/opac-user.pl index 55c04a3..2f4b238 100755 --- a/opac/opac-user.pl +++ b/opac/opac-user.pl @@ -60,6 +60,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( { @@ -71,6 +87,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; -- 2.10.2