|
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 |
- |
|
|