View | Details | Raw Unified | Return to bug 39224
Collapse All | Expand All

(-)a/C4/Auth.pm (-20 / +26 lines)
Lines 173-179 sub get_template_and_user { Link Here
173
    my $cookie_mgr = Koha::CookieManager->new;
173
    my $cookie_mgr = Koha::CookieManager->new;
174
174
175
    # Get shibboleth login attribute
175
    # Get shibboleth login attribute
176
    my $shib       = C4::Context->config('useshibboleth') && shib_ok();
176
    my $shib       = C4::Context->preference('ShibbolethAuthentication') && shib_ok();
177
    my $shib_login = $shib ? get_login_shib() : undef;
177
    my $shib_login = $shib ? get_login_shib() : undef;
178
178
179
    C4::Context->interface( $in->{type} );
179
    C4::Context->interface( $in->{type} );
Lines 811-817 sub checkauth { Link Here
811
    my $query = shift;
811
    my $query = shift;
812
812
813
    # Get shibboleth login attribute
813
    # Get shibboleth login attribute
814
    my $shib       = C4::Context->config('useshibboleth') && shib_ok();
814
    my $shib       = C4::Context->preference('ShibbolethAuthentication') && shib_ok();
815
    my $shib_login = $shib ? get_login_shib() : undef;
815
    my $shib_login = $shib ? get_login_shib() : undef;
816
816
817
    # $authnotrequired will be set for scripts which will run without authentication
817
    # $authnotrequired will be set for scripts which will run without authentication
Lines 1021-1026 sub checkauth { Link Here
1021
        $auth_state = 'logout';
1021
        $auth_state = 'logout';
1022
    }
1022
    }
1023
1023
1024
    my $is_sco_sci =
1025
        (      $query->param('sco_user_login')
1026
            || $query->param('sci_user_login')
1027
            || ( $template_name && $template_name =~ m|sc[io]/| ) );
1028
    my $forced_provider =
1029
        $is_sco_sci ? undef : Koha::Auth::Identity::Providers->find_forced_provider( $ENV{SERVER_NAME} );
1030
1024
    unless ($userid) {
1031
    unless ($userid) {
1025
1032
1026
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
1033
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
Lines 1151-1165 sub checkauth { Link Here
1151
            }
1158
            }
1152
1159
1153
            # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login.
1160
            # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login.
1154
            if (
1161
            if (   $shib
1155
                   $shib
1156
                && !$shibSuccess
1162
                && !$shibSuccess
1157
                && (
1163
                && $forced_provider )
1158
                    ( ( $type eq 'opac' ) && C4::Context->preference('OPACShibOnly') )
1159
                    || ( ( $type ne 'opac' )
1160
                        && C4::Context->preference('staffShibOnly') )
1161
                )
1162
                )
1163
            {
1164
            {
1164
                $return = 0;
1165
                $return = 0;
1165
            }
1166
            }
Lines 1522-1527 sub checkauth { Link Here
1522
        );
1523
        );
1523
    }
1524
    }
1524
1525
1526
    if ($forced_provider) {
1527
        my $redirect_url;
1528
        if ( $forced_provider->protocol eq 'SAML2' ) {
1529
            $redirect_url = login_shib_url($query);
1530
        } elsif ( $forced_provider->protocol eq 'OIDC' || $forced_provider->protocol eq 'OAuth' ) {
1531
            my $base = ( $type eq 'opac' ) ? "/api/v1/public/oauth/login" : "/api/v1/oauth/login";
1532
            $redirect_url = "$base/" . $forced_provider->code . "/$type";
1533
        }
1534
        if ($redirect_url) {
1535
            print $query->redirect( -uri => $redirect_url, -status => 303 );
1536
            safe_exit;
1537
        }
1538
    }
1539
1525
    if ($cas) {
1540
    if ($cas) {
1526
1541
1527
        # Is authentication against multiple CAS servers enabled?
1542
        # Is authentication against multiple CAS servers enabled?
Lines 1544-1558 sub checkauth { Link Here
1544
1559
1545
    if ($shib) {
1560
    if ($shib) {
1546
1561
1547
        #If shibOnly is enabled just go ahead and redirect directly
1548
        if (   ( ( $type eq 'opac' ) && C4::Context->preference('OPACShibOnly') )
1549
            || ( ( $type ne 'opac' ) && C4::Context->preference('staffShibOnly') ) )
1550
        {
1551
            my $redirect_url = login_shib_url($query);
1552
            print $query->redirect( -uri => "$redirect_url", -status => 303 );
1553
            safe_exit;
1554
        }
1555
1556
        $template->param(
1562
        $template->param(
1557
            shibbolethAuthentication => $shib,
1563
            shibbolethAuthentication => $shib,
1558
            shibbolethLoginUrl       => login_shib_url($query),
1564
            shibbolethLoginUrl       => login_shib_url($query),
Lines 2003-2009 sub checkpw { Link Here
2003
    $type = 'opac' unless $type;
2009
    $type = 'opac' unless $type;
2004
2010
2005
    # Get shibboleth login attribute
2011
    # Get shibboleth login attribute
2006
    my $shib       = C4::Context->config('useshibboleth') && shib_ok();
2012
    my $shib       = C4::Context->preference('ShibbolethAuthentication') && shib_ok();
2007
    my $shib_login = $shib ? get_login_shib() : undef;
2013
    my $shib_login = $shib ? get_login_shib() : undef;
2008
2014
2009
    my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2015
    my $anonymous_patron = C4::Context->preference('AnonymousPatron');
(-)a/C4/Auth_with_shibboleth.pm (-19 / +54 lines)
Lines 18-27 package C4::Auth_with_shibboleth; Link Here
18
# along with Koha; if not, see <https://www.gnu.org/licenses>.
18
# along with Koha; if not, see <https://www.gnu.org/licenses>.
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
use base 'Exporter';
21
22
our ( @ISA, @EXPORT_OK );
22
23
23
BEGIN {
24
BEGIN {
24
    our @EXPORT_OK = qw(shib_ok logout_shib login_shib_url checkpw_shib get_login_shib);
25
    require Exporter;
26
    @ISA       = qw(Exporter);
27
    @EXPORT_OK = qw(shib_ok logout_shib login_shib_url checkpw_shib get_login_shib);
25
}
28
}
26
29
27
use C4::Context;
30
use C4::Context;
Lines 34-39 use Carp qw( carp ); Link Here
34
use List::MoreUtils qw( any );
37
use List::MoreUtils qw( any );
35
38
36
use Koha::Logger;
39
use Koha::Logger;
40
use Koha::Auth::Identity::Providers;
37
41
38
# Check that shib config is not malformed
42
# Check that shib config is not malformed
39
43
Lines 241-268 sub _get_return { Link Here
241
}
245
}
242
246
243
sub _get_shib_config {
247
sub _get_shib_config {
244
    my $config = C4::Context->config('shibboleth');
248
    my $hostname = $ENV{SERVER_NAME};
245
249
    my $provider = Koha::Auth::Identity::Providers->search(
246
    if ( !$config ) {
250
        {
247
        Koha::Logger->get->warn('shibboleth config not defined');
251
            'me.protocol'          => 'SAML2',
252
            'me.enabled'           => 1,
253
            'hostname.hostname'    => $hostname,
254
            'hostnames.is_enabled' => 1,
255
        },
256
        { prefetch => [ 'mappings', { 'hostnames' => 'hostname' } ], rows => 1 }
257
    )->next;
258
    return 0 unless $provider;
259
260
    my $mapping       = $provider->mappings->as_auth_mapping;
261
    my $hostname_link = $provider->hostnames->search(
262
        { 'hostname.hostname' => $hostname },
263
        { join                => 'hostname' }
264
    )->next;
265
    my $matchpoint = $hostname_link ? $hostname_link->matchpoint : undef;
266
267
    unless ($matchpoint) {
268
        carp 'shibboleth matchpoint not defined';
248
        return 0;
269
        return 0;
249
    }
270
    }
250
271
251
    if ( $config->{matchpoint}
272
    unless ( defined $mapping->{$matchpoint}->{is} ) {
252
        && defined( $config->{mapping}->{ $config->{matchpoint} }->{is} ) )
273
        carp 'shibboleth matchpoint not mapped';
253
    {
254
        my $logger = Koha::Logger->get;
255
        $logger->debug( "koha borrower field to match: " . $config->{matchpoint} );
256
        $logger->debug( "shibboleth attribute to match: " . $config->{mapping}->{ $config->{matchpoint} }->{is} );
257
        return $config;
258
    } else {
259
        if ( !$config->{matchpoint} ) {
260
            carp 'shibboleth matchpoint not defined';
261
        } else {
262
            carp 'shibboleth matchpoint not mapped';
263
        }
264
        return 0;
274
        return 0;
265
    }
275
    }
276
277
    my $saml2_config = $provider->get_config // {};
278
279
    my $config = {
280
        matchpoint => $matchpoint,
281
        mapping    => $mapping,
282
        autocreate => $saml2_config->{autocreate} || 0,
283
        sync       => $saml2_config->{sync}       || 0,
284
        welcome    => $saml2_config->{welcome}    || 0,
285
    };
286
287
    my $logger = Koha::Logger->get;
288
    $logger->debug( "koha borrower field to match: " . $config->{matchpoint} );
289
    $logger->debug( "shibboleth attribute to match: " . $mapping->{ $config->{matchpoint} }->{is} );
290
291
    return $config;
266
}
292
}
267
293
268
1;
294
1;
Lines 405-410 A sugar function to that simply returns the current page URI with appropriate pr Link Here
405
431
406
This routine is NOT exported
432
This routine is NOT exported
407
433
434
=head2 get_force_sso_setting
435
436
  my $force_sso = get_force_sso_setting($interface);
437
438
Returns the force SSO setting for the given interface (opac or intranet).
439
Returns 0 if shibboleth is not configured or the setting is disabled.
440
441
  $interface - 'opac' or 'intranet'
442
408
=head2 _get_shib_config
443
=head2 _get_shib_config
409
444
410
  my $config = _get_shib_config();
445
  my $config = _get_shib_config();
(-)a/Koha/Auth/Client.pm (-1 / +3 lines)
Lines 59-69 sub get_user { Link Here
59
    my $data          = $params->{data};
59
    my $data          = $params->{data};
60
    my $interface     = $params->{interface};
60
    my $interface     = $params->{interface};
61
    my $config        = $params->{config};
61
    my $config        = $params->{config};
62
    my $hostname      = $params->{hostname};
62
63
63
    my $provider = Koha::Auth::Identity::Providers->search( { code => $provider_code } )->next;
64
    my $provider = Koha::Auth::Identity::Providers->search( { code => $provider_code } )->next;
64
65
65
    my ( $mapped_data, $patron ) =
66
    my ( $mapped_data, $patron ) =
66
        $self->_get_data_and_patron( { provider => $provider, data => $data, config => $config } );
67
        $self->_get_data_and_patron(
68
        { provider => $provider, data => $data, config => $config, hostname => $hostname } );
67
69
68
    $mapped_data //= {};
70
    $mapped_data //= {};
69
71
(-)a/Koha/Auth/Client/OAuth.pm (-9 / +16 lines)
Lines 54-75 sub _get_data_and_patron { Link Here
54
    my $provider = $params->{provider};
54
    my $provider = $params->{provider};
55
    my $data     = $params->{data};
55
    my $data     = $params->{data};
56
    my $config   = $params->{config};
56
    my $config   = $params->{config};
57
    my $hostname = $params->{hostname};
57
58
58
    my $patron;
59
    my $patron;
59
    my $mapped_data;
60
    my $mapped_data;
60
61
61
    my $mapping    = decode_json( $provider->mapping );
62
    my $mapping       = $provider->mappings->as_auth_mapping;
62
    my $matchpoint = $provider->matchpoint;
63
    my $hostname_link = $hostname
64
        ? $provider->hostnames->search(
65
        { 'hostname.hostname' => $hostname },
66
        { join                => 'hostname' }
67
        )->next
68
        : undef;
69
    my $matchpoint = $hostname_link ? $hostname_link->matchpoint : undef;
63
70
64
    if ( $data->{id_token} ) {
71
    if ( $data->{id_token} ) {
65
        my ( $header_part, $claims_part, $footer_part ) = split( /\./, $data->{id_token} );
72
        my ( $header_part, $claims_part, $footer_part ) = split( /\./, $data->{id_token} );
66
73
67
        my $claim = decode_json( decode_base64url($claims_part) );
74
        my $claim = decode_json( decode_base64url($claims_part) );
68
75
69
        foreach my $key ( keys %$mapping ) {
76
        foreach my $koha_field ( keys %$mapping ) {
70
            my $pkey = $mapping->{$key};
77
            my $pkey = $mapping->{$koha_field}{is};
71
            $mapped_data->{$key} = $claim->{$pkey}
78
            $mapped_data->{$koha_field} = $claim->{$pkey}
72
                if defined $claim->{$pkey};
79
                if defined $pkey && defined $claim->{$pkey};
73
        }
80
        }
74
81
75
        $patron = $self->_find_patron_by_matchpoint( $matchpoint, $mapped_data->{$matchpoint} );
82
        $patron = $self->_find_patron_by_matchpoint( $matchpoint, $mapped_data->{$matchpoint} );
Lines 87-96 sub _get_data_and_patron { Link Here
87
            ? $tx->res->json
94
            ? $tx->res->json
88
            : Mojo::Parameters->new( $tx->res->body )->to_hash;
95
            : Mojo::Parameters->new( $tx->res->body )->to_hash;
89
96
90
        foreach my $key ( keys %$mapping ) {
97
        foreach my $koha_field ( keys %$mapping ) {
91
            my $pkey  = $mapping->{$key};
98
            my $pkey  = $mapping->{$koha_field}{is};
92
            my $value = $self->_traverse_hash( { base => $claim, keys => $pkey } );
99
            my $value = $self->_traverse_hash( { base => $claim, keys => $pkey } );
93
            $mapped_data->{$key} = $value
100
            $mapped_data->{$koha_field} = $value
94
                if defined $value;
101
                if defined $value;
95
        }
102
        }
96
103
(-)a/Koha/Auth/Hostname.pm (+87 lines)
Line 0 Link Here
1
package Koha::Auth::Hostname;
2
3
# Copyright Koha Community 2026
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <https://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use base qw(Koha::Object);
23
24
use C4::Context;
25
use Koha::Database;
26
27
=head1 NAME
28
29
Koha::Auth::Hostname - Koha Auth Hostname Object class
30
31
=head1 API
32
33
=head2 Class methods
34
35
=head3 sync_from_syspref
36
37
    Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', $url );
38
39
Updates (or inserts) the reserved hostname row for the given URL syspref.
40
C<OPACBaseURL> is always stored as C<hostname_id = 1>; C<staffClientBaseURL>
41
as C<hostname_id = 2>. Does nothing if C<$value> is empty or does not contain
42
a recognisable hostname.
43
44
=cut
45
46
my %SYSPREF_HOSTNAME_ID = (
47
    opacbaseurl        => 1,
48
    staffclientbaseurl => 2,
49
);
50
51
sub sync_from_syspref {
52
    my ( $class, $pref_name, $value ) = @_;
53
    $value //= '';
54
    my $hostname_id = $SYSPREF_HOSTNAME_ID{ lc $pref_name } or return;
55
    my ($hostname)  = $value =~ m{^https?://([^/:?#]+)}     or return;
56
    Koha::Database->new->schema->resultset('Hostname')->update_or_create(
57
        { hostname_id => $hostname_id, hostname => $hostname },
58
        { key         => 'primary' }
59
    );
60
}
61
62
=head3 sync_from_sysprefs
63
64
Ensures that the hostnames derived from OPACBaseURL and staffClientBaseURL
65
are present in the hostnames table. OPACBaseURL maps to C<hostname_id = 1>
66
and staffClientBaseURL to C<hostname_id = 2>.
67
68
=cut
69
70
sub sync_from_sysprefs {
71
    my ($class) = @_;
72
    for my $pref ( keys %SYSPREF_HOSTNAME_ID ) {
73
        $class->sync_from_syspref( $pref, C4::Context->preference($pref) );
74
    }
75
}
76
77
=head2 Internal methods
78
79
=head3 _type
80
81
=cut
82
83
sub _type {
84
    return 'Hostname';
85
}
86
87
1;
(-)a/Koha/Auth/Hostnames.pm (+50 lines)
Line 0 Link Here
1
package Koha::Auth::Hostnames;
2
3
# Copyright Koha Community 2026
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <https://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use base qw(Koha::Objects);
23
24
use Koha::Auth::Hostname;
25
26
=head1 NAME
27
28
Koha::Auth::Hostnames - Koha Auth Hostnames Object set class
29
30
=head1 API
31
32
=head2 Internal methods
33
34
=head3 _type
35
36
=cut
37
38
sub _type {
39
    return 'Hostname';
40
}
41
42
=head3 object_class
43
44
=cut
45
46
sub object_class {
47
    return 'Koha::Auth::Hostname';
48
}
49
50
1;
(-)a/Koha/Auth/Identity/Provider.pm (-62 / +47 lines)
Lines 22-27 use Modern::Perl; Link Here
22
use base qw(Koha::Object Koha::Object::JSONFields);
22
use base qw(Koha::Object Koha::Object::JSONFields);
23
23
24
use Koha::Auth::Identity::Provider::Domains;
24
use Koha::Auth::Identity::Provider::Domains;
25
use Koha::Auth::Identity::Provider::Hostnames;
26
use Koha::Auth::Identity::Provider::Mappings;
25
use Koha::Exceptions;
27
use Koha::Exceptions;
26
28
27
=head1 NAME
29
=head1 NAME
Lines 46-51 sub domains { Link Here
46
    return Koha::Auth::Identity::Provider::Domains->_new_from_dbic( scalar $self->_result->domains );
48
    return Koha::Auth::Identity::Provider::Domains->_new_from_dbic( scalar $self->_result->domains );
47
}
49
}
48
50
51
=head3 mappings
52
53
    my $mappings = $provider->mappings;
54
55
Returns the related I<Koha::Auth::Identity::Provider::Mappings> iterator.
56
57
=cut
58
59
sub mappings {
60
    my ($self) = @_;
61
62
    return Koha::Auth::Identity::Provider::Mappings->_new_from_dbic(
63
        scalar $self->_result->identity_provider_mappings );
64
}
65
66
=head3 hostnames
67
68
    my $hostnames = $provider->hostnames;
69
70
Returns the related I<Koha::Auth::Identity::Provider::Hostnames> iterator.
71
72
=cut
73
74
sub hostnames {
75
    my ($self) = @_;
76
77
    return Koha::Auth::Identity::Provider::Hostnames->_new_from_dbic( scalar $self->_result->hostnames );
78
}
79
49
=head3 get_config
80
=head3 get_config
50
81
51
    my $config = $provider->get_config;
82
    my $config = $provider->get_config;
Lines 81-86 sub get_config { Link Here
81
        }
112
        }
82
    );
113
    );
83
114
115
    # SAML2 (config holds protocol-specific settings like autocreate/sync/welcome)
116
    $provider->set_config(
117
        {
118
            autocreate => 1,
119
            sync       => 1,
120
            welcome    => 0,
121
        }
122
    );
123
84
This method stores the passed config in JSON format.
124
This method stores the passed config in JSON format.
85
125
86
=cut
126
=cut
Lines 99-151 sub set_config { Link Here
99
    return $self->set_encoded_json_field( { data => $config, field => 'config' } );
139
    return $self->set_encoded_json_field( { data => $config, field => 'config' } );
100
}
140
}
101
141
102
=head3 get_mapping
142
=head3 store
103
143
104
    my $mapping = $provider->get_mapping;
144
Identity provider specific store method to ensure config population before saving to db
105
106
Returns a I<hashref> containing the attribute mapping for the provider.
107
145
108
=cut
146
=cut
109
147
110
sub get_mapping {
148
sub store {
111
    my ($self) = @_;
149
    my ($self) = @_;
112
150
    $self->config( $self->config // '{}' );
113
    return $self->decode_json_field( { field => 'mapping' } );
151
    return $self->SUPER::store();
114
}
115
116
=head3 set_mapping
117
118
    $provider->mapping( $mapping );
119
120
This method stores the passed mappings in JSON format.
121
122
=cut
123
124
sub set_mapping {
125
    my ( $self, $mapping ) = @_;
126
127
    return $self->set_encoded_json_field( { data => $mapping, field => 'mapping' } );
128
}
129
130
=head3 upgrade_class
131
132
    my $upgraded_object = $provider->upgrade_class
133
134
Returns a new instance of the object, with the right class.
135
136
=cut
137
138
sub upgrade_class {
139
    my ($self) = @_;
140
    my $protocol = $self->protocol;
141
142
    my $class = $self->protocol_to_class_mapping->{$protocol};
143
144
    Koha::Exception->throw( $protocol . ' is not a valid protocol' )
145
        unless $class;
146
147
    eval "require $class";
148
    return $class->_new_from_dbic( $self->_result );
149
}
152
}
150
153
151
=head2 Internal methods
154
=head2 Internal methods
Lines 162-173 suitable for API output. Link Here
162
sub to_api {
165
sub to_api {
163
    my ( $self, $params ) = @_;
166
    my ( $self, $params ) = @_;
164
167
165
    my $config  = $self->get_config;
168
    my $config = $self->get_config;
166
    my $mapping = $self->get_mapping;
167
169
168
    my $json = $self->SUPER::to_api($params);
170
    my $json = $self->SUPER::to_api($params);
169
    $json->{config}  = $config;
171
    $json->{config} = $config;
170
    $json->{mapping} = $mapping;
171
172
172
    return $json;
173
    return $json;
173
}
174
}
Lines 180-201 sub _type { Link Here
180
    return 'IdentityProvider';
181
    return 'IdentityProvider';
181
}
182
}
182
183
183
=head3 protocol_to_class_mapping
184
185
    my $mapping = Koha::Auth::Identity::Provider::protocol_to_class_mapping
186
187
Internal method that returns a mapping between I<protocol> codes and
188
implementing I<classes>. To be used by B<upgrade_class>.
189
190
=cut
191
192
sub protocol_to_class_mapping {
193
    return {
194
        OAuth => 'Koha::Auth::Identity::Provider::OAuth',
195
        OIDC  => 'Koha::Auth::Identity::Provider::OIDC',
196
    };
197
}
198
199
=head3 mandatory_config_attributes
184
=head3 mandatory_config_attributes
200
185
201
Stub method for raising exceptions on invalid protocols.
186
Stub method for raising exceptions on invalid protocols.
(-)a/Koha/Auth/Identity/Provider/Hostname.pm (+56 lines)
Line 0 Link Here
1
package Koha::Auth::Identity::Provider::Hostname;
2
3
# Copyright Koha Community 2026
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <https://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use base qw(Koha::Object);
23
24
=head1 NAME
25
26
Koha::Auth::Identity::Provider::Hostname - Koha Auth Provider Hostname Object class
27
28
=head1 API
29
30
=head2 Class methods
31
32
=head3 to_api
33
34
Overrides the default serialization to embed the hostname string from the
35
related Hostname record alongside the hostname_id FK.
36
37
=cut
38
39
sub to_api {
40
    my ( $self, $params ) = @_;
41
    my $data = $self->SUPER::to_api($params);
42
    $data->{hostname} = $self->_result->hostname->hostname;
43
    return $data;
44
}
45
46
=head2 Internal methods
47
48
=head3 _type
49
50
=cut
51
52
sub _type {
53
    return 'IdentityProviderHostname';
54
}
55
56
1;
(-)a/Koha/Auth/Identity/Provider/Hostnames.pm (+51 lines)
Line 0 Link Here
1
package Koha::Auth::Identity::Provider::Hostnames;
2
3
# Copyright Koha Community 2026
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <https://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::Auth::Identity::Provider::Hostname;
24
25
use base qw(Koha::Objects);
26
27
=head1 NAME
28
29
Koha::Auth::Identity::Provider::Hostnames - Koha Auth Provider Hostnames Object set class
30
31
=head1 API
32
33
=head2 Internal methods
34
35
=head3 _type
36
37
=cut
38
39
sub _type {
40
    return 'IdentityProviderHostname';
41
}
42
43
=head3 object_class
44
45
=cut
46
47
sub object_class {
48
    return 'Koha::Auth::Identity::Provider::Hostname';
49
}
50
51
1;
(-)a/Koha/Auth/Identity/Provider/Mapping.pm (+60 lines)
Line 0 Link Here
1
package Koha::Auth::Identity::Provider::Mapping;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use base qw(Koha::Object);
21
22
use Koha::Exceptions;
23
use Koha::Auth::Identity::Provider::Mappings;
24
25
=head1 NAME
26
27
Koha::Auth::Identity::Provider::Mapping - Koha Identity Provider field mapping object
28
29
=head1 API
30
31
=head2 Class methods
32
33
=head3 store
34
35
Override store to validate that at least one of C<provider_field> or
36
C<default_content> is supplied.
37
38
=cut
39
40
sub store {
41
    my ($self) = @_;
42
43
    unless ( defined $self->provider_field || defined $self->default_content ) {
44
        Koha::Exceptions::MissingParameter->throw( parameter => 'provider_field or default_content' );
45
    }
46
47
    return $self->SUPER::store();
48
}
49
50
=head2 Internal methods
51
52
=head3 _type
53
54
=cut
55
56
sub _type {
57
    return 'IdentityProviderMapping';
58
}
59
60
1;
(-)a/Koha/Auth/Identity/Provider/Mappings.pm (+75 lines)
Line 0 Link Here
1
package Koha::Auth::Identity::Provider::Mappings;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use base qw(Koha::Objects);
21
22
use Koha::Auth::Identity::Provider::Mapping;
23
24
=head1 NAME
25
26
Koha::Auth::Identity::Provider::Mappings - Koha Identity Provider field mappings collection
27
28
=head1 API
29
30
=head2 Class methods
31
32
=head3 as_auth_mapping
33
34
    my $mapping_hashref = $mappings->as_auth_mapping;
35
36
Returns a hashref suitable for use by the authentication layer:
37
C<{ koha_field => { is => provider_field, content => default_content }, ... }>.
38
The matchpoint is stored on the provider itself (C<identity_providers.matchpoint>).
39
40
=cut
41
42
sub as_auth_mapping {
43
    my ($self) = @_;
44
45
    my %mapping;
46
47
    while ( my $m = $self->next ) {
48
        $mapping{ $m->koha_field } = {
49
            is      => $m->provider_field,
50
            content => $m->default_content,
51
        };
52
    }
53
54
    return \%mapping;
55
}
56
57
=head2 Internal methods
58
59
=head3 _type
60
61
=cut
62
63
sub _type {
64
    return 'IdentityProviderMapping';
65
}
66
67
=head3 object_class
68
69
=cut
70
71
sub object_class {
72
    return 'Koha::Auth::Identity::Provider::Mapping';
73
}
74
75
1;
(-)a/Koha/Auth/Identity/Providers.pm (-2 / +68 lines)
Lines 19-26 package Koha::Auth::Identity::Providers; Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Koha::Database;
23
use Koha::Auth::Identity::Provider;
22
use Koha::Auth::Identity::Provider;
23
use Koha::Auth::Identity::Provider::Hostnames;
24
use Koha::Auth::Identity::Provider::OAuth;
25
use Koha::Auth::Identity::Provider::OIDC;
24
26
25
use base qw(Koha::Objects);
27
use base qw(Koha::Objects);
26
28
Lines 30-35 Koha::Auth::Identity::Providers - Koha Auth Provider Object class Link Here
30
32
31
=head1 API
33
=head1 API
32
34
35
=head2 Class methods
36
37
=head3 find_forced_provider
38
39
    my $provider = Koha::Auth::Identity::Providers->find_forced_provider($hostname);
40
41
Returns the I<Koha::Auth::Identity::Provider> object if the given I<hostname> is configured to
42
force SSO, or C<undef> if no forced provider is found.
43
44
=cut
45
46
sub find_forced_provider {
47
    my ( $self, $hostname ) = @_;
48
    return unless $hostname;
49
50
    my $hostname_link = Koha::Auth::Identity::Provider::Hostnames->search(
51
        {
52
            'hostname.hostname'         => $hostname,
53
            'identity_provider.enabled' => 1,
54
            'me.is_enabled'             => 1,
55
            'me.force_sso'              => 1,
56
        },
57
        { join => [ 'hostname', 'identity_provider' ] }
58
    )->next;
59
60
    return unless $hostname_link;
61
62
    my $ip_result = $hostname_link->_result->identity_provider;
63
    return $self->object_class($ip_result)->_new_from_dbic($ip_result);
64
}
65
33
=head2 Internal methods
66
=head2 Internal methods
34
67
35
=cut
68
=cut
Lines 42-53 sub _type { Link Here
42
    return 'IdentityProvider';
75
    return 'IdentityProvider';
43
}
76
}
44
77
78
=head3 _polymorphic_field
79
80
Return the field in the table that defines the polymorphic class to be built
81
82
=cut
83
84
sub _polymorphic_field {
85
    return 'protocol';
86
}
87
88
=head3 _polymorphic_map
89
90
Return the mapping from protocol value to implementing class name
91
92
=cut
93
94
sub _polymorphic_map {
95
    return {
96
        OAuth => 'Koha::Auth::Identity::Provider::OAuth',
97
        OIDC  => 'Koha::Auth::Identity::Provider::OIDC',
98
    };
99
}
100
45
=head3 object_class
101
=head3 object_class
46
102
103
Return object class dynamically based on protocol
104
47
=cut
105
=cut
48
106
49
sub object_class {
107
sub object_class {
50
    return 'Koha::Auth::Identity::Provider';
108
    my ( $self, $object ) = @_;
109
110
    return 'Koha::Auth::Identity::Provider' unless $object;
111
112
    my $field = $self->_polymorphic_field;
113
    my $map   = $self->_polymorphic_map;
114
115
    return $map->{ $object->$field } || 'Koha::Auth::Identity::Provider';
51
}
116
}
52
117
53
1;
118
1;
119
(-)a/Koha/Config/SysPref.pm (-1 / +8 lines)
Lines 46-52 sub store { Link Here
46
46
47
    C4::Log::logaction( 'SYSTEMPREFERENCE', $action, undef, $self->variable . ' | ' . $self->value );
47
    C4::Log::logaction( 'SYSTEMPREFERENCE', $action, undef, $self->variable . ' | ' . $self->value );
48
48
49
    return $self->SUPER::store($self);
49
    my $result = $self->SUPER::store($self);
50
51
    if ( $self->variable =~ /\A(?:opacbaseurl|staffclientbaseurl)\z/i ) {
52
        require Koha::Auth::Hostname;
53
        Koha::Auth::Hostname->sync_from_syspref( $self->variable, $self->value );
54
    }
55
56
    return $result;
50
}
57
}
51
58
52
=head3 delete
59
=head3 delete
(-)a/Koha/REST/V1/OAuth/Client.pm (-1 / +2 lines)
Lines 131-137 sub login { Link Here
131
                        provider  => $provider,
131
                        provider  => $provider,
132
                        data      => $response,
132
                        data      => $response,
133
                        interface => $interface,
133
                        interface => $interface,
134
                        config    => $c->oauth2->providers->{$provider}
134
                        config    => $c->oauth2->providers->{$provider},
135
                        hostname  => $c->req->url->base->host,
135
                    }
136
                    }
136
                );
137
                );
137
138
(-)a/Koha/Template/Plugin/AuthClient.pm (-2 / +15 lines)
Lines 55-62 sub get_providers { Link Here
55
55
56
    # Handle database upgrade state where schema might be out of sync
56
    # Handle database upgrade state where schema might be out of sync
57
    try {
57
    try {
58
        my $providers =
58
        my $hostname  = $ENV{SERVER_NAME};
59
            Koha::Auth::Identity::Providers->search( { "domains.allow_$interface" => 1 }, { prefetch => 'domains' } );
59
        my $providers = Koha::Auth::Identity::Providers->search(
60
            {
61
                "domains.allow_$interface" => 1,
62
                "me.enabled"               => 1,
63
                -or                        => [
64
                    { 'hostname.hostname'                       => $hostname, 'hostnames.is_enabled' => 1 },
65
                    { 'hostnames.identity_provider_hostname_id' => undef }
66
                ]
67
            },
68
            {
69
                join     => [ 'domains', { 'hostnames' => 'hostname' } ],
70
                distinct => 1
71
            }
72
        );
60
        my $base_url = ( $interface eq 'staff' ) ? "/api/v1/oauth/login" : "/api/v1/public/oauth/login";
73
        my $base_url = ( $interface eq 'staff' ) ? "/api/v1/oauth/login" : "/api/v1/public/oauth/login";
61
74
62
        while ( my $provider = $providers->next ) {
75
        while ( my $provider = $providers->next ) {
(-)a/Makefile.PL (-1 / +1 lines)
Lines 363-368 my $target_map = { Link Here
363
    './serials'                                        => 'INTRANET_CGI_DIR',
363
    './serials'                                        => 'INTRANET_CGI_DIR',
364
    './services'                                       => 'INTRANET_CGI_DIR',
364
    './services'                                       => 'INTRANET_CGI_DIR',
365
    './sip2'                                           => 'INTRANET_CGI_DIR',
365
    './sip2'                                           => 'INTRANET_CGI_DIR',
366
    './shibboleth'                                     => 'INTRANET_CGI_DIR',
366
    './skel'                                           => 'NONE',
367
    './skel'                                           => 'NONE',
367
    './skel/var/lock/koha'                             => { target => 'LOCK_DIR',       trimdir => -1 },
368
    './skel/var/lock/koha'                             => { target => 'LOCK_DIR',       trimdir => -1 },
368
    './skel/var/log/koha'                              => { target => 'LOG_DIR',        trimdir => -1 },
369
    './skel/var/log/koha'                              => { target => 'LOG_DIR',        trimdir => -1 },
369
- 

Return to bug 39224