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

(-)a/C4/Auth_with_cas.pm (-160 / +137 lines)
Lines 29-282 use C4::Context; Link Here
29
use Koha::AuthUtils qw( get_script_name );
29
use Koha::AuthUtils qw( get_script_name );
30
use Authen::CAS::Client;
30
use Authen::CAS::Client;
31
use CGI qw ( -utf8 );
31
use CGI qw ( -utf8 );
32
use YAML::XS;
33
use URI::Escape;
32
use URI::Escape;
34
33
34
use Koha::Auth::Identity::Providers;
35
use Koha::Logger;
35
use Koha::Logger;
36
36
37
my $defaultcasserver;
38
my $casservers;
39
my $yamlauthfile = C4::Context->config('intranetdir') . "/C4/Auth_cas_servers.yaml";
40
41
# If there's a configuration for multiple cas servers, then we get it
42
if ( multipleAuth() ) {
43
    ( $defaultcasserver, $casservers ) = YAML::XS::LoadFile($yamlauthfile);
44
    $defaultcasserver = $defaultcasserver->{'default'};
45
} else {
46
47
    # Else, we fall back to casServerUrl syspref
48
    $defaultcasserver = 'default';
49
    $casservers       = { 'default' => C4::Context->preference('casServerUrl') };
50
}
51
52
=head1 Subroutines
37
=head1 Subroutines
53
38
54
=cut
39
=cut
55
40
56
# Is there a configuration file for multiple cas servers?
57
58
=head2 multipleAuth
41
=head2 multipleAuth
59
42
60
Missing POD for multipleAuth.
43
Returns true when more than one enabled CAS identity provider is configured.
61
44
62
=cut
45
=cut
63
46
64
sub multipleAuth {
47
sub multipleAuth {
65
    return ( -e qq($yamlauthfile) );
48
    return Koha::Auth::Identity::Providers->search( { protocol => 'CAS', enabled => 1 } )->count > 1;
66
}
49
}
67
50
68
# Returns configured CAS servers' list if multiple authentication is enabled
69
70
=head2 getMultipleAuth
51
=head2 getMultipleAuth
71
52
72
Missing POD for getMultipleAuth.
53
Returns a hashref of C<< { provider_code => server_url } >> for all enabled
54
CAS identity providers.
73
55
74
=cut
56
=cut
75
57
76
sub getMultipleAuth {
58
sub getMultipleAuth {
77
    return $casservers;
59
    my %servers;
60
    my $providers = Koha::Auth::Identity::Providers->search( { protocol => 'CAS', enabled => 1 } );
61
    while ( my $p = $providers->next ) {
62
        $servers{ $p->code } = $p->get_config->{server_url};
63
    }
64
    return \%servers;
78
}
65
}
79
66
80
# Logout from CAS
81
82
=head2 logout_cas
67
=head2 logout_cas
83
68
84
Missing POD for logout_cas.
69
Redirect the browser to the CAS server logout URL.  C<$provider_code> is the
70
identity provider code stored in the session; if omitted the first enabled
71
CAS provider is used.
85
72
86
=cut
73
=cut
87
74
88
sub logout_cas {
75
sub logout_cas {
89
    my ( $query, $type ) = @_;
76
    my ( $query, $type, $provider_code ) = @_;
90
    my ( $cas,   $uri )  = _get_cas_and_service( $query, undef, $type );
77
    my $provider = _get_cas_provider($provider_code);
78
    return unless $provider;
79
80
    my $config = $provider->get_config;
81
    my $uri    = _url_with_get_params( $query, $type );
91
82
92
    # We don't want to keep triggering a logout, if we got here,
83
    # We don't want to keep triggering a logout, if we got here,
93
    # the borrower is already logged out of Koha
84
    # the borrower is already logged out of Koha
94
    $uri =~ s/\?logout\.x=1//;
85
    $uri =~ s/\?logout\.x=1//;
95
86
87
    my $cas        = Authen::CAS::Client->new( $config->{server_url} );
96
    my $logout_url = $cas->logout_url( url => $uri );
88
    my $logout_url = $cas->logout_url( url => $uri );
97
    $logout_url =~ s/url=/service=/
89
    my $version    = $config->{version} || '2';
98
        if C4::Context->preference('casServerVersion') eq '3';
90
    $logout_url =~ s/url=/service=/ if $version eq '3';
99
91
100
    print $query->redirect($logout_url);
92
    print $query->redirect($logout_url);
101
}
93
}
102
94
103
# Login to CAS
104
105
=head2 login_cas
95
=head2 login_cas
106
96
107
Missing POD for login_cas.
97
Redirect the browser to the CAS login URL.
108
98
109
=cut
99
=cut
110
100
111
sub login_cas {
101
sub login_cas {
112
    my ( $query, $type ) = @_;
102
    my ( $query, $type ) = @_;
113
    my ( $cas,   $uri )  = _get_cas_and_service( $query, undef, $type );
103
    my $provider = _get_cas_provider( $query->param('cas_provider') );
104
    return unless $provider;
105
    my $uri = _url_with_get_params_for_provider( $query, $type, $provider );
106
    my $cas = Authen::CAS::Client->new( $provider->get_config->{server_url} );
114
    print $query->redirect( $cas->login_url($uri) );
107
    print $query->redirect( $cas->login_url($uri) );
115
}
108
}
116
109
117
# Returns CAS login URL with callback to the requesting URL
118
119
=head2 login_cas_url
110
=head2 login_cas_url
120
111
121
Missing POD for login_cas_url.
112
Returns the CAS login URL for the provider identified by C<$key> (a provider
113
code), or the first enabled CAS provider when C<$key> is not given.
114
115
The provider code is encoded into the returned service URL so that it is
116
available when CAS redirects back with a ticket.
122
117
123
=cut
118
=cut
124
119
125
sub login_cas_url {
120
sub login_cas_url {
126
    my ( $query, $key, $type ) = @_;
121
    my ( $query, $key, $type ) = @_;
127
    my ( $cas, $uri ) = _get_cas_and_service( $query, $key, $type );
122
    my $provider = _get_cas_provider($key);
123
    return undef unless $provider;
124
    my $uri = _url_with_get_params_for_provider( $query, $type, $provider );
125
    my $cas = Authen::CAS::Client->new( $provider->get_config->{server_url} );
128
    return $cas->login_url($uri);
126
    return $cas->login_url($uri);
129
}
127
}
130
128
131
# Checks for password correctness
132
# In our case : is there a ticket, is it valid and does it match one of our users ?
133
134
=head2 checkpw_cas
129
=head2 checkpw_cas
135
130
136
Missing POD for checkpw_cas.
131
Validates a CAS service ticket.  The identity provider is identified by the
132
C<cas_provider> CGI parameter that CAS echoed back in the redirect URL.
133
134
Returns C<(1, cardnumber, userid, ticket, patron)> on success or C<0> on
135
failure.
137
136
138
=cut
137
=cut
139
138
140
sub checkpw_cas {
139
sub checkpw_cas {
141
    my ( $ticket, $query, $type ) = @_;
140
    my ( $ticket, $query, $type ) = @_;
142
    my $retnumber;
141
    return 0 unless $ticket;
143
    my ( $cas, $uri ) = _get_cas_and_service( $query, undef, $type );
144
145
    # If we got a ticket
146
    if ($ticket) {
147
148
        # We try to validate it
149
        my $val = $cas->service_validate( $uri, $ticket );
150
151
        # If it's valid
152
        if ( $val->is_success() ) {
153
154
            my $userid = $val->user();
155
142
156
            # we should store the CAS ticekt too, we need this for single logout https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#233-single-logout
143
    my $provider = _get_cas_provider( $query->param('cas_provider') );
144
    return 0 unless $provider;
157
145
158
            # Does it match one of our users ?
146
    my $config = $provider->get_config;
159
            my $dbh    = C4::Context->dbh;
147
    my $uri    = _url_with_get_params_for_provider( $query, $type, $provider );
160
            my $patron = Koha::Patrons->find_by_identifier($userid);
148
    my $cas    = Authen::CAS::Client->new( $config->{server_url} );
161
            if ($patron) {
162
                return ( 1, $patron->cardnumber, $patron->userid, $ticket, $patron );
163
            }
164
149
165
            # If we reach this point, then the user is a valid CAS user, but not a Koha user
150
    my $val = $cas->service_validate( $uri, $ticket );
166
            Koha::Logger->get->info("User $userid is not a valid Koha user");
167
151
168
        } else {
152
    if ( $val->is_success() ) {
169
            my $logger = Koha::Logger->get;
153
        my $userid = $val->user();
170
            $logger->debug("Problem when validating ticket : $ticket");
154
        my $patron = Koha::Patrons->find_by_identifier($userid);
171
            $logger->debug( "Authen::CAS::Client::Response::Error: " . $val->error() )     if $val->is_error();
155
        if ($patron) {
172
            $logger->debug( "Authen::CAS::Client::Response::Failure: " . $val->message() ) if $val->is_failure();
156
            return ( 1, $patron->cardnumber, $patron->userid, $ticket, $patron );
173
            $logger->debug( Data::Dumper::Dumper($@) ) if $val->is_error() or $val->is_failure();
174
            return 0;
175
        }
157
        }
158
        Koha::Logger->get->info( "CAS provider '" . $provider->code . "': user $userid is not a valid Koha user" );
159
    } else {
160
        my $logger = Koha::Logger->get;
161
        $logger->debug( "CAS provider '" . $provider->code . "': problem validating ticket: $ticket" );
162
        $logger->debug( "Authen::CAS::Client::Response::Error: " . $val->error() )     if $val->is_error();
163
        $logger->debug( "Authen::CAS::Client::Response::Failure: " . $val->message() ) if $val->is_failure();
176
    }
164
    }
165
177
    return 0;
166
    return 0;
178
}
167
}
179
168
180
# Proxy CAS auth
181
182
=head2 check_api_auth_cas
169
=head2 check_api_auth_cas
183
170
184
Missing POD for check_api_auth_cas.
171
Proxy CAS ticket validation used by the API auth path.
185
172
186
=cut
173
=cut
187
174
188
sub check_api_auth_cas {
175
sub check_api_auth_cas {
189
    my ( $PT, $query, $type ) = @_;
176
    my ( $PT, $query, $type ) = @_;
190
    my $retnumber;
177
    return 0 unless $PT;
191
    my ( $cas, $uri ) = _get_cas_and_service( $query, undef, $type );
192
193
    # If we have a Proxy Ticket
194
    if ($PT) {
195
        my $r = $cas->proxy_validate( $uri, $PT );
196
197
        # If the PT is valid
198
        if ( $r->is_success ) {
199
200
            # We've got a username !
201
            my $userid = $r->user;
202
203
            # we should store the CAS ticket too, we need this for single logout https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#233-single-logout
204
205
            # Does it match one of our users ?
206
            my $dbh = C4::Context->dbh;
207
            my $sth = $dbh->prepare("select cardnumber from borrowers where userid=?");
208
            $sth->execute($userid);
209
            if ( $sth->rows ) {
210
                $retnumber = $sth->fetchrow;
211
                return ( 1, $retnumber, $userid, $PT );
212
            }
213
            $sth = $dbh->prepare("select userid from borrowers where cardnumber=?");
214
            return $r->user;
215
            $sth->execute($userid);
216
            if ( $sth->rows ) {
217
                $retnumber = $sth->fetchrow;
218
                return ( 1, $retnumber, $userid, $PT );
219
            }
220
221
            # If we reach this point, then the user is a valid CAS user, but not a Koha user
222
            Koha::Logger->get->info("User $userid is not a valid Koha user");
223
224
        } else {
225
            Koha::Logger->get->debug("Proxy Ticket authentication failed");
226
            return 0;
227
        }
228
    }
229
    return 0;
230
}
231
232
# Get CAS handler and service URI
233
sub _get_cas_and_service {
234
    my $query = shift;
235
    my $key   = shift;    # optional
236
    my $type  = shift;
237
238
    my $uri = _url_with_get_params( $query, $type );
239
240
    my $casparam = $defaultcasserver;
241
    $casparam = $query->param('cas') if defined $query->param('cas');
242
    $casparam = $key                 if defined $key;
243
    my $cas = Authen::CAS::Client->new( $casservers->{$casparam} );
244
245
    return ( $cas, $uri );
246
}
247
178
248
# Get the current URL with parameters contained directly into URL (GET params)
179
    my $provider = _get_cas_provider( $query->param('cas_provider') );
249
# This method replaces $query->url() which will give both GET and POST params
180
    return 0 unless $provider;
250
sub _url_with_get_params {
251
    my $query = shift;
252
    my $type  = shift;
253
181
254
    my $uri_base_part =
182
    my $config = $provider->get_config;
255
        ( $type eq 'opac' )
183
    my $uri    = _url_with_get_params_for_provider( $query, $type, $provider );
256
        ? C4::Context->preference('OPACBaseURL')
184
    my $cas    = Authen::CAS::Client->new( $config->{server_url} );
257
        : C4::Context->preference('staffClientBaseURL');
258
    $uri_base_part .= get_script_name();
259
185
260
    my $uri_params_part = '';
186
    my $r = $cas->proxy_validate( $uri, $PT );
261
    foreach my $param ( $query->url_param() ) {
262
187
263
        # url_param() always returns parameters that were deleted by delete()
188
    if ( $r->is_success ) {
264
        # This additional check ensure that parameter was not deleted.
189
        my $userid = $r->user;
265
        my $uriPiece = $query->param($param);
190
        my $patron = Koha::Patrons->find_by_identifier($userid);
266
        if ($uriPiece) {
191
        if ($patron) {
267
            $uri_params_part .= '&' if $uri_params_part;
192
            return ( 1, $patron->cardnumber, $userid, $PT );
268
            $uri_params_part .= $param . '=';
269
            $uri_params_part .= URI::Escape::uri_escape($uriPiece);
270
        }
193
        }
194
        Koha::Logger->get->info("CAS proxy user $userid is not a valid Koha user");
195
    } else {
196
        Koha::Logger->get->debug("CAS proxy ticket authentication failed");
271
    }
197
    }
272
    $uri_base_part .= '?' if $uri_params_part;
273
198
274
    return $uri_base_part . $uri_params_part;
199
    return 0;
275
}
200
}
276
201
277
=head2 logout_if_required
202
=head2 logout_if_required
278
203
279
    If using CAS, this subroutine will trigger single-signout of the CAS server.
