LP#1817645: RemoteAuth handler for basic HTTP authentication (RFC 7617)
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / RemoteAuth / Basic.pm
1 # Copyright (C) 2019 BC Libraries Cooperative
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16
17 # ====================================================================== 
18 # - RemoteAuth handler for HTTP basic access authorization (RFC 7617)
19 # - patron credentials are Bas64-encoded in Authorization header
20 # - no client authorization - restricting access by IP or other methods
21 #   is strongly recommended!
22 # ====================================================================== 
23
24 package OpenILS::WWW::RemoteAuth::Basic;
25 use strict; use warnings;
26 use OpenILS::WWW::RemoteAuth;
27 use base "OpenILS::WWW::RemoteAuth";
28
29 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN AUTH_REQUIRED HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
30 use MIME::Base64;
31 use OpenSRF::EX qw(:try);
32 use OpenSRF::Utils::Logger qw/$logger/;
33 use OpenILS::Utils::CStoreEditor qw/:funcs/;
34 use OpenSRF::Utils::JSON;
35
36 sub new {
37     my( $class, $args ) = @_;
38     $args ||= {};
39     $class = ref $class || $class;
40     return bless($args, $class);
41 }
42
43 # here's our main method; it controls the various steps of the auth flow,
44 # prepares the response content, and returns an HTTP status code
45 sub process {
46     my ($self, $r) = @_;
47     my ($authtoken, $editor, $config);
48
49     # authorize client
50     try {
51         my $client_user = $r->dir_config('OILSRemoteAuthClientUsername');
52         my $client_pw = $r->dir_config('OILSRemoteAuthClientPassword');
53         $authtoken = $self->do_client_auth($client_user, $client_pw);
54     } catch Error with {
55         $logger->error("RemoteAuth Basic failed on client auth: @_");
56         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
57     };
58     return $self->client_not_authorized unless $authtoken;
59
60     # load config
61     try {
62         $editor = new_editor( authtoken => $authtoken );
63         $config = $self->load_config($editor, $r);
64     } catch Error with {
65         $logger->error("RemoteAuth Basic failed on load config: @_");
66         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
67     };
68     return $self->backend_error unless $config;
69
70     # extract patron id/password from Authorization request header
71     my $auth_header = $r->headers_in->get('Authorization');
72     unless (defined $auth_header && $auth_header =~ /^Basic /) {
73         # include WWW-Authenticate header on 401 responses, per RFC 7617
74         my $name = $config->name;
75         $r->err_headers_out->add('WWW-Authenticate' => "Basic realm=\"$name\"");
76         return Apache2::Const::AUTH_REQUIRED;
77     }
78     $auth_header =~ s/^Basic //;
79     my ($id, $password) = split(/:/, decode_base64($auth_header), 2);
80
81     # authenticate patron
82     my $stat = $self->do_patron_auth($editor, $config, $id, $password);
83     return $stat unless $stat == Apache2::Const::OK;
84
85     # XXX RFC 7617 doesn't require any particular content in the body of the
86     # response.  The response content could be made configurable, but for now,
87     # let's respond with a simple JSON message containing the username/barcode
88     # used to authenticate the user: it's a predictable response, it doesn't
89     # require us to retrieve any additional patron information, and it's
90     # compatible with the Apereo CAS server's requirements for remote REST
91     # authentication, as documented here:
92     # https://apereo.github.io/cas/5.0.x/installation/Rest-Authentication.html
93
94     my $response_content = { id => $id };
95     $r->content_type('application/json');
96     $r->print( OpenSRF::Utils::JSON->perl2JSON($response_content) );
97     return Apache2::Const::OK;
98
99 }
100
101 # ... and here are all our util methods:
102
103 # success
104 sub success {
105     return Apache2::Const::OK;
106 }
107
108 # generic backend error
109 sub backend_error {
110     return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
111 }
112
113 # client error (e.g. missing params)
114 sub client_error {
115     return Apache2::Const::HTTP_BAD_REQUEST;
116 }
117
118 # client auth failed
119 sub client_not_authorized {
120     return Apache2::Const::AUTH_REQUIRED;
121 }
122
123 # patron auth failed (bad password etc)
124 sub patron_not_authenticated {
125     return Apache2::Const::FORBIDDEN;
126 }
127
128 # patron does not exist or is inactive/deleted
129 sub patron_not_found {
130     return Apache2::Const::FORBIDDEN;
131 }
132
133 # patron is barred or has blocking penalties
134 sub patron_is_blocked {
135     return Apache2::Const::FORBIDDEN;
136 }
137
138 # patron is expired
139 sub patron_is_expired {
140     return Apache2::Const::FORBIDDEN;
141 }
142
143 1;
144