From a0a5317f78b940441ce38dd70ee13f5a4c94d4da Mon Sep 17 00:00:00 2001 From: David Cook Date: Fri, 26 Sep 2025 07:10:41 +0000 Subject: [PATCH] Bug 34164: Redirect to referring URL when doing OIDC/OAuth SSO This patch makes it so that a successful SSO will return you to the place where you initiated the SSO. This is especially useful when doing searches, placing holds, etc. Test plan: 0. Apply patch and koha-plack --restart kohadev 1. Set up OIDC SSO using https://wiki.koha-community.org/wiki/Testing_SSO 2. Go to http://localhost:8081/cgi-bin/koha/circ/circulation-home.pl 3. Do a staff login using SSO 4. Note that you're redirected to http://localhost:8081/cgi-bin/koha/circ/circulation-home.pl after the successful login 5. Go to http://localhost:8080/cgi-bin/koha/opac-search.pl?idx=&q=test&weight_search=1 6. Do a OPAC SSO login 7. Note that you're returned to http://localhost:8080/cgi-bin/koha/opac-search.pl?idx=&q=test&weight_search=1 after the successful login 8. Logout of the staff admin and OPAC 9. Note your URL ends in ?logout.x=1 10. Try logging back into them using SSO 11a. Note that you're logged in and not automatically logged out 11b. Note that if you're on the OPAC, you should be redirected to opac-user.pl instead of opac-main.pl or the root path 12. If you change the domain for the SSO users to something that your user does not match, try logging in again, and note that the error message should display on mainpage.pl or opac-main.pl/opac-user.pl for the OPAC. (This is because these are the pages that show SSO errors.) --- Koha/Auth/Identity/Referer.pm | 121 +++++++++++++++++ Koha/REST/V1/OAuth/Client.pm | 25 ++++ t/Koha/Auth/Identity/Referer.t | 234 +++++++++++++++++++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 Koha/Auth/Identity/Referer.pm create mode 100755 t/Koha/Auth/Identity/Referer.t diff --git a/Koha/Auth/Identity/Referer.pm b/Koha/Auth/Identity/Referer.pm new file mode 100644 index 0000000000..166136c3a4 --- /dev/null +++ b/Koha/Auth/Identity/Referer.pm @@ -0,0 +1,121 @@ +package Koha::Auth::Identity::Referer; + +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use C4::Context; + +=head1 NAME + +Koha::Auth::Identity::Referer + +=head1 SYNOPSIS + + use Koha::Auth::Identity::Referer; + + Koha::Auth::Identity::Referer->store_referer({ + referer => $c->req->headers->referer, + interface => $interface, + session => $session, + }); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer({ + session => $session, + }); + +=head1 DESCRIPTION + + Class for working with HTTP referrers specifically for + when doing SSO. + +=head1 FUNCTIONS + +=head2 store_referer + + Koha::Auth::Identity::Referer->store_referer({ + referer => $c->req->headers->referer, + interface => $interface, + session => $session, + }); + + If the referer is for a Koha URL, then it gets saved + in a session variable for use after a successful SSO login. + +=cut + +sub store_referer { + my ( $class, $args ) = @_; + my $referer = $args->{referer}; + my $interface = $args->{interface}; + my $session = $args->{session}; + my $valid_referer; + if ( $referer && $interface && $session ) { + my $base_url; + if ( $interface eq 'opac' ) { + $base_url = C4::Context->preference('OPACBaseURL'); + } else { + $base_url = C4::Context->preference('staffClientBaseURL'); + } + if ( $base_url && $referer && $referer =~ /^\Q$base_url\E/ ) { + my $referer_uri = URI->new($referer); + + #NOTE: Remove logout.x query param to prevent logout loops + $referer_uri->query_param_delete('logout.x') if $referer_uri->query_param('logout.x'); + my $referer_to_save = $referer_uri->path_query(); + if ($referer_to_save) { + + #NOTE: Don't bother saving a root referer + #NOTE: Don't save opac-main.pl because it should redirect to opac-user.pl + unless ( ( $referer_to_save eq '/' ) || ( $referer_to_save eq '/cgi-bin/koha/opac-main.pl' ) ) { + $session->param( 'idp_referer', $referer_to_save ); + $session->flush(); + } + } + } + } + return $valid_referer; +} + +=head2 get_referer + + my $target_uri = Koha::Auth::Identity::Referer->get_referer({ + session => $session, + }); + + If a referer was stored in a session variable, then we retrieve + it for use by SSO functionality. That is, after a successful SSO, we'll + redirect to this referer, so we wind up back where we started. + +=cut + +sub get_referer { + my ( $class, $args ) = @_; + my $referer; + my $session = $args->{session}; + if ($session) { + my $idp_referer = $session->param('idp_referer'); + if ($idp_referer) { + $referer = $idp_referer; + $session->clear('idp_referer'); + $session->flush(); + } + } + return $referer; +} + +1; diff --git a/Koha/REST/V1/OAuth/Client.pm b/Koha/REST/V1/OAuth/Client.pm index e724b20f3b..87343a1b15 100644 --- a/Koha/REST/V1/OAuth/Client.pm +++ b/Koha/REST/V1/OAuth/Client.pm @@ -25,6 +25,8 @@ use Scalar::Util qw(blessed); use Try::Tiny; use Koha::Token; use URI::Escape qw(uri_escape_utf8); +use Koha::Session; +use Koha::Auth::Identity::Referer; =head1 NAME @@ -63,6 +65,13 @@ sub login { $uri = '/cgi-bin/koha/mainpage.pl'; } + my $current_session; + my $current_session_cookie = $c->req->cookie('CGISESSID'); + if ($current_session_cookie) { + my $current_session_id = $current_session_cookie->value; + $current_session = Koha::Session->get_session( { sessionID => $current_session_id } ); + } + unless ( $provider_config && $provider_config->{authorize_url} ) { my $error = "No configuration found for your provider"; return $c->redirect_to( $uri . "?auth_error=$error" ); @@ -98,6 +107,14 @@ sub login { # initial request, generate CSRF token $state = Koha::Token->new->generate_csrf( { session_id => $c->req->cookie('CGISESSID')->value } ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => $c->req->headers->referer, + interface => $interface, + session => $current_session, + } + ); } return $c->oauth2->get_token_p( @@ -133,6 +150,14 @@ sub login { $c->cookie( CGISESSID => $session_id, { path => "/" } ); + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $current_session, + } + ); + if ($target_uri) { + $uri = $target_uri; + } $c->redirect_to($uri); } catch { my $error = $_; diff --git a/t/Koha/Auth/Identity/Referer.t b/t/Koha/Auth/Identity/Referer.t new file mode 100755 index 0000000000..3fdc56e8bd --- /dev/null +++ b/t/Koha/Auth/Identity/Referer.t @@ -0,0 +1,234 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::NoWarnings; +use Test::More tests => 10; +use t::lib::Mocks; + +use C4::Auth qw(); + +use_ok('Koha::Auth::Identity::Referer'); + +subtest 'Typical staff success' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'staffClientBaseURL', 'http://mystaffadmin' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://mystaffadmin/cgi-bin/koha/catalogue/search.pl?q=test', + interface => 'staff', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, '/cgi-bin/koha/catalogue/search.pl?q=test', 'Correct staff referer processed' ); + + $session->delete; + $session->flush; +}; + +subtest 'External staff referrers do not get saved' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'staffClientBaseURL', 'http://mystaffadmin' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://externalreferer', + interface => 'staff', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, undef, 'Bad referrers do not get stored' ); + + $session->delete; + $session->flush; +}; + +subtest 'Staff logout referer does not lead to loop' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'staffClientBaseURL', 'http://mystaffadmin' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://mystaffadmin/cgi-bin/koha/mainpage.pl?logout.x=1', + interface => 'staff', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, '/cgi-bin/koha/mainpage.pl', 'Staff logout URL does not lead to loop' ); + + $session->delete; + $session->flush; +}; + +subtest 'Typical OPAC success' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'http://myopac' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://myopac/cgi-bin/koha/opac-detail.pl?biblionumber=999', + interface => 'opac', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, '/cgi-bin/koha/opac-detail.pl?biblionumber=999', 'Correct OPAC referer processed' ); + + $session->delete; + $session->flush; +}; + +subtest 'OPAC logout referer does not lead to loop' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'http://myopac' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://myopac/cgi-bin/koha/opac-main.pl?logout.x=1', + interface => 'opac', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, undef, 'OPAC logout URL does not lead to loop' ); + + $session->delete; + $session->flush; +}; + +subtest 'Do not save referer to OPAC root' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'http://myopac' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://myopac', + interface => 'opac', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, undef, 'Do not save referer to OPAC root' ); + + $session->delete; + $session->flush; +}; + +subtest 'Do not save referer to OPAC root (ie opac-main.pl)' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'http://myopac' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://myopac/cgi-bin/koha/opac-main.pl', + interface => 'opac', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, undef, 'Do not save referer to OPAC root (ie opac-main.pl)' ); + + $session->delete; + $session->flush; +}; + +subtest 'External OPAC referrers do not get saved' => sub { + plan tests => 1; + my $session = C4::Auth::get_session(""); + + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'http://myopac' ); + + Koha::Auth::Identity::Referer->store_referer( + { + referer => 'http://externalreferer', + interface => 'opac', + session => $session, + } + ); + + my $target_uri = Koha::Auth::Identity::Referer->get_referer( + { + session => $session, + } + ); + + is( $target_uri, undef, 'Bad referrers do not get stored' ); + + $session->delete; + $session->flush; +}; -- 2.39.5