204
If using CAS, this subroutine will trigger single-signout of the CAS server.
280
205
281
=cut
206
=cut
282
207
Lines 320-342 sub delete_cas_session { Link Here
320
    }
245
    }
321
}
246
}
322
247
248
# Returns an enabled CAS identity provider by code, or the first enabled one
249
# when $code is not given.
250
sub _get_cas_provider {
251
    my ($code) = @_;
252
    if ($code) {
253
        return Koha::Auth::Identity::Providers->search( { code => $code, protocol => 'CAS', enabled => 1 } )->next;
254
    }
255
    return Koha::Auth::Identity::Providers->search(
256
        { protocol => 'CAS', enabled => 1 },
257
        { order_by => 'identity_provider_id' }
258
    )->next;
259
}
260
261
# Build the service URL for $provider, appending cas_provider=<code> so that
262
# CAS echoes it back in the redirect and we can identify the provider on return.
263
sub _url_with_get_params_for_provider {
264
    my ( $query, $type, $provider ) = @_;
265
    my $uri = _url_with_get_params( $query, $type );
266
    my $sep = ( $uri =~ /\?/ ) ? '&' : '?';
267
    return $uri . $sep . 'cas_provider=' . URI::Escape::uri_escape( $provider->code );
268
}
269
270
# Get the current URL with parameters contained directly in the URL (GET params).
271
# This method replaces $query->url() which gives both GET and POST params.
272
sub _url_with_get_params {
273
    my $query = shift;
274
    my $type  = shift;
275
276
    my $uri_base_part =
277
        ( $type eq 'opac' )
278
        ? C4::Context->preference('OPACBaseURL')
279
        : C4::Context->preference('staffClientBaseURL');
280
    $uri_base_part .= get_script_name();
281
282
    my $uri_params_part = '';
283
    foreach my $param ( $query->url_param() ) {
284
285
        # url_param() always returns parameters that were deleted by delete()
286
        # This additional check ensures that parameter was not deleted.
287
        my $uriPiece = $query->param($param);
288
        if ($uriPiece) {
289
            $uri_params_part .= '&' if $uri_params_part;
290
            $uri_params_part .= $param . '=';
291
            $uri_params_part .= URI::Escape::uri_escape($uriPiece);
292
        }
293
    }
294
    $uri_base_part .= '?' if $uri_params_part;
295
296
    return $uri_base_part . $uri_params_part;
297
}
298
323
1;
299
1;
324
__END__
300
__END__
325
301
326
=head1 NAME
302
=head1 NAME
327
303
328
C4::Auth - Authenticates Koha users
304
C4::Auth_with_cas - CAS authentication for Koha via Identity Providers
329
305
330
=head1 SYNOPSIS
306
=head1 SYNOPSIS
331
307
332
  use C4::Auth_with_cas;
308
  use C4::Auth_with_cas;
333
309
334
=cut
310
=head1 DESCRIPTION
335
311
336
=head1 SEE ALSO
312
CAS authentication is configured through the Identity Providers admin interface
313
(Admin > Identity providers).  Create a provider with protocol C<CAS> and set
314
C<server_url> (and optionally C<version>) in the configuration.
337
315
338
CGI(3)
316
=head1 SEE ALSO
339
317
340
Authen::CAS::Client
318
CGI(3), Authen::CAS::Client
341
319
342
=cut
320
=cut
343
- 

Return to bug 40596