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.
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.
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.
19 OpenILS::Application::AuthProxy - Negotiator for proxy-style authentication
23 Dan Wells, dbw2@calvin.edu
27 package OpenILS::Application::AuthProxy;
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::Utils::CStoreEditor qw/:funcs/;
40 use UNIVERSAL::require;
41 use Digest::MD5 qw/md5_hex/;
42 my $U = 'OpenILS::Application::AppUtils';
44 # NOTE: code assumes throughout that '0' is never a valid username, barcode,
45 # or password; some logic will need to be tweaked to support it if needed.
48 my %authenticators_by_name;
49 my $enabled = 'false';
56 my $conf = OpenSRF::Utils::SettingsClient->new;
57 $cache = OpenSRF::Utils::Cache->new();
59 my @pfx = ( "apps", "open-ils.auth", "app_settings", "auth_limits" );
61 # read in (or set defaults) for brute force blocking settings
62 $seed_timeout = $conf->config_value( @pfx, "seed" );
63 $seed_timeout = 30 if (!$seed_timeout or $seed_timeout < 0);
64 $block_timeout = $conf->config_value( @pfx, "block_time" );
65 $block_timeout = $seed_timeout * 3 if (!$block_timeout or $block_timeout < 0);
66 $block_count = $conf->config_value( @pfx, "block_count" );
67 $block_count = 10 if (!$block_count or $block_count < 0);
69 @pfx = ( "apps", "open-ils.auth_proxy", "app_settings" );
71 $enabled = $conf->config_value( @pfx, 'enabled' );
73 my $auth_configs = $conf->config_value( @pfx, 'authenticators', 'authenticator' );
74 $auth_configs = [$auth_configs] if ref($auth_configs) eq 'HASH';
76 if ( !@$auth_configs ) {
77 $logger->error("AuthProxy: authenticators list not found!");
79 foreach my $auth_config (@$auth_configs) {
81 if ($auth_config->{'name'} eq 'native') {
82 $auth_handler = 'OpenILS::Application::AuthProxy::Native';
84 $auth_handler = $auth_config->{module};
85 next unless $auth_handler;
87 $logger->debug("Attempting to load AuthProxy handler: $auth_handler");
90 $logger->error("Unable to load AuthProxy handler [$auth_handler]: $@");
95 &_make_option_array($auth_config, 'login_types', 'type');
96 &_make_option_array($auth_config, 'org_units', 'unit');
98 my $authenticator = $auth_handler->new($auth_config);
99 push @authenticators, $authenticator;
100 $authenticators_by_name{$authenticator->name} = $authenticator;
101 $logger->debug("Successfully loaded AuthProxy handler: $auth_handler");
103 $logger->debug("AuthProxy: authenticators loaded");
107 # helper function to simplify the config structure
108 sub _make_option_array {
109 my ($auth_config, $container_name, $node_name) = @_;
111 if (exists $auth_config->{$container_name}
112 and ref $auth_config->{$container_name} eq 'HASH') {
113 my $nodes = $auth_config->{$container_name}{$node_name};
115 if (ref $nodes ne 'ARRAY') {
116 $auth_config->{$container_name} = [$nodes];
118 $auth_config->{$container_name} = $nodes;
121 delete $auth_config->{$container_name};
124 delete $auth_config->{$container_name};
130 __PACKAGE__->register_method(
132 api_name => "open-ils.auth_proxy.enabled",
137 desc => q/Check if AuthProxy is enabled/,
139 desc => "True if enabled, false if not",
145 return (!$enabled or $enabled eq 'false') ? 0 : 1;
148 __PACKAGE__->register_method(
150 api_name => "open-ils.auth_proxy.login",
155 desc => q/Basic single-factor login method/,
157 {name=> "args", desc => q/A hash of arguments. Valid keys and their meanings:
158 username := Username to authenticate.
159 barcode := Barcode of user to authenticate
160 password := Password for verifying the user.
161 type := Type of login being attempted (Staff Client, OPAC, etc.).
167 desc => "Authentication seed or failure event",
173 my ( $self, $conn, $args ) = @_;
176 return OpenILS::Event->new( 'LOGIN_FAILED' )
177 unless (&enabled() and ($args->{'username'} or $args->{'barcode'}));
179 # provided username may not be the user's actual EG username;
180 # hang onto the provided value (if any) so we can use it later
181 $args->{'provided_username'} = $args->{'username'};
183 if ($args->{barcode} and !$args->{username}) {
184 # translate barcode logins into username logins by locating
185 # the matching card/user and collecting the username.
187 my $card = new_editor()->search_actor_card([
188 {barcode => $args->{barcode}, active => 't'},
189 {flesh => 1, flesh_fields => {ac => ['usr']}}
193 $args->{username} = $card->usr->usrname;
194 } else { # must have or resolve to a username
195 return OpenILS::Event->new( 'LOGIN_FAILED' );
199 # check for possibility of brute-force attack
200 my $fail_count = $cache->get_cache('oils_auth_' . $args->{'username'} . '_count') || 0;
201 if ($fail_count >= $block_count) {
202 $logger->debug("AuthProxy found too many recent failures for '" . $args->{'username'} . "' : $fail_count, forcing failure state.");
203 $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
204 return OpenILS::Event->new( 'LOGIN_FAILED' );
208 my $authenticated = 0;
211 # if they specify an authenticator by name, only try that one
212 if ($args->{'name'}) {
213 $auths = [$authenticators_by_name{$args->{'name'}}];
215 $auths = \@authenticators;
218 foreach my $authenticator (@$auths) {
219 # skip authenticators specified for a different login type
221 if ($authenticator->login_types and $args->{'type'}) {
222 next unless grep(/^(all|$args->{'type'})$/, @{$authenticator->{'login_types'}});
224 if ($authenticator->org_units and $args->{'org'}) {
225 next unless grep(/^(all|$args->{'org'})$/, @{$authenticator->{'org_units'}});
229 # treat native specially
230 if ($authenticator->name eq 'native') {
231 $event = &_do_login($args);
233 $event = $authenticator->authenticate($args);
235 my $code = $U->event_code($event);
237 push @error_events, $event;
238 } elsif (defined $code) { # code is '0', i.e. SUCCESS
239 if ($authenticator->name eq 'native' and exists $event->{'payload'}) { # we have a complete native login
241 } else { # create an EG session for the successful external login
242 # if external login returns a payload, that payload is the
243 # user's Evergreen username
244 if ($event->{'payload'}) {
245 $args->{'username'} = $event->{'payload'};
248 # before we actually create the session, let's first check if
249 # Evergreen thinks this user is allowed to login
251 # (we do this *after* authentication to avoid any personal data
255 my $user = $U->cstorereq(
256 "open-ils.cstore.direct.actor.user.search.atomic",
257 { usrname => $args->{'username'} }
260 $logger->debug("Authenticated username '" . $args->{'username'} . "' has no Evergreen account, aborting");
261 return OpenILS::Event->new( 'LOGIN_FAILED' );
263 # TODO: verify that this authenticator is allowed to do auth
264 # for the specified username (i.e. if the authenticator is for
265 # Library A only, it shouldn't be able to do auth for
267 $args->{user_id} = $user->[0]->id;
270 # validate the account
272 user_id => $args->{user_id},
273 login_type => $args->{type},
274 workstation => $args->{workstation},
275 org_unit => $args->{org}
277 $event = &_auth_internal('user.validate', $trimmed_args);
278 if ($U->event_code($event)) { # non-zero = we didn't succeed
279 # can't recover from invalid user, return right away
281 } else { # it's all good
282 return &_auth_internal('session.create', $trimmed_args);
288 # if we got this far, we failed
289 # increment the brute force counter if 'native' didn't already
290 if (!exists $authenticators_by_name{'native'}) {
291 $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
293 # TODO: send back some form of collected error events
294 return OpenILS::Event->new( 'LOGIN_FAILED' );
298 my ($method, $args) = @_;
300 my $response = OpenSRF::AppSession->create("open-ils.auth_internal")->request(
301 'open-ils.auth_internal.'.$method,
305 return OpenILS::Event->new( 'LOGIN_FAILED' )
313 my $authenticated = shift;
315 my $seeder = $args->{'username'} ? $args->{'username'} : $args->{'barcode'};
317 OpenSRF::AppSession->create("open-ils.auth")
318 ->request( 'open-ils.auth.authenticate.init', $seeder )->gather(1);
320 return OpenILS::Event->new( 'LOGIN_FAILED' )
323 my $real_password = $args->{'password'};
324 $args->{'password'} = md5_hex( $seed . md5_hex($real_password) );
325 my $response = OpenSRF::AppSession->create("open-ils.auth")->request(
326 'open-ils.auth.authenticate.complete',
329 $args->{'password'} = $real_password;
331 return OpenILS::Event->new( 'LOGIN_FAILED' )
337 __PACKAGE__->register_method(
338 method => "authenticators",
339 api_name => "open-ils.auth_proxy.authenticators",
344 desc => q/Get a list of viable authenticators/,
346 {name=> "args", desc => q/A hash of arguments. Valid keys and their meanings:
347 type := Type of login being attempted (Staff Client, OPAC, etc.).
353 desc => "List of viable authenticators",
359 my ( $self, $conn, $args ) = @_;
363 foreach my $authenticator (@authenticators) {
364 # skip authenticators specified for a different login type
366 if ($authenticator->login_types and $args->{'type'}) {
367 next unless grep(/^(all|$args->{'type'})$/, @{$authenticator->login_types});
369 if ($authenticator->org_units and $args->{'org'}) {
370 next unless grep(/^(all|$args->{'org'})$/, @{$authenticator->org_units});
373 push @viable_auths, $authenticator->name;
376 return \@viable_auths;
380 # --------------------------------------------------------------------------
381 # Stub package for 'native' authenticator
382 # --------------------------------------------------------------------------
383 package OpenILS::Application::AuthProxy::Native;
384 use strict; use warnings;
385 use base 'OpenILS::Application::AuthProxy::AuthBase';