Tie AuthProxy.pm to brute-force prevention setup
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / AuthProxy.pm
1 #!/usr/bin/perl
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 =head1 NAME
18
19 OpenILS::Application::AuthProxy - Negotiator for proxy-style authentication
20
21 =head1 AUTHOR
22
23 Dan Wells, dbw2@calvin.edu
24
25 =cut
26
27 package OpenILS::Application::AuthProxy;
28
29 use strict;
30 use warnings;
31 use OpenILS::Application;
32 use base qw/OpenILS::Application/;
33 use OpenSRF::Utils::Cache;
34 use OpenSRF::Utils::Logger qw(:logger);
35 use OpenSRF::Utils::SettingsClient;
36 use OpenILS::Application::AppUtils;
37 use OpenILS::Utils::Fieldmapper;
38 use OpenILS::Event;
39 use UNIVERSAL::require;
40 use Digest::MD5 qw/md5_hex/;
41 my $U = 'OpenILS::Application::AppUtils';
42
43 # NOTE: code assumes throughout that '0' is never a valid username, barcode,
44 # or password; some logic will need to be tweaked to support it if needed.
45
46 my @authenticators;
47 my %authenticators_by_name;
48 my $enabled = 'false';
49 my $cache = OpenSRF::Utils::Cache->new();
50 my $seed_timeout;
51 my $block_timeout;
52 my $block_count;
53
54 sub initialize {
55     my $conf = OpenSRF::Utils::SettingsClient->new;
56
57     my @pfx = ( "apps", "open-ils.auth", "app_settings", "auth_limits" );
58
59     # read in (or set defaults) for brute force blocking settings
60     $seed_timeout = $conf->config_value( @pfx, "seed_timeout" );
61     $seed_timeout = 30 if (!$seed_timeout or $seed_timeout < 0);
62     $block_timeout = $conf->config_value( @pfx, "seed_timeout" );
63     $block_timeout = $seed_timeout * 3 if (!$block_timeout or $block_timeout < 0);
64     $block_count = $conf->config_value( @pfx, "block_count" );
65     $block_count = 10 if (!$block_count or $block_count < 0);
66
67     @pfx = ( "apps", "open-ils.auth_proxy", "app_settings" );
68
69     $enabled = $conf->config_value( @pfx, 'enabled' );
70
71     my $auth_configs = $conf->config_value( @pfx, 'authenticators', 'authenticator' );
72     $auth_configs = [$auth_configs] if ref($auth_configs) eq 'HASH';
73
74     if ( !@$auth_configs ) {
75         $logger->error("AuthProxy: authenticators list not found!");
76     } else {
77         foreach my $auth_config (@$auth_configs) {
78             my $auth_handler;
79             if ($auth_config->{'name'} eq 'native') {
80                 $auth_handler = 'OpenILS::Application::AuthProxy::Native';
81             } else {
82                 $auth_handler = $auth_config->{module};
83                 next unless $auth_handler;
84
85                 $logger->debug("Attempting to load AuthProxy handler: $auth_handler");
86                 $auth_handler->use;
87                 if($@) {
88                     $logger->error("Unable to load AuthProxy handler [$auth_handler]: $@");
89                     next;
90                 }
91             }
92
93             &_make_option_array($auth_config, 'login_types', 'type');
94             &_make_option_array($auth_config, 'org_units', 'unit');
95
96             my $authenticator = $auth_handler->new($auth_config);
97             push @authenticators, $authenticator;
98             $authenticators_by_name{$authenticator->name} = $authenticator;
99             $logger->debug("Successfully loaded AuthProxy handler: $auth_handler");
100         }
101         $logger->debug("AuthProxy: authenticators loaded");
102     }
103 }
104
105 # helper function to simplify the config structure
106 sub _make_option_array {
107     my ($auth_config, $container_name, $node_name) = @_;
108
109     if (exists $auth_config->{$container_name}
110         and ref $auth_config->{$container_name} eq 'HASH') {
111         my $nodes = $auth_config->{$container_name}{$node_name};
112         if ($nodes) {
113             if (ref $nodes ne 'ARRAY') {
114                 $auth_config->{$container_name} = [$nodes];
115             } else {
116                 $auth_config->{$container_name} = $nodes;
117             }
118         } else {
119             delete $auth_config->{$container_name};
120         }
121     } else {
122         delete $auth_config->{$container_name};
123     }
124 }
125
126
127
128 __PACKAGE__->register_method(
129     method    => "enabled",
130     api_name  => "open-ils.auth_proxy.enabled",
131     api_level => 1,
132     stream    => 1,
133     argc      => 0,
134     signature => {
135         desc => q/Check if AuthProxy is enabled/,
136         return => {
137             desc => "True if enabled, false if not",
138             type => "bool"
139         }
140     }
141 );
142 sub enabled {
143     return (!$enabled or $enabled eq 'false') ? 0 : 1;
144 }
145
146 __PACKAGE__->register_method(
147     method    => "login",
148     api_name  => "open-ils.auth_proxy.login",
149     api_level => 1,
150     stream    => 1,
151     argc      => 1,
152     signature => {
153         desc => q/Basic single-factor login method/,
154         params => [
155             {name=> "args", desc => q/A hash of arguments.  Valid keys and their meanings:
156     username := Username to authenticate.
157     barcode  := Barcode of user to authenticate (currently supported by 'native' only!)
158     password := Password for verifying the user.
159     type     := Type of login being attempted (Staff Client, OPAC, etc.).
160     org      := Org unit id
161 /,
162                 type => "hash"}
163         ],
164         return => {
165             desc => "Authentication seed or failure event",
166             type => "mixed"
167         }
168     }
169 );
170 sub login {
171     my ( $self, $conn, $args ) = @_;
172
173     return OpenILS::Event->new( 'LOGIN_FAILED' )
174       unless (&enabled() and ($args->{'username'} or $args->{'barcode'}));
175
176     # check for possibility of brute-force attack
177     my $fail_count;
178     # since barcode logins are for 'native' only, we will rely on the blocking
179     # code built-in to 'native' for those logins
180     if ($args->{'username'}) {
181         $fail_count = $cache->get_cache('oils_auth_' . $args->{'username'} . '_count') || 0;
182         if ($fail_count >= $block_count) {
183             $logger->debug("AuthProxy found too many recent failures for '" . $args->{'username'} . "' : $fail_count, forcing failure state.");
184             $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
185             return OpenILS::Event->new( 'LOGIN_FAILED' );
186         }
187     }
188
189     my @error_events;
190     my $authenticated = 0;
191     my $auths;
192
193     # if they specify an authenticator by name, only try that one
194     if ($args->{'name'}) {
195         $auths = [$authenticators_by_name{$args->{'name'}}];
196     } else {
197         $auths = \@authenticators;
198     }
199
200     foreach my $authenticator (@$auths) {
201         # skip authenticators specified for a different login type
202         # or org unit id
203         if ($authenticator->login_types and $args->{'type'}) {
204             next unless grep(/^(all|$args->{'type'})$/, @{$authenticator->{'login_types'}});
205         }
206         if ($authenticator->org_units and $args->{'org'}) {
207             next unless grep(/^(all|$args->{'org'})$/, @{$authenticator->{'org_units'}});
208         }
209
210         my $event;
211         # treat native specially
212         if ($authenticator->name eq 'native') {
213             $event = &_do_login($args);
214         } else {
215             $event = $authenticator->authenticate($args);
216         }
217         my $code = $U->event_code($event);
218         if ($code) {
219             push @error_events, $event;
220         } elsif (defined $code) { # code is '0', i.e. SUCCESS
221             if (exists $event->{'payload'}) { # we have a complete native login
222                 return $event;
223             } else { # do a 'forced' login
224                 return &_do_login($args, 1);
225             }
226         }
227     }
228
229     # if we got this far, we failed
230     # increment the brute force counter if 'native' didn't already
231     if ($args->{'username'} and !exists $authenticators_by_name{'native'}) {
232         $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
233     }
234     # TODO: send back some form of collected error events
235     return OpenILS::Event->new( 'LOGIN_FAILED' );
236 }
237
238 sub _do_login {
239     my $args = shift;
240     my $authenticated = shift;
241
242     my $seeder = $args->{'username'} ? $args->{'username'} : $args->{'barcode'};
243     my $seed =
244       OpenSRF::AppSession->create("open-ils.auth")
245       ->request( 'open-ils.auth.authenticate.init', $seeder )->gather(1);
246
247     return OpenILS::Event->new( 'LOGIN_FAILED' )
248       unless $seed;
249
250     my $real_password = $args->{'password'};
251     # if we have already authenticated, look up the password needed to finish
252     if ($authenticated) {
253         # barcode-based login is supported only for 'native' logins
254         return OpenILS::Event->new( 'LOGIN_FAILED' ) if !$args->{'username'};
255         my $user = $U->cstorereq(
256             "open-ils.cstore.direct.actor.user.search.atomic",
257             { usrname => $args->{'username'} }
258         );
259         $args->{'password'} = md5_hex( $seed . $user->[0]->passwd );
260     } else {
261         $args->{'password'} = md5_hex( $seed . md5_hex($real_password) );
262     }
263     my $response = OpenSRF::AppSession->create("open-ils.auth")->request(
264         'open-ils.auth.authenticate.complete',
265         $args
266     )->gather(1);
267     $args->{'password'} = $real_password;
268
269     return OpenILS::Event->new( 'LOGIN_FAILED' )
270       unless $response;
271
272     return $response;
273 }
274
275 __PACKAGE__->register_method(
276     method    => "authenticators",
277     api_name  => "open-ils.auth_proxy.authenticators",
278     api_level => 1,
279     stream    => 1,
280     argc      => 1,
281     signature => {
282         desc => q/Get a list of viable authenticators/,
283         params => [
284             {name=> "args", desc => q/A hash of arguments.  Valid keys and their meanings:
285     type     := Type of login being attempted (Staff Client, OPAC, etc.).
286     org      := Org unit id
287 /,
288                 type => "hash"}
289         ],
290         return => {
291             desc => "List of viable authenticators",
292             type => "array"
293         }
294     }
295 );
296 sub authenticators {
297     my ( $self, $conn, $args ) = @_;
298
299     my @viable_auths;
300
301     foreach my $authenticator (@authenticators) {
302         # skip authenticators specified for a different login type
303         # or org unit id
304         if ($authenticator->login_types and $args->{'type'}) {
305             next unless grep(/^(all|$args->{'type'})$/, @{$authenticator->login_types});
306         }
307         if ($authenticator->org_units and $args->{'org'}) {
308             next unless grep(/^(all|$args->{'org'})$/, @{$authenticator->org_units});
309         }
310
311         push @viable_auths, $authenticator->name;
312     }
313
314     return \@viable_auths;
315 }
316
317
318 # --------------------------------------------------------------------------
319 # Stub package for 'native' authenticator
320 # --------------------------------------------------------------------------
321 package OpenILS::Application::AuthProxy::Native;
322 use strict; use warnings;
323 use base 'OpenILS::Application::AuthProxy::AuthBase';
324
325 1;