RemoteAuth EZProxy CGI authentication
authorJeff Davis <jeff.davis@bc.libraries.coop>
Thu, 21 Nov 2019 22:43:33 +0000 (14:43 -0800)
committerGalen Charlton <gmc@equinoxinitiative.org>
Tue, 25 Aug 2020 20:45:33 +0000 (16:45 -0400)
This commit adds a RemoteAuth handler (and associated templates,
configuration, and sample data) for EZProxy CGI user authentication:

https://help.oclc.org/Library_Management/EZproxy/Authenticate_users/EZproxy_authentication_methods/CGI_authentication

The user is presented with a login form.  If their account is
authorized, they will be redirected to EZProxy with a valid
authentication ticket, allowing them to access online resources.  If
they are not authorized, an error message is displayed indicating why
the auth attempt failed.  The login form and error messages use
Template Toolkit (TT2) templates and can be customized.

Signed-off-by: Jeff Davis <jeff.davis@bc.libraries.coop>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

Open-ILS/examples/apache_24/eg_startup.in
Open-ILS/examples/apache_24/eg_vhost.conf.in
Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/EZProxyCGI.pm [new file with mode: 0644]
Open-ILS/src/templates/remoteauth/ezproxycgi/error.tt2 [new file with mode: 0644]
Open-ILS/src/templates/remoteauth/ezproxycgi/footer.tt2 [new file with mode: 0644]
Open-ILS/src/templates/remoteauth/ezproxycgi/header.tt2 [new file with mode: 0644]
Open-ILS/src/templates/remoteauth/ezproxycgi/login.tt2 [new file with mode: 0644]
Open-ILS/src/templates/remoteauth/ezproxycgi/login_form.tt2 [new file with mode: 0644]
Open-ILS/tests/datasets/sql/remoteauth.sql

index 89afdfa..b5c679a 100755 (executable)
@@ -14,7 +14,7 @@ use OpenILS::WWW::EGWeb ('@sysconfdir@/opensrf_core.xml', 'OpenILS::WWW::EGCatLo
 use OpenILS::WWW::IDL2js ('@sysconfdir@/opensrf_core.xml');
 use OpenILS::WWW::FlatFielder;
 use OpenILS::WWW::PhoneList ('@sysconfdir@/opensrf_core.xml');
-use OpenILS::WWW::RemoteAuth ('@sysconfdir@/opensrf_core.xml', 'OpenILS::WWW::RemoteAuth::Basic');
+use OpenILS::WWW::RemoteAuth ('@sysconfdir@/opensrf_core.xml', 'OpenILS::WWW::RemoteAuth::Basic', 'OpenILS::WWW::RemoteAuth::EZProxyCGI');
 
 # Pass second argument of '1' to enable template caching.
 use OpenILS::WWW::PrintTemplate ('@sysconfdir@/opensrf_core.xml', 0);
index 4953fc9..b7183fe 100644 (file)
@@ -856,6 +856,39 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT]
     PerlSetVar OILSRemoteAuthClientPassword "demo123"
 </Location>
 
+<Location /api/ezproxy>
+    SetHandler perl-script
+    PerlHandler OpenILS::WWW::RemoteAuth
+    Options +ExecCGI
+
+    # access restricted to localhost by default; since this module provides no
+    # client authentiation, restricting access by IP or other means is stongly
+    # recommended
+    Require local
+
+    # remoteauth profile name
+    PerlSetVar OILSRemoteAuthProfile "EZProxyCGI"
+    # Perl module for processing requests
+    PerlSetVar OILSRemoteAuthHandler "OpenILS::WWW::RemoteAuth::EZProxyCGI"
+
+    # staff username/password for config lookup and patron retrieval
+    PerlSetVar OILSRemoteAuthClientUsername "admin"
+    PerlSetVar OILSRemoteAuthClientPassword "demo123"
+
+    # Location of TT2 templates for EZProxy login form and error pages.
+    # Templates will be loaded from the following paths in reverse order.
+    PerlAddVar OILSRemoteAuthTemplatePath "@localstatedir@/templates/remoteauth/ezproxycgi"
+    #PerlAddVar OILSRemoteAuthTemplatePath "@localstatedir@/templates_localskin/remoteauth/ezproxycgi"
+
+    # Locale (defaults to en_us)
+    #PerlAddVar OILSRemoteAuthLocale "en_us"
+
+    # Base URI of your EZProxy server
+    PerlSetVar OILSRemoteAuthEZProxyBaseURI "http://example.com/ezproxy/"
+
+    # shared secret used to generate EZProxy authentication ticket
+    PerlSetVar OILSRemoteAuthEZProxySecret "secret"
+</Location>
 
 # Uncomment the following to force SSL for everything. Note that this defeats caching
 # and you will suffer a performance hit.
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/EZProxyCGI.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/EZProxyCGI.pm
new file mode 100644 (file)
index 0000000..f18063e
--- /dev/null
@@ -0,0 +1,201 @@
+# Copyright (C) 2019 BC Libraries Cooperative
+#
+# This program 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 2
+# of the License, or (at your option) any later version.
+# 
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+# ====================================================================== 
+# - RemoteAuth handler for EZProxy CGI authentication:
+#   https://help.oclc.org/Library_Management/EZproxy/Authenticate_users/EZproxy_authentication_methods/CGI_authentication
+# ====================================================================== 
+
+package OpenILS::WWW::RemoteAuth::EZProxyCGI;
+use strict; use warnings;
+use OpenILS::WWW::RemoteAuth;
+use base "OpenILS::WWW::RemoteAuth";
+
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN AUTH_REQUIRED HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use CGI qw(:all -utf8);
+use URI::Escape;
+use Digest::MD5 qw/md5_hex/;
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenSRF::Utils::JSON;
+use OpenILS::WWW::RemoteAuth::Template;
+
+sub new {
+    my( $class, $args ) = @_;
+    $args ||= {};
+    $args->{cgi} ||= new CGI;
+    $class = ref $class || $class;
+    return bless($args, $class);
+}
+
+sub r {
+    my ($self, $r) = @_;
+    $self->{r} = $r if $r;
+    return $self->{r};
+}
+
+# CGI handle
+sub cgi {
+    my($self, $cgi) = @_; 
+    $self->{cgi} = $cgi if $cgi;
+    return $self->{cgi};
+}
+
+sub process {
+    my ($self, $r) = @_;
+    my ($authtoken, $editor, $config);
+
+    $self->r($r);
+
+    # get params from incoming request
+    $self->{args} = {
+        id => scalar $self->cgi->param('id'),
+        password => scalar $self->cgi->param('password'),
+        url => scalar $self->cgi->param('url')
+    };
+
+    return $self->login unless (defined $self->{args}->{id} and defined $self->{args}->{password});
+
+    # authorize client
+    try {
+        my $client_user = $r->dir_config('OILSRemoteAuthClientUsername');
+        my $client_pw = $r->dir_config('OILSRemoteAuthClientPassword');
+        $authtoken = $self->do_client_auth($client_user, $client_pw);
+    } catch Error with {
+        $logger->error("RemoteAuth EZProxyCGI failed on client auth: @_");
+        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+    };
+    return $self->client_not_authorized unless $authtoken;
+
+    # load config
+    try {
+        $editor = new_editor( authtoken => $authtoken );
+        $config = $self->load_config($editor, $r);
+    } catch Error with {
+        $logger->error("RemoteAuth EZProxyCGI failed on load config: @_");
+        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+    };
+    return $self->backend_error unless $config;
+
+    # authenticate patron
+    # this uses our util methods (success, patron_not_found, etc) to return the
+    # appropriate response depending on the outcome of the auth request:
+    # - if auth succeeded, redirect to EZProxy
+    # - otherwise, TT2-based error page or login form
+    return $self->do_patron_auth($editor, $config, $self->{args}->{id}, $self->{args}->{password});
+}
+
+sub ezproxy_url {
+    my ($r, $url, $user, $groups) = @_;
+    my ($packet, $ticket);
+
+    my $secret = $r->dir_config('OILSRemoteAuthEZProxySecret');
+    my $base_uri = $r->dir_config('OILSRemoteAuthEZProxyBaseURI');
+
+       return unless defined($secret);
+
+    # generate ticket
+    $packet = '$u' . time();
+    if ($groups) {
+        $packet .= '$g' . $groups;
+    }
+    $packet .= '$e';
+    $ticket = md5_hex($secret . $user . $packet) . $packet;
+
+    # escape our URL params
+    $user = uri_escape($user);
+    $ticket = uri_escape($ticket);
+    $url = uri_escape($url);
+
+    return "$base_uri/login?user=$user&ticket=$ticket&url=$url";
+}
+
+# redirect to EZProxy URL on successful auth
+sub success {
+    my ($self, $user) = @_;
+    my $redirect_url = ezproxy_url($self->r, $self->{args}->{url}, $user->usrname);
+    print $self->cgi->redirect($redirect_url);
+    return Apache2::Const::REDIRECT;
+}
+
+# show login form
+sub login {
+    my $self = shift;
+    my $ctx = {
+        page => 'login',
+        args => $self->{args}
+    };
+    my $tt = new OpenILS::WWW::RemoteAuth::Template;
+    return $tt->process('login', $ctx, $self->r);
+}
+
+# wrapper method for auth failures
+sub error {
+    my ($self, $msg) = @_;
+    my $ctx = {
+        page => 'error',
+        args => $self->{args},
+        error_msg => $msg
+    };
+    my $tt = new OpenILS::WWW::RemoteAuth::Template;
+    return $tt->process('error', $ctx, $self->r);
+}
+
+# generic backend error
+sub backend_error {
+    my $self = shift;
+    return $self->error('backend_error');
+}
+
+# client error (e.g. missing params)
+sub client_error {
+    my $self = shift;
+    return $self->error('client_error');
+}
+
+# client auth failed
+sub client_not_authorized {
+    my $self = shift;
+    return $self->error('client_not_authorized');
+}
+
+# patron auth failed (bad password etc)
+sub patron_not_authenticated {
+    my $self = shift;
+    return $self->error('patron_not_authenticated');
+}
+
+# patron does not exist or is inactive/deleted
+sub patron_not_found {
+    my $self = shift;
+    return $self->error('patron_not_found');
+}
+
+# patron is barred or has blocking penalties
+sub patron_is_blocked {
+    my $self = shift;
+    return $self->error('patron_is_blocked');
+}
+
+# patron is expired
+sub patron_is_expired {
+    my $self = shift;
+    return $self->error('patron_is_expired');
+}
+
+1;
+
diff --git a/Open-ILS/src/templates/remoteauth/ezproxycgi/error.tt2 b/Open-ILS/src/templates/remoteauth/ezproxycgi/error.tt2
new file mode 100644 (file)
index 0000000..d078a08
--- /dev/null
@@ -0,0 +1,18 @@
+[% INCLUDE "header.tt2" %]
+
+[% IF ctx.error_msg %]
+<div id='login-failed-message'>
+    [% IF ctx.error_msg == 'patron_not_found' %]
+    Patron not found.
+    [% ELSIF ctx.error_msg == 'patron_is_blocked' %]
+    Your account is blocked.
+    [% ELSIF ctx.error_msg == 'patron_is_expired' %]
+    Your account is expired.
+    [% ELSE %]
+    Unable to authenticate.
+    [% END %]
+</div>
+[% END %]
+
+[% INCLUDE "login_form.tt2" %]
+[% INCLUDE "footer.tt2" %]
diff --git a/Open-ILS/src/templates/remoteauth/ezproxycgi/footer.tt2 b/Open-ILS/src/templates/remoteauth/ezproxycgi/footer.tt2
new file mode 100644 (file)
index 0000000..2ab5c0d
--- /dev/null
@@ -0,0 +1,2 @@
+    </body>
+</html>
diff --git a/Open-ILS/src/templates/remoteauth/ezproxycgi/header.tt2 b/Open-ILS/src/templates/remoteauth/ezproxycgi/header.tt2
new file mode 100644 (file)
index 0000000..fb4db72
--- /dev/null
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang='[% ctx.locale %]'>
+    <head>
+        <meta charset="utf-8" />
+        
+        <title>
+            [% IF ctx.page == 'error' %]
+                Login Failed
+            [% ELSE %]
+                Login
+            [% END %]
+        </title>
+    </head>
+    <body>
diff --git a/Open-ILS/src/templates/remoteauth/ezproxycgi/login.tt2 b/Open-ILS/src/templates/remoteauth/ezproxycgi/login.tt2
new file mode 100644 (file)
index 0000000..e97f30a
--- /dev/null
@@ -0,0 +1,6 @@
+[% INCLUDE "header.tt2" %]
+
+<h1 class="sr-only">EZProxy Login</h1>
+[% INCLUDE "login_form.tt2" %]
+
+[% INCLUDE "footer.tt2" %]
diff --git a/Open-ILS/src/templates/remoteauth/ezproxycgi/login_form.tt2 b/Open-ILS/src/templates/remoteauth/ezproxycgi/login_form.tt2
new file mode 100644 (file)
index 0000000..82a2739
--- /dev/null
@@ -0,0 +1,23 @@
+<div id='login-form-box' class='login_boxes left_brain float-left'>
+    <h1>Log in to Your Account</h1>
+    Please enter the following information:
+    <form method='post'>
+        <input type="hidden" id="url" name="url" value="[% ctx.args.url | html %]"/>
+        <div class='login-form-left'>
+            <label for='username_field' class="lbl1" >Library Card Number or Username</label>
+            <div class="input_bg">
+                <input type='text' id="id" name="id" autofocus />
+            </div>
+        </div>
+        <div class='float-left'>
+            <label for="password_field" class="lbl1" >PIN Number or Password</label>
+            <div class="input_bg">
+                <input id="password" name="password" type="password" />
+            </div>
+        </div>
+        <div style="clear: both; padding-top: 15px;">
+            <input type="submit" value="Log in" class="opac-button" />
+        </div>
+    </form>
+</div>
+
index f967efb..dff2454 100644 (file)
@@ -9,3 +9,14 @@ INSERT INTO config.remoteauth_profile
     VALUES ('Basic', 'Basic HTTP Authentication for SYS1', 2, TRUE, 1,
         TRUE, FALSE, FALSE, NULL, 1001);
 
+INSERT INTO config.usr_activity_type (id, ewho, ewhat, ehow, egroup, label) VALUES
+ ( 1002, 'ezproxy', 'login', 'apache', 'authen',
+    oils_i18n_gettext(1002, 'RemoteAuth Login: EZProxy CGI Authentication', 'cuat', 'label'));
+
+-- config for EZProxy CGI Authentication (SYS2)
+INSERT INTO config.remoteauth_profile
+    (name, description, context_org, enabled, perm,
+        restrict_to_org, allow_inactive, allow_expired, block_list, usr_activity_type)
+    VALUES ('EZProxyCGI', 'EZProxy CGI Authentication for SYS2', 3, TRUE, 1,
+        TRUE, FALSE, FALSE, NULL, 1002);
+