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

(-)a/Koha/Auth/Client/CAS.pm (+147 lines)
Line 0 Link Here
1
package Koha::Auth::Client::CAS;
2
3
# Copyright 2025 Koha Development team
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 <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Authen::CAS::Client;
23
use Try::Tiny qw( catch try );
24
25
use Koha::Logger;
26
27
use base qw(Koha::Auth::Client);
28
29
=head1 NAME
30
31
Koha::Auth::Client::CAS - CAS authentication client
32
33
=head1 API
34
35
=head2 Class methods
36
37
=head3 new
38
39
    my $client = Koha::Auth::Client::CAS->new( { provider => $provider } );
40
41
Constructor. Inherits from L<Koha::Auth::Client>.
42
43
=cut
44
45
=head2 Instance methods
46
47
=head3 get_user
48
49
    my ( $user_data, $patron ) = $client->get_user( { ticket => $ticket, service => $service_url } );
50
51
Authenticates a user and returns their data and patron object. This method is
52
inherited from the parent class and calls C<_get_data_and_patron> internally.
53
54
=cut
55
56
=head3 _get_data_and_patron
57
58
    my ( $user_data, $patron ) = $client->_get_data_and_patron( { ticket => $ticket, service => $service_url } );
59
60
Validates a CAS ticket and returns user data and patron object.
61
62
=cut
63
64
sub _get_data_and_patron {
65
    my ( $self, $data ) = @_;
66
67
    my $config      = $self->provider->get_config;
68
    my $ticket      = $data->{ticket};
69
    my $service_url = $data->{service};
70
71
    my $logger = Koha::Logger->get();
72
73
    return ( {}, undef ) unless $ticket && $service_url;
74
75
    try {
76
        # Initialize CAS client
77
        my $cas = Authen::CAS::Client->new( $config->{cas_url} );
78
79
        # Validate the ticket
80
        my $response = $cas->service_validate(
81
            service => $service_url,
82
            ticket  => $ticket
83
        );
84
85
        if ( $response->is_success ) {
86
            my $user_identifier = $response->user;
87
            $logger->info("CAS authentication successful for user: $user_identifier");
88
89
            # Map CAS response to user data
90
            my $user_data = {
91
                userid => $user_identifier,
92
                email  => $user_identifier,    # CAS typically returns userid, may need mapping
93
            };
94
95
            # Find patron using the configured matchpoint
96
            my $patron = $self->_get_patron($user_data);
97
98
            return ( $user_data, $patron );
99
        } else {
100
            $logger->warn( "CAS ticket validation failed: " . $response->code );
101
            return ( {}, undef );
102
        }
103
    } catch {
104
        $logger->error("CAS authentication error: $_");
105
        return ( {}, undef );
106
    };
107
}
108
109
=head3 get_cas_login_url
110
111
    my $login_url = $client->get_cas_login_url( $config, $service_url );
112
113
Returns the CAS login URL for the given service URL.
114
115
=cut
116
117
sub get_cas_login_url {
118
    my ( $self, $config, $service_url ) = @_;
119
120
    my $cas = Authen::CAS::Client->new( $config->{cas_url} );
121
122
    return $cas->login_url( service => $service_url );
123
}
124
125
=head3 get_cas_logout_url
126
127
    my $logout_url = $client->get_cas_logout_url( $config, $return_url );
128
129
Returns the CAS logout URL for the given return URL.
130
131
=cut
132
133
sub get_cas_logout_url {
134
    my ( $self, $config, $return_url ) = @_;
135
136
    my $cas = Authen::CAS::Client->new( $config->{cas_url} );
137
138
    return $cas->logout_url( url => $return_url );
139
}
140
141
=head1 AUTHOR
142
143
Koha Development Team
144
145
=cut
146
147
1;
(-)a/Koha/Auth/Client/Shibboleth.pm (+114 lines)
Line 0 Link Here
1
package Koha::Auth::Client::Shibboleth;
2
3
# Copyright 2025 Koha Development team
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 <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Logger;
23
24
use base qw(Koha::Auth::Client);
25
26
=head1 NAME
27
28
Koha::Auth::Client::Shibboleth - Shibboleth authentication client
29
30
=head1 API
31
32
=head2 Class methods
33
34
=head3 new
35
36
    my $client = Koha::Auth::Client::Shibboleth->new( { provider => $provider } );
37
38
Constructor. Inherits from L<Koha::Auth::Client>.
39
40
=cut
41
42
=head2 Instance methods
43
44
=head3 get_user
45
46
    my ( $user_data, $patron ) = $client->get_user( { user_identifier => $user_id } );
47
48
Authenticates a user and returns their data and patron object. This method is
49
inherited from the parent class and calls C<_get_data_and_patron> internally.
50
51
=cut
52
53
=head3 _get_data_and_patron
54
55
    my ( $user_data, $patron ) = $client->_get_data_and_patron( { user_identifier => $user_id } );
56
57
Internal method that extracts Shibboleth attributes from the environment and
58
maps them to Koha patron fields according to the provider's configuration.
59
60
This method:
61
62
=over 4
63
64
=item * Extracts attributes from environment variables set by the Shibboleth SP
65
66
=item * Maps Shibboleth attributes to Koha patron fields using the provider's mapping configuration
67
68
=item * Ensures the user identifier is included in the user data
69
70
=item * Attempts to find an existing patron using the configured matchpoint
71
72
=item * Logs authentication attempts for auditing purposes
73
74
=back
75
76
=cut
77
78
sub _get_data_and_patron {
79
    my ( $self, $data ) = @_;
80
81
    my $user_identifier = $data->{user_identifier};
82
    return ( {}, undef ) unless $user_identifier;
83
84
    my $logger = Koha::Logger->get();
85
86
    # Extract Shibboleth attributes from environment
87
    my $mapping   = $self->provider->get_mapping;
88
    my $user_data = {};
89
90
    foreach my $koha_field ( keys %$mapping ) {
91
        my $shib_attribute = $mapping->{$koha_field};
92
        if ( exists $ENV{$shib_attribute} ) {
93
            $user_data->{$koha_field} = $ENV{$shib_attribute};
94
        }
95
    }
96
97
    # Ensure we have the user identifier
98
    $user_data->{userid} = $user_identifier unless $user_data->{userid};
99
100
    $logger->info("Shibboleth authentication for user: $user_identifier");
101
102
    # Find patron using the configured matchpoint
103
    my $patron = $self->_get_patron($user_data);
104
105
    return ( $user_data, $patron );
106
}
107
108
=head1 AUTHOR
109
110
Koha Development Team
111
112
=cut
113
114
1;
(-)a/t/db_dependent/Koha/Auth/Client/CAS.t (+174 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2025 Koha Development team
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 <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Test::More tests => 5;
23
use Test::MockModule;
24
use Test::MockObject;
25
use Test::NoWarnings;
26
27
use t::lib::TestBuilder;
28
use t::lib::Mocks;
29
30
use Koha::Database;
31
use Koha::Auth::Identity::Providers;
32
33
BEGIN {
34
    use_ok('Koha::Auth::Client::CAS');
35
}
36
37
my $schema  = Koha::Database->schema;
38
my $builder = t::lib::TestBuilder->new;
39
40
subtest 'CAS client instantiation' => sub {
41
    plan tests => 2;
42
43
    $schema->storage->txn_begin;
44
45
    my $client = Koha::Auth::Client::CAS->new();
46
    isa_ok( $client, 'Koha::Auth::Client::CAS' );
47
    isa_ok( $client, 'Koha::Auth::Client' );
48
49
    $schema->storage->txn_rollback;
50
};
51
52
subtest '_get_data_and_patron() tests' => sub {
53
    plan tests => 8;
54
55
    $schema->storage->txn_begin;
56
57
    # Create a CAS provider
58
    my $provider = $builder->build_object(
59
        {
60
            class => 'Koha::Auth::Identity::Providers',
61
            value => {
62
                protocol   => 'CAS',
63
                config     => '{"cas_url": "https://cas.example.com/cas"}',
64
                mapping    => '{"userid": "user", "email": "mail"}',
65
                matchpoint => 'userid'
66
            }
67
        }
68
    );
69
70
    # Create a patron to match against
71
    my $patron = $builder->build_object(
72
        {
73
            class => 'Koha::Patrons',
74
            value => { userid => 'testuser' }
75
        }
76
    );
77
78
    my $client = Koha::Auth::Client::CAS->new();
79
80
    # Mock Authen::CAS::Client
81
    my $cas_mock      = Test::MockModule->new('Authen::CAS::Client');
82
    my $response_mock = Test::MockObject->new();
83
84
    # Test successful ticket validation
85
    $response_mock->mock( 'is_success', sub { return 1; } );
86
    $response_mock->mock( 'user',       sub { return 'testuser'; } );
87
88
    my $cas_client_mock = Test::MockObject->new();
89
    $cas_client_mock->mock( 'service_validate', sub { return $response_mock; } );
90
91
    $cas_mock->mock( 'new', sub { return $cas_client_mock; } );
92
93
    # Mock the client methods that depend on provider
94
    my $client_mock = Test::MockModule->new('Koha::Auth::Client::CAS');
95
    $client_mock->mock( 'provider',    sub { return $provider; } );
96
    $client_mock->mock( '_get_patron', sub { return $patron; } );
97
98
    my ( $user_data, $returned_patron ) = $client->_get_data_and_patron(
99
        {
100
            ticket  => 'ST-123456',
101
            service => 'https://koha.example.com/cgi-bin/koha/opac-main.pl'
102
        }
103
    );
104
105
    is( ref($user_data),      'HASH',     'Returns user data as hashref' );
106
    is( $user_data->{userid}, 'testuser', 'User data contains correct userid' );
107
    is( $user_data->{email},  'testuser', 'User data contains email (mapped from userid)' );
108
    isa_ok( $returned_patron, 'Koha::Patron', 'Returns patron object' );
109
    is( $returned_patron->id, $patron->id, 'Returns correct patron' );
110
111
    # Test failed ticket validation
112
    $response_mock->mock( 'is_success', sub { return 0; } );
113
    $response_mock->mock( 'code',       sub { return 'INVALID_TICKET'; } );
114
115
    ( $user_data, $returned_patron ) = $client->_get_data_and_patron(
116
        {
117
            ticket  => 'ST-invalid',
118
            service => 'https://koha.example.com/cgi-bin/koha/opac-main.pl'
119
        }
120
    );
121
122
    is( ref($user_data),         'HASH', 'Returns empty hashref on failure' );
123
    is( scalar keys %$user_data, 0,      'User data is empty on failure' );
124
    is( $returned_patron,        undef,  'Returns undef patron on failure' );
125
126
    $schema->storage->txn_rollback;
127
};
128
129
subtest 'CAS URL generation methods' => sub {
130
    plan tests => 4;
131
132
    $schema->storage->txn_begin;
133
134
    my $client = Koha::Auth::Client::CAS->new();
135
    my $config = { cas_url => 'https://cas.example.com/cas' };
136
137
    # Mock Authen::CAS::Client
138
    my $cas_mock        = Test::MockModule->new('Authen::CAS::Client');
139
    my $cas_client_mock = Test::MockObject->new();
140
141
    $cas_client_mock->mock(
142
        'login_url',
143
        sub {
144
            my ( $self, %params ) = @_;
145
            return 'https://cas.example.com/cas/login?service=' . $params{service};
146
        }
147
    );
148
149
    $cas_client_mock->mock(
150
        'logout_url',
151
        sub {
152
            my ( $self, %params ) = @_;
153
            return 'https://cas.example.com/cas/logout?url=' . $params{url};
154
        }
155
    );
156
157
    $cas_mock->mock( 'new', sub { return $cas_client_mock; } );
158
159
    # Test login URL generation
160
    my $service_url = 'https://koha.example.com/cgi-bin/koha/opac-main.pl';
161
    my $login_url   = $client->get_cas_login_url( $config, $service_url );
162
163
    like( $login_url, qr/https:\/\/cas\.example\.com\/cas\/login/, 'Login URL contains CAS login endpoint' );
164
    like( $login_url, qr/service=/,                                'Login URL contains service parameter' );
165
166
    # Test logout URL generation
167
    my $return_url = 'https://koha.example.com/cgi-bin/koha/opac-main.pl';
168
    my $logout_url = $client->get_cas_logout_url( $config, $return_url );
169
170
    like( $logout_url, qr/https:\/\/cas\.example\.com\/cas\/logout/, 'Logout URL contains CAS logout endpoint' );
171
    like( $logout_url, qr/url=/,                                     'Logout URL contains return URL parameter' );
172
173
    $schema->storage->txn_rollback;
174
};
(-)a/t/db_dependent/Koha/Auth/Client/Shibboleth.t (-1 / +128 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2025 Koha Development team
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 <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Test::More tests => 4;
23
use Test::MockModule;
24
use Test::NoWarnings;
25
26
use t::lib::TestBuilder;
27
use t::lib::Mocks;
28
29
use Koha::Database;
30
use Koha::Auth::Identity::Providers;
31
32
BEGIN {
33
    use_ok('Koha::Auth::Client::Shibboleth');
34
}
35
36
my $schema  = Koha::Database->schema;
37
my $builder = t::lib::TestBuilder->new;
38
39
subtest 'Shibboleth client instantiation' => sub {
40
    plan tests => 2;
41
42
    $schema->storage->txn_begin;
43
44
    my $client = Koha::Auth::Client::Shibboleth->new();
45
    isa_ok( $client, 'Koha::Auth::Client::Shibboleth' );
46
    isa_ok( $client, 'Koha::Auth::Client' );
47
48
    $schema->storage->txn_rollback;
49
};
50
51
subtest '_get_data_and_patron() tests' => sub {
52
    plan tests => 12;
53
54
    $schema->storage->txn_begin;
55
56
    # Create a Shibboleth provider
57
    my $provider = $builder->build_object(
58
        {
59
            class => 'Koha::Auth::Identity::Providers',
60
            value => {
61
                protocol => 'Shibboleth',
62
                config   =>
63
                    '{"sso_url": "https://idp.example.com/sso", "slo_url": "https://idp.example.com/slo", "entity_id": "https://idp.example.com"}',
64
                mapping    => '{"userid": "REMOTE_USER", "email": "mail", "firstname": "givenName", "surname": "sn"}',
65
                matchpoint => 'userid'
66
            }
67
        }
68
    );
69
70
    # Create a patron to match against
71
    my $patron = $builder->build_object(
72
        {
73
            class => 'Koha::Patrons',
74
            value => { userid => 'jdoe' }
75
        }
76
    );
77
78
    my $client = Koha::Auth::Client::Shibboleth->new();
79
80
    # Mock the client methods that depend on provider
81
    my $client_mock = Test::MockModule->new('Koha::Auth::Client::Shibboleth');
82
    $client_mock->mock( 'provider',    sub { return $provider; } );
83
    $client_mock->mock( '_get_patron', sub { return $patron; } );
84
85
    # Test with complete Shibboleth environment variables
86
    {
87
        local %ENV = (
88
            %ENV,
89
            'REMOTE_USER' => 'jdoe',
90
            'mail'        => 'john.doe@example.edu',
91
            'givenName'   => 'John',
92
            'sn'          => 'Doe'
93
        );
94
95
        my ( $user_data, $returned_patron ) = $client->_get_data_and_patron( { user_identifier => 'jdoe' } );
96
97
        is( ref($user_data),         'HASH',                 'Returns user data as hashref' );
98
        is( $user_data->{userid},    'jdoe',                 'User data contains correct userid' );
99
        is( $user_data->{email},     'john.doe@example.edu', 'User data contains correct email' );
100
        is( $user_data->{firstname}, 'John',                 'User data contains correct firstname' );
101
        is( $user_data->{surname},   'Doe',                  'User data contains correct surname' );
102
        isa_ok( $returned_patron, 'Koha::Patron', 'Returns patron object' );
103
        is( $returned_patron->id, $patron->id, 'Returns correct patron' );
104
    }
105
106
    # Test with missing environment variables
107
    {
108
        local %ENV = %ENV;
109
        delete $ENV{'mail'};
110
        delete $ENV{'givenName'};
111
        delete $ENV{'sn'};
112
        $ENV{'REMOTE_USER'} = 'jdoe';
113
114
        my ( $user_data, $returned_patron ) = $client->_get_data_and_patron( { user_identifier => 'jdoe' } );
115
116
        is( $user_data->{userid},    'jdoe', 'User data contains userid even when other attributes missing' );
117
        is( $user_data->{email},     undef,  'Missing environment variables result in undef values' );
118
        is( $user_data->{firstname}, undef,  'Missing environment variables result in undef values' );
119
    }
120
121
    # Test with no user identifier
122
    my ( $user_data, $returned_patron ) = $client->_get_data_and_patron( {} );
123
124
    is( ref($user_data),  'HASH', 'Returns empty hashref when no user identifier' );
125
    is( $returned_patron, undef,  'Returns undef patron when no user identifier' );
126
127
    $schema->storage->txn_rollback;
128
};

Return to bug 40596