From 17af7b39a8e5ed9cd2c1d2f5f6d8ecbc8c13130e Mon Sep 17 00:00:00 2001 From: Agustin Moyano Date: Thu, 18 Aug 2022 16:29:19 -0300 Subject: [PATCH] Bug 31378: Add Koha::Auth::Client* modules Signed-off-by: Lukasz Koszyk Signed-off-by: Tomas Cohen Arazi --- Koha/Auth/Client.pm | 198 ++++++++++++++++++++++++++++++++++++++ Koha/Auth/Client/OAuth.pm | 118 +++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 Koha/Auth/Client.pm create mode 100644 Koha/Auth/Client/OAuth.pm diff --git a/Koha/Auth/Client.pm b/Koha/Auth/Client.pm new file mode 100644 index 00000000000..7fd66271973 --- /dev/null +++ b/Koha/Auth/Client.pm @@ -0,0 +1,198 @@ +package Koha::Auth::Client; + +# Copyright Theke Solutions 2022 +# +# This file is part of Koha. +# +# Koha 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 3 of the License, or +# (at your option) any later version. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Koha::Exceptions::Auth; +use Koha::Auth::Providers; + +=head1 NAME + +Koha::Auth::Client - Base Koha auth client + +=head1 API + +=head2 Methods + +=head3 new + + my $auth_client = Koha::Auth::Client->new(); + +=cut + +sub new { + my ($class) = @_; + my $self = {}; + + bless( $self, $class ); +} + +=head3 get_user + + $auth_client->get_user($provider, $data) + +Get user data according to provider's mapping configuration + +=cut + +sub get_user { + my ( $self, $params ) = @_; + my $provider_code = $params->{provider}; + my $data = $params->{data}; + my $interface = $params->{interface}; + my $config = $params->{config}; + + my $provider = Koha::Auth::Providers->search({ code => $provider_code })->next; + + my ( $mapped_data, $patron ) = $self->_get_data_and_patron({ provider => $provider, data => $data, config => $config }); + + if ($mapped_data) { + my $domain = $self->has_valid_domain_config({ provider => $provider, email => $mapped_data->{email}, interface => $interface}); + + $mapped_data->{categorycode} = $domain->default_category_id; + $mapped_data->{branchcode} = $domain->default_library_id; + + return ( $patron, $mapped_data, $domain ); + } +} + +=head3 get_valid_domain_config + + my $domain = Koha::Auth::Client->get_valid_domain_config( + { provider => $provider, + email => $user_email, + interface => $interface + } + ); + +Gets the best suited valid domain configuration for the given provider. + +=cut + +sub get_valid_domain_config { + # FIXME: Should be a hashref param + my ( $self, $params ) = @_; + my $provider = $params->{provider}; + my $user_email = $params->{email}; + my $interface = $params->{interface}; + + my $domains = $provider->domains; + my $pattern = '@'; + my $allow = "allow_$interface"; + my @subdomain_matches; + my $default_match; + + while ( my $domain = $domains->next ) { + next unless $domain->$allow; + + my $domain_text = $domain->domain; + unless ( defined $domain_text && $domain_text ne '') { + $default_match = $domain; + next; + } + my ( $asterisk, $domain_name ) = ( $domain_text =~ /^(\*)?(.+)$/ ); + if ( $asterisk eq '*' ) { + $pattern .= '.*'; + } + $domain_name =~ s/\./\\\./g; + $pattern .= $domain_name . '$'; + if ( $user_email =~ /$pattern/ ) { + if ( $asterisk eq '*' ) { + push @subdomain_matches, { domain => $domain, match_length => length $domain_name }; + } else { + + # Perfect match.. return this one. + return $domain; + } + } + } + + if ( @subdomain_matches ) { + @subdomain_matches = sort { $b->{match_length} <=> $a->{match_length} } @subdomain_matches + unless scalar @subdomain_matches == 1; + return $subdomain_matches[0]->{domain}; + } + + return $default_match || 0; +} + +=head3 has_valid_domain_config + + my $has_valid_domain = Koha::Auth::Client->has_valid_domain_config( + { provider => $provider, + email => $user_email, + interface => $interface + } + ); + +Checks if provider has a valid domain for user email. If has, returns that domain. + +=cut + +sub has_valid_domain_config { + # FIXME: Should be a hashref param + my ( $self, $params ) = @_; + my $domain = $self->get_valid_domain_config( $params ); + + Koha::Exceptions::Auth::NoValidDomain->throw( code => 401 ) + unless $domain; + + return $domain; +} + +=head3 _get_data_and_patron + + my $mapping = $auth_client->_get_data_and_patron( + { provider => $provider, + data => $data, + config => $config + } + ); + +Generic method that maps raw data to patron schema, and returns a patron if it can. + +Note: this only returns an empty I. Each class should have its +own mapping returned. + +=cut + +sub _get_data_and_patron { + return {}; +} + +=head3 _tranverse_hash + + my $value = $auth_client->_tranverse_hash( { base => $base_hash, keys => $key_string } ); + +Get deep nested value in a hash. + +=cut + +sub _tranverse_hash { + my ($self, $params) = @_; + my $base = $params->{base}; + my $keys = $params->{keys}; + my ($key, $rest) = ($keys =~ /^([^.]+)(?:\.(.*))?/); + return unless defined $key; + my $value = ref $base eq 'HASH' ? $base->{$key} : $base->[$key]; + return $value unless $rest; + return $self->_tranverse_hash({ base => $value, keys => $rest }); +} + +1; diff --git a/Koha/Auth/Client/OAuth.pm b/Koha/Auth/Client/OAuth.pm new file mode 100644 index 00000000000..728fa4e3ec4 --- /dev/null +++ b/Koha/Auth/Client/OAuth.pm @@ -0,0 +1,118 @@ +package Koha::Auth::Client::OAuth; + +# Copyright Theke Solutions 2022 +# +# This file is part of Koha. +# +# Koha 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 3 of the License, or +# (at your option) any later version. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use JSON qw( decode_json ); +use MIME::Base64 qw{ decode_base64url }; +use Koha::Patrons; +use Mojo::UserAgent; +use Mojo::Parameters; + +use base qw( Koha::Auth::Client ); + +=head1 NAME + +Koha::Auth::Client::OAuth - Koha OAuth Client + +=head1 API + +=head2 Class methods + +=head3 _get_data_and_patron + + my $mapping = $object->_get_data_and_patron( + { provider => $provider, + data => $data, + config => $config + } + ); + +Maps OAuth raw data to a patron schema, and returns a patron if it can. + +=cut + +sub _get_data_and_patron { + my ( $self, $params ) = @_; + + my $provider = $params->{provider}; + my $data = $params->{data}; + my $config = $params->{config}; + + my $patron; + my $mapped_data; + + my $mapping = decode_json( $provider->mapping ); + my $matchpoint = $provider->matchpoint; + + if ( $data->{id_token} ) { + my ( $header_part, $claims_part, $footer_part ) = split( /\./, $data->{id_token} ); + + my $claim = decode_json( decode_base64url($claims_part) ); + foreach my $key ( keys %$mapping ) { + my $pkey = $mapping->{$key}; + $mapped_data->{$key} = $claim->{$pkey} + if defined $claim->{$pkey}; + } + + my $value = $mapped_data->{$matchpoint}; + + my $matchpoint_rs = Koha::Patrons->search( { $matchpoint => $value } ); + + if ( defined $value and $matchpoint_rs->count ) { + $patron = $matchpoint_rs->next; + } + + return ( $mapped_data, $patron ) + if $patron; + } + + if ( defined $config->{userinfo_url} ) { + my $access_token = $data->{access_token}; + my $ua = Mojo::UserAgent->new; + my $tx = $ua->get( $config->{userinfo_url} => { Authorization => "Bearer $access_token" } ); + my $code = $tx->res->code || 'No response'; + + return if $code ne '200'; + my $claim = + $tx->res->headers->content_type =~ m!^(application/json|text/javascript)(;\s*charset=\S+)?$! + ? $tx->res->json + : Mojo::Parameters->new( $tx->res->body )->to_hash; + + foreach my $key ( keys %$mapping ) { + my $pkey = $mapping->{$key}; + my $value = $self->_tranverse_hash( { base => $claim, keys => $pkey } ); + $mapped_data->{$key} = $value + if defined $value; + } + + my $value = $mapped_data->{$matchpoint}; + + my $matchpoint_rs = Koha::Patrons->search( { $matchpoint => $value } ); + + if ( defined $value and $matchpoint_rs->count ) { + $patron = $matchpoint_rs->next; + } + + return ( $mapped_data, $patron ) + if $patron; + } +} + +1; -- 2.34.1