LP#1871211: Shibboleth integration support
authorMike Rylander <mrylander@gmail.com>
Fri, 28 Aug 2020 19:38:57 +0000 (15:38 -0400)
committerJane Sandberg <sandbej@linnbenton.edu>
Fri, 26 Feb 2021 17:46:27 +0000 (09:46 -0800)
This commit adds Shibboleth integration to Evergreen for use in the
OPAC.  Using Shibboleth, libraries can authenticate patrons against a
wide variety of 3rd party services, using many different protocols and
standards.

Several settings control if, when and how to make use of the Shibboleth
integration:
 * Enable Shibboleth SSO for the OPAC
  - The main on/off switch.
 * Allow both Shibboleth and native OPAC authentication
  - By default only one or the other will be allowed.  This enables both
    native and Shibboleth login.
 * Log out of the Shibboleth IdP
  - If supported by the IdP configured for use on the other side of
    Shibboleth, this tells Evergreen to tell Shibboleth to log out of
    the IdP on Evergreen logout.
 * Shibboleth SSO Entity ID
  - If multiple IdPs are configured for Shibboleth, and available to a
    particular hostname, this setting defines the one to use for a
    given context org unit.
 * Evergreen SSO matchpoint
  - The Evergreen-side user field to use when looking up the patron
    after successful SSO login.
 * Shibboleth SSO matchpoint
  - The Shibboleth-side field, defined in the attribute map, that
    contains the IdP user identifier value used to look up the Evergreen
    patron.

Two apache sesttings control how Evergreen interacts with Shibboeth:
 * SetEnv sso_loc XXX, which acts in a way analogous to the physical_loc
   environment variable to define the context OU for SSO settings.
 * ShibRequestSetting applicationId XXX, which helps Shibboleth identify
   the correct set of entity ID and attribute mapping configuration.

Additional Shibboleth-focused documentation and examples will be
provided for system administrators.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Christine Burns <christine.burns@bc.libraries.coop>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>

Open-ILS/examples/apache_24/eg_vhost.conf.in
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.shib_sso.sql [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/login/form.tt2
Open-ILS/src/templates/opac/parts/topnav.tt2

index 07c12ac..809d311 100644 (file)
@@ -733,6 +733,10 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT]
     </IfModule>
 </LocationMatch>
 <Location /eg/opac>
+    # Uncomment the entries below to enable Shibboleth authentication
+    #AuthType shibboleth
+    #Require shibboleth
+
     PerlSetVar OILSWebContextLoader "OpenILS::WWW::EGCatLoader"
     # Expire the HTML quickly since we're loading dynamic data for each page
     ExpiresActive On
index 053e4b6..111ea8b 100644 (file)
@@ -13,6 +13,7 @@ use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Fieldmapper;
 use OpenSRF::Utils::Cache;
+use OpenILS::Event;
 use DateTime::Format::ISO8601;
 use CGI qw(:all -utf8);
 use Time::HiRes;
@@ -33,6 +34,8 @@ my $U = 'OpenILS::Application::AppUtils';
 
 use constant COOKIE_SES => 'ses';
 use constant COOKIE_LOGGEDIN => 'eg_loggedin';
+use constant COOKIE_SHIB_LOGGEDOUT => 'eg_shib_logged_out';
+use constant COOKIE_SHIB_LOGGEDIN => 'eg_shib_logged_in';
 use constant COOKIE_TZ => 'client_tz';
 use constant COOKIE_PHYSICAL_LOC => 'eg_physical_loc';
 use constant COOKIE_SSS_EXPAND => 'eg_sss_expand';
@@ -224,6 +227,7 @@ sub load {
         }
     }
 
+    return $self->load_manual_shib_login if $path =~ m|opac/manual_shib_login|;
     # ----------------------------------------------------------------
     #  Everything below here requires authentication
     # ----------------------------------------------------------------
@@ -294,9 +298,17 @@ sub redirect_ssl {
 # -----------------------------------------------------------------------------
 sub redirect_auth {
     my $self = shift;
-    my $login_page = sprintf('%s://%s%s/login',($self->ctx->{is_staff} ? 'oils' : 'https'), $self->ctx->{hostname}, $self->ctx->{opac_root});
+
+    my $sso_org = $ENV{sso_loc} || $self->get_physical_loc || $self->_get_search_lib();
+    my $sso_enabled = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.enable');
+    my $sso_native = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.allow_native');
+
+    my $login_type = ($sso_enabled and !$sso_native) ? 'manual_shib_login' : 'login';
+    my $login_page = sprintf('%s://%s%s/%s',($self->ctx->{is_staff} ? 'oils' : 'https'), $self->ctx->{hostname}, $self->ctx->{opac_root}, $login_type);
     my $redirect_to = uri_escape_utf8($self->apache->unparsed_uri);
-    return $self->generic_redirect("$login_page?redirect_to=$redirect_to");
+    my $redirect_url = "$login_page?redirect_to=$redirect_to";
+
+    return $self->generic_redirect($redirect_url);
 }
 
 # -----------------------------------------------------------------------------
@@ -547,6 +559,13 @@ sub load_login {
 
     $self->timelog("Load login begins");
 
+    my $sso_org = $ENV{sso_loc} || $self->get_physical_loc || $self->_get_search_lib();
+    $ctx->{sso_org} = $sso_org;
+    my $sso_enabled = $ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.enable');
+    my $sso_native = $ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.allow_native');
+    my $sso_eg_match = $ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.evergreen_matchpoint') || 'usrname';
+    my $sso_shib_match = $ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.shib_matchpoint') || 'uid';
+
     $ctx->{page} = 'login';
 
     my $username = $cgi->param('username') || '';
@@ -556,50 +575,84 @@ sub load_login {
     my $persist = $cgi->param('persist');
     my $client_tz = $cgi->param('client_tz');
 
-    # initial log form only
-    return Apache2::Const::OK unless $username and $password;
+    my $sso_user_match_value;
+    my $response;
+    my $sso_logged_in;
+    $self->timelog("SSO is enabled") if ($sso_enabled);
+    if ($sso_enabled
+        and $sso_user_match_value = $ENV{$sso_shib_match}
+        and !$self->cgi->cookie(COOKIE_SHIB_LOGGEDOUT)
+    ) { # we have a shib session, and have not cleared a previous shib-login cookie
+        $self->timelog("Have an SSO user match value: $sso_user_match_value");
+
+        if ($sso_eg_match eq 'barcode') { # barcode is special
+            my $card = $self->editor->search_actor_card({barcode => $sso_user_match_value})->[0];
+            $sso_user_match_value = $card ? $card->usr : undef;
+            $sso_eg_match = 'id';
+        }
 
-    my $auth_proxy_enabled = 0; # default false
-    try { # if the service is not running, just let this fail silently
-        $auth_proxy_enabled = $U->simplereq(
-            'open-ils.auth_proxy',
-            'open-ils.auth_proxy.enabled');
-    } catch Error with {};
+        if ($sso_user_match_value && $sso_eg_match) {
+            my $user = $self->editor->search_actor_user({ $sso_eg_match => $sso_user_match_value })->[0];
+            if ($user) { # create a session
+                $response = $U->simplereq(
+                    'open-ils.auth_internal',
+                    'open-ils.auth_internal.session.create',
+                    { user_id => $user->id, login_type => 'opac' }
+                );
+                $sso_logged_in = $response ? 1 : 0;
+            }
+        }
 
-    $self->timelog("Checked for auth proxy: $auth_proxy_enabled; org = $org_unit; username = $username");
+        $self->timelog("Checked for SSO login");
+    }
 
-    my $args = {
-        type => ($persist) ? 'persist' : 'opac',
-        org => $org_unit,
-        agent => 'opac'
-    };
+    if (!$sso_enabled || (!$response && $sso_native)) {
+        # initial log form only
+        return Apache2::Const::OK unless $username and $password;
 
-    my $bc_regex = $ctx->{get_org_setting}->($org_unit, 'opac.barcode_regex');
+        my $auth_proxy_enabled = 0; # default false
+        try { # if the service is not running, just let this fail silently
+            $auth_proxy_enabled = $U->simplereq(
+                'open-ils.auth_proxy',
+                'open-ils.auth_proxy.enabled');
+        } catch Error with {};
 
-    # To avoid surprises, default to "Barcodes start with digits"
-    $bc_regex = '^\d' unless $bc_regex;
+        $self->timelog("Checked for auth proxy: $auth_proxy_enabled; org = $org_unit; username = $username");
 
-    if ($bc_regex and ($username =~ /$bc_regex/)) {
-        $args->{barcode} = $username;
-    } else {
-        $args->{username} = $username;
-    }
+        my $args = {
+            type => ($persist) ? 'persist' : 'opac',
+            org => $org_unit,
+            agent => 'opac'
+        };
 
-    my $response;
-    if (!$auth_proxy_enabled) {
-        my $seed = $U->simplereq(
-            'open-ils.auth',
-            'open-ils.auth.authenticate.init', $username);
-        $args->{password} = md5_hex($seed . md5_hex($password));
-        $response = $U->simplereq(
-            'open-ils.auth', 'open-ils.auth.authenticate.complete', $args);
+        my $bc_regex = $ctx->{get_org_setting}->($org_unit, 'opac.barcode_regex');
+
+        # To avoid surprises, default to "Barcodes start with digits"
+        $bc_regex = '^\d' unless $bc_regex;
+
+        if ($bc_regex and ($username =~ /$bc_regex/)) {
+            $args->{barcode} = $username;
+        } else {
+            $args->{username} = $username;
+        }
+
+        if (!$auth_proxy_enabled) {
+            my $seed = $U->simplereq(
+                'open-ils.auth',
+                'open-ils.auth.authenticate.init', $username);
+            $args->{password} = md5_hex($seed . md5_hex($password));
+            $response = $U->simplereq(
+                'open-ils.auth', 'open-ils.auth.authenticate.complete', $args);
+        } else {
+            $args->{password} = $password;
+            $response = $U->simplereq(
+                'open-ils.auth_proxy',
+                'open-ils.auth_proxy.login', $args);
+        }
+        $self->timelog("Checked password");
     } else {
-        $args->{password} = $password;
-        $response = $U->simplereq(
-            'open-ils.auth_proxy',
-            'open-ils.auth_proxy.login', $args);
+        $response ||= OpenILS::Event->new( 'LOGIN_FAILED' ); # assume failure
     }
-    $self->timelog("Checked password");
 
     if($U->event_code($response)) { 
         # login failed, report the reason to the template
@@ -647,18 +700,76 @@ sub load_login {
         );
     }
 
+    if ($sso_logged_in) {
+        # tells us if we're logged in via shib, so we can decide whether to try logging in again.
+        push @$cookie_list, $cgi->cookie(
+            -name => COOKIE_SHIB_LOGGEDOUT,
+            -path => '/',
+            -secure => 0,
+            -value => '0',
+            -expires => '-1h'
+        );
+        push @$cookie_list, $cgi->cookie(
+            -name => COOKIE_SHIB_LOGGEDIN,
+            -path => '/',
+            -secure => 0,
+            -value => '1',
+            -expires => $login_cookie_expires
+        );
+    }
+
     return $self->generic_redirect(
         $cgi->param('redirect_to') || $acct,
         $cookie_list
     );
 }
 
+sub load_manual_shib_login {
+    my $self = shift;
+    my $redirect_to = shift || $self->cgi->param('redirect_to');
+
+    my $sso_org = $ENV{sso_loc} || $self->get_physical_loc || $self->_get_search_lib();
+    my $sso_entity_id = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.entityId');
+    my $sso_shib_match = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.shib_matchpoint') || 'uid';
+
+    return $self->load_login if ($ENV{$sso_shib_match});
+
+    my $url = '/Shibboleth.sso/Login?target=' . ($redirect_to || $self->ctx->{home_page});
+    if ($sso_entity_id) {
+        $url .= '&entityID=' . $sso_entity_id;
+    }
+
+    return $self->generic_redirect( $url,
+        [
+            $self->cgi->cookie(
+                -name => COOKIE_SHIB_LOGGEDOUT,
+                -path => '/',
+                -value => '0',
+                -expires => '-1h'
+            )
+        ]
+    );
+}
+
 # -----------------------------------------------------------------------------
 # Log out and redirect to the home page
 # -----------------------------------------------------------------------------
 sub load_logout {
     my $self = shift;
     my $redirect_to = shift || $self->cgi->param('redirect_to');
+    my $active_logout = $self->cgi->param('active_logout');
+
+    my $sso_org = $ENV{sso_loc} || $self->get_physical_loc || $self->_get_search_lib();
+    $self->ctx->{sso_org} = $sso_org;
+    my $sso_enabled = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.enable');
+    my $sso_entity_id = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.entityId');
+    my $sso_logout = $self->ctx->{get_org_setting}->($sso_org, 'opac.login.shib_sso.logout');
+    if ($sso_enabled && $sso_logout) {
+        $redirect_to = '/Shibboleth.sso/Logout?return=' . ($redirect_to || $self->ctx->{home_page});
+        if ($sso_entity_id) {
+            $redirect_to .= '&entityID=' . $sso_entity_id;
+        }
+    }
 
     # If the user was adding anyting to an anonymous cache 
     # while logged in, go ahead and clear it out.
@@ -687,6 +798,18 @@ sub load_logout {
                 -path => '/',
                 -value => '',
                 -expires => '-1h'
+            ),
+            ($active_logout ? ($self->cgi->cookie(
+                -name => COOKIE_SHIB_LOGGEDOUT,
+                -path => '/',
+                -value => '1',
+                -expires => '2147483647'
+            )) : ()),
+            $self->cgi->cookie(
+                -name => COOKIE_SHIB_LOGGEDIN,
+                -path => '/',
+                -value => '0',
+                -expires => '-1h'
             )
         ]
     );
index 4f2948e..3fd6d82 100644 (file)
@@ -1948,7 +1948,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 625, 'VIEW_BOOKING_RESERVATION', oils_i18n_gettext(625,
     'View booking reservations', 'ppl', 'description')),
  ( 626, 'VIEW_BOOKING_RESERVATION_ATTR_MAP', oils_i18n_gettext(626,
-    'View booking reservation attribute maps', 'ppl', 'description'))
+    'View booking reservation attribute maps', 'ppl', 'description')),
+ ( 627, 'SSO_ADMIN', oils_i18n_gettext(627,
+    'Modify patron SSO settings', 'ppl', 'description'))
 ;
 
 
@@ -20282,6 +20284,43 @@ INSERT into config.org_unit_setting_type
         'coust', 'description'),
     'bool', null);
 
+INSERT INTO config.org_unit_setting_type
+( name, grp, label, description, datatype, update_perm )
+VALUES
+('opac.login.shib_sso.enable',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.enable', 'Enable Shibboleth SSO for the OPAC', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.enable', 'Enable Shibboleth SSO for the OPAC', 'coust', 'description'),
+ 'bool', 627),
+('opac.login.shib_sso.entityId',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.entityId', 'Shibboleth SSO Entity ID', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.entityId', 'Which configured Entity ID to use for SSO when there is more than one available to Shibboleth', 'coust', 'description'),
+ 'string', 627),
+('opac.login.shib_sso.logout',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.logout', 'Log out of the Shibboleth IdP', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.logout', 'When logging out of Evergreen, also force a logout of the IdP behind Shibboleth', 'coust', 'description'),
+ 'bool', 627),
+('opac.login.shib_sso.allow_native',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.allow_native', 'Allow both Shibboleth and native OPAC authentication', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.allow_native', 'When Shibboleth SSO is enabled, also allow native Evergreen authentication', 'coust', 'description'),
+ 'bool', 627),
+('opac.login.shib_sso.evergreen_matchpoint',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.evergreen_matchpoint', 'Evergreen SSO matchpoint', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.evergreen_matchpoint',
+  'Evergreen-side field to match a patron against for Shibboleth SSO. Default is usrname.  Other reasonable values would be barcode or email.',
+  'coust', 'description'),
+ 'string', 627),
+('opac.login.shib_sso.shib_matchpoint',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.shib_matchpoint', 'Shibboleth SSO matchpoint', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.shib_matchpoint',
+  'Shibboleth-side field to match a patron against for Shibboleth SSO. Default is uid; use eppn for Active Directory', 'coust', 'description'),
+ 'string', 627)
+;
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.shib_sso.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.shib_sso.sql
new file mode 100644 (file)
index 0000000..4dd5f84
--- /dev/null
@@ -0,0 +1,46 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+-- XXX Check perm number collisions, and adjust update_perm below if necessary!
+INSERT INTO permission.perm_list (id,code,description) VALUES (627,'SSO_ADMIN','Modify patron SSO settings');
+
+INSERT INTO config.org_unit_setting_type
+( name, grp, label, description, datatype, update_perm )
+VALUES
+('opac.login.shib_sso.enable',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.enable', 'Enable Shibboleth SSO for the OPAC', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.enable', 'Enable Shibboleth SSO for the OPAC', 'coust', 'description'),
+ 'bool', 627),
+('opac.login.shib_sso.entityId',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.entityId', 'Shibboleth SSO Entity ID', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.entityId', 'Which configured Entity ID to use for SSO when there is more than one available to Shibboleth', 'coust', 'description'),
+ 'string', 627),
+('opac.login.shib_sso.logout',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.logout', 'Log out of the Shibboleth IdP', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.logout', 'When logging out of Evergreen, also force a logout of the IdP behind Shibboleth', 'coust', 'description'),
+ 'bool', 627),
+('opac.login.shib_sso.allow_native',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.allow_native', 'Allow both Shibboleth and native OPAC authentication', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.allow_native', 'When Shibboleth SSO is enabled, also allow native Evergreen authentication', 'coust', 'description'),
+ 'bool', 627),
+('opac.login.shib_sso.evergreen_matchpoint',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.evergreen_matchpoint', 'Evergreen SSO matchpoint', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.evergreen_matchpoint',
+  'Evergreen-side field to match a patron against for Shibboleth SSO. Default is usrname.  Other reasonable values would be barcode or email.',
+  'coust', 'description'),
+ 'string', 627),
+('opac.login.shib_sso.shib_matchpoint',
+ 'opac',
+ oils_i18n_gettext('opac.login.shib_sso.shib_matchpoint', 'Shibboleth SSO matchpoint', 'coust', 'label'),
+ oils_i18n_gettext('opac.login.shib_sso.shib_matchpoint',
+  'Shibboleth-side field to match a patron against for Shibboleth SSO. Default is uid; use eppn for Active Directory', 'coust', 'description'),
+ 'string', 627)
+;
+
+COMMIT;
index fa391f0..6ceca0b 100644 (file)
 </div>
 [% END %]
 
+[%
+    redirect = CGI.param('redirect_to');
+    # Don't use referer unless we got here from elsewhere within the TPAC
+    IF !redirect AND ctx.referer.match('^https?://' _ ctx.hostname _ ctx.opac_root);
+        redirect = ctx.referer;
+    END;
+    # If no redirect is offered or it's leading us back to the
+    # login form, redirect the user to My Account
+    IF !redirect OR redirect.match(ctx.path_info _ '$');
+        redirect = CGI.url('-full' => 1) _ '/opac/myopac/main';
+    END;
+    redirect = redirect | replace('^http:', 'https:');
+%]
+
+[% sso_enabled = ctx.get_org_setting(ctx.sso_org, 'opac.login.shib_sso.enable');
+   sso_native = ctx.get_org_setting(ctx.sso_org, 'opac.login.shib_sso.allow_native'); 
+%]
+
 <div id='login-form-box' class='login_boxes left_brain float-left'>
     <h1>[% l('Log in to Your Account') %]</h1>
+    [% IF sso_enabled %]
+    [%      final_redirect = redirect | html %]
+    <div id='sso-login-notice'>[%-      l('Please use our ') -%]
+      <a href="[% mkurl(ctx.opac_root _ '/manual_shib_login', { redirect_to => final_redirect }) %]">
+        [% l('Single Sign On service') %]
+      </a>
+    [%-      l('to log into the catalog') -%]
+    [%-      IF sso_native; l(' or use the form below'); END -%]
+    [%-      l('.') -%]</div>
+            <br/><br/>
+    [% END %]
+    [% IF !sso_enabled || sso_native %]
     [% l('Please enter the following information:') %]
     <form method='post'>
         <div class='login-form-left'>
             [% END %]
         </div>
         <div style="clear: both; padding-top: 15px;">
-        [%
-            redirect = CGI.param('redirect_to');
-            # Don't use referer unless we got here from elsewhere within the TPAC
-            IF !redirect AND ctx.referer.match('^https?://' _ ctx.hostname _ ctx.opac_root);
-                redirect = ctx.referer;
-            END;
-            # If no redirect is offered or it's leading us back to the
-            # login form, redirect the user to My Account
-            IF !redirect OR redirect.match(ctx.path_info _ '$');
-                redirect = CGI.url('-full' => 1) _ '/opac/myopac/main';
-            END;
-                redirect = redirect  | replace('^http:', 'https:');
-            %]
             <input type='hidden' name='redirect_to' value='[% redirect | html %]'/>
             <input type="checkbox" name="persist" id="login_persist" /><label for="login_persist"> [% l('Stay logged in?') %]</label>
             <input type="submit" value="[% l('Log in') %]" class="opac-button" />
         </div>
         <input id="client_tz_id" name="client_tz" type="hidden" />
     </form>
+    [% END; # native block %]
 </div>
 [% INCLUDE "opac/parts/login/help.tt2" %]
index f6390b7..4c7c401 100644 (file)
@@ -41,7 +41,7 @@
                     class="opac-button">[% l('My Account') %]</a>
                 <a href="[% mkurl(ctx.opac_root _ '/myopac/lists', {}, ['single', 'message_id', 'hid', 'from_basket']) %]"
                     class="opac-button">[% l('My Lists') %]</a>
-                <a href="[% mkurl(ctx.opac_root _ '/logout', {}, 1) %]"
+                <a href="[% mkurl(ctx.opac_root _ '/logout', {active_logout => 1}, 1) %]"
                     class="opac-button" id="logout_link">[% l('Logout') %]</a>
                 </span>
             </div>