Bug 28799: Log when item was lost and now found
[koha.git] / C4 / Auth_with_shibboleth.pm
1 package C4::Auth_with_shibboleth;
2
3 # Copyright 2014 PTFS Europe
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 C4::Context;
23 use Koha::AuthUtils qw( get_script_name );
24 use Koha::Database;
25 use Koha::Patrons;
26 use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
27 use C4::Members::Messaging;
28 use Carp qw( carp );
29 use List::MoreUtils qw( any );
30
31 use Koha::Logger;
32
33 our (@ISA, @EXPORT_OK);
34 BEGIN {
35     require Exporter;
36     @ISA     = qw(Exporter);
37     @EXPORT_OK =
38       qw(shib_ok logout_shib login_shib_url checkpw_shib get_login_shib);
39 }
40
41 # Check that shib config is not malformed
42 sub shib_ok {
43     my $config = _get_shib_config();
44
45     if ($config) {
46         return 1;
47     }
48
49     return 0;
50 }
51
52 # Logout from Shibboleth
53 sub logout_shib {
54     my ($query) = @_;
55     my $uri = _get_uri();
56     my $return = _get_return($query);
57     print $query->redirect( $uri . "/Shibboleth.sso/Logout?return=$return" );
58 }
59
60 # Returns Shibboleth login URL with callback to the requesting URL
61 sub login_shib_url {
62     my ($query) = @_;
63
64     my $target = _get_return($query);
65     my $uri = _get_uri() . "/Shibboleth.sso/Login?target=" . $target;
66
67     return $uri;
68 }
69
70 # Returns shibboleth user login
71 sub get_login_shib {
72
73 # In case of a Shibboleth authentication, we expect a shibboleth user attribute
74 # to contain the login match point of the shibboleth-authenticated user. This match
75 # point is configured in koha-conf.xml
76
77 # Shibboleth attributes are mapped into http environmement variables, so we're getting
78 # the match point of the user this way
79
80     # Get shibboleth config
81     my $config = _get_shib_config();
82
83     my $matchAttribute = $config->{mapping}->{ $config->{matchpoint} }->{is};
84
85     if ( C4::Context->psgi_env ) {
86       return $ENV{"HTTP_".uc($matchAttribute)} || '';
87     } else {
88       return $ENV{$matchAttribute} || '';
89     }
90 }
91
92 # Checks for password correctness
93 # In our case : does the given attribute match one of our users ?
94 sub checkpw_shib {
95
96     my ( $match ) = @_;
97     my $config = _get_shib_config();
98
99     # Does the given shibboleth attribute value ($match) match a valid koha user ?
100     my $borrowers = Koha::Patrons->search( { $config->{matchpoint} => $match } );
101     if ( $borrowers->count > 1 ){
102         # If we have more than 1 borrower the matchpoint is not unique
103         # we cannot know which patron is the correct one, so we should fail
104         Koha::Logger->get->warn("There are several users with $config->{matchpoint} of $match, matchpoints must be unique");
105         return 0;
106     }
107     my $borrower = $borrowers->next;
108     if ( defined($borrower) ) {
109         if ($config->{'sync'}) {
110             _sync($borrower->borrowernumber, $config, $match);
111         }
112         return ( 1, $borrower->get_column('cardnumber'), $borrower->get_column('userid') );
113     }
114
115     if ( $config->{'autocreate'} ) {
116         return _autocreate( $config, $match );
117     } else {
118         # If we reach this point, the user is not a valid koha user
119         Koha::Logger->get->info("There are several users with $config->{matchpoint} of $match, matchpoints must be unique");
120         return 0;
121     }
122 }
123
124 sub _autocreate {
125     my ( $config, $match ) = @_;
126
127     my %borrower = ( $config->{matchpoint} => $match );
128
129     while ( my ( $key, $entry ) = each %{$config->{'mapping'}} ) {
130         if ( C4::Context->psgi_env ) {
131             $borrower{$key} = ( $entry->{'is'} && $ENV{"HTTP_" . uc($entry->{'is'}) } ) || $entry->{'content'} || '';
132         } else {
133             $borrower{$key} = ( $entry->{'is'} && $ENV{ $entry->{'is'} } ) || $entry->{'content'} || '';
134         }
135     }
136
137     my $patron = Koha::Patron->new( \%borrower )->store;
138     C4::Members::Messaging::SetMessagingPreferencesFromDefaults(
139         {
140             borrowernumber => $patron->borrowernumber,
141             categorycode   => $patron->categorycode
142         }
143     );
144
145     # Send welcome email if enabled
146     if ( $config->{welcome} ) {
147         my $emailaddr = $patron->notice_email_address;
148
149         # if we manage to find a valid email address, send notice
150         if ($emailaddr) {
151             my $letter = C4::Letters::GetPreparedLetter(
152                 module      => 'members',
153                 letter_code => 'WELCOME',
154                 branchcode  => $patron->branchcode,
155                 ,
156                 lang   => $patron->lang || 'default',
157                 tables => {
158                     'branches'  => $patron->branchcode,
159                     'borrowers' => $patron->borrowernumber,
160                 },
161                 want_librarian => 1,
162             ) or return;
163
164             my $message_id = C4::Letters::EnqueueLetter(
165                 {
166                     letter                 => $letter,
167                     borrowernumber         => $patron->id,
168                     to_address             => $emailaddr,
169                     message_transport_type => 'email'
170                 }
171             );
172             C4::Letters::SendQueuedMessages( { message_id => $message_id } );
173         }
174     }
175     return ( 1, $patron->cardnumber, $patron->userid );
176 }
177
178 sub _sync {
179     my ($borrowernumber, $config, $match ) = @_;
180     my %borrower;
181     $borrower{'borrowernumber'} = $borrowernumber;
182     while ( my ( $key, $entry ) = each %{$config->{'mapping'}} ) {
183         if ( C4::Context->psgi_env ) {
184             $borrower{$key} = ( $entry->{'is'} && $ENV{"HTTP_" . uc($entry->{'is'}) } ) || $entry->{'content'} || '';
185         } else {
186             $borrower{$key} = ( $entry->{'is'} && $ENV{ $entry->{'is'} } ) || $entry->{'content'} || '';
187         }
188     }
189     my $patron = Koha::Patrons->find( $borrowernumber );
190     $patron->set(\%borrower)->store;
191 }
192
193 sub _get_uri {
194
195     my $protocol = "https://";
196     my $interface = C4::Context->interface;
197
198     my $uri =
199       $interface eq 'intranet'
200       ? C4::Context->preference('staffClientBaseURL')
201       : C4::Context->preference('OPACBaseURL');
202
203     $uri or Koha::Logger->get->warn("Syspref staffClientBaseURL or OPACBaseURL not set!"); # FIXME We should die here
204
205     $uri ||= "";
206
207     if ($uri =~ /(.*):\/\/(.*)/) {
208         my $oldprotocol = $1;
209         if ($oldprotocol ne 'https') {
210             Koha::Logger->get->warn('Shibboleth requires OPACBaseURL/staffClientBaseURL to use the https protocol!');
211         }
212         $uri = $2;
213     }
214     my $return = $protocol . $uri;
215     return $return;
216 }
217
218 sub _get_return {
219     my ($query) = @_;
220
221     my $uri_base_part = _get_uri() . get_script_name();
222
223     my $uri_params_part = '';
224     foreach my $param ( sort $query->url_param() ) {
225         # url_param() always returns parameters that were deleted by delete()
226         # This additional check ensure that parameter was not deleted.
227         my $uriPiece = $query->param($param);
228         if ($uriPiece) {
229             $uri_params_part .= '&' if $uri_params_part;
230             $uri_params_part .= $param . '=';
231             $uri_params_part .= $uriPiece;
232         }
233     }
234     $uri_base_part .= '%3F' if $uri_params_part;
235
236     return $uri_base_part . URI::Escape::uri_escape_utf8($uri_params_part);
237 }
238
239 sub _get_shib_config {
240     my $config = C4::Context->config('shibboleth');
241
242     if ( !$config ) {
243         Koha::Logger->get->warn('shibboleth config not defined');
244         return 0;
245     }
246
247     if ( $config->{matchpoint}
248         && defined( $config->{mapping}->{ $config->{matchpoint} }->{is} ) )
249     {
250         my $logger = Koha::Logger->get;
251         $logger->debug("koha borrower field to match: " . $config->{matchpoint});
252         $logger->debug("shibboleth attribute to match: " . $config->{mapping}->{ $config->{matchpoint} }->{is});
253         return $config;
254     }
255     else {
256         if ( !$config->{matchpoint} ) {
257             carp 'shibboleth matchpoint not defined';
258         }
259         else {
260             carp 'shibboleth matchpoint not mapped';
261         }
262         return 0;
263     }
264 }
265
266 1;
267 __END__
268
269 =head1 NAME
270
271 C4::Auth_with_shibboleth
272
273 =head1 SYNOPSIS
274
275 use C4::Auth_with_shibboleth;
276
277 =head1 DESCRIPTION
278
279 This module is specific to Shibboleth authentication in koha and relies heavily upon the native shibboleth service provider package in your operating system.
280
281 =head1 CONFIGURATION
282
283 To use this type of authentication these additional packages are required:
284
285 =over
286
287 =item *
288
289 libapache2-mod-shib2
290
291 =item *
292
293 libshibsp5:amd64
294
295 =item *
296
297 shibboleth-sp2-schemas
298
299 =back
300
301 We let the native shibboleth service provider packages handle all the complexities of shibboleth negotiation for us, and configuring this is beyond the scope of this documentation.
302
303 But to sum up, to get shibboleth working in koha, as a minimum you will need to:
304
305 =over
306
307 =item 1.
308
309 Create some metadata for your koha instance (if you're in a single instance setup then the default metadata available at https://youraddress.com/Shibboleth.sso/Metadata should be adequate)
310
311 =item 2.
312
313 Swap metadata with your Identidy Provider (IdP)
314
315 =item 3.
316
317 Map their attributes to what you want to see in koha
318
319 =item 4.
320
321 Tell apache that we wish to allow koha to authenticate via shibboleth.
322
323 This is as simple as adding the below to your virtualhost config (for CGI running):
324
325  <Location />
326    AuthType shibboleth
327    Require shibboleth
328  </Location>
329
330 Or (for Plack running):
331
332  <Location />
333    AuthType shibboleth
334    Require shibboleth
335    ShibUseEnvironment Off
336    ShibUseHeaders On
337  </Location>
338
339 IMPORTANT: Please note, if you are running in the plack configuration you should consult https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSpoofChecking for security advice regarding header spoof checking settings. (See also bug 17776 on Bugzilla about enabling ShibUseHeaders.)
340
341 =item 5.
342
343 Configure koha to listen for shibboleth environment variables.
344
345 This is as simple as enabling B<useshibboleth> in koha-conf.xml:
346
347  <useshibboleth>1</useshibboleth>
348
349 =item 6.
350
351 Map shibboleth attributes to koha fields, and configure authentication match point in koha-conf.xml.
352
353  <shibboleth>
354    <matchpoint>userid</matchpoint> <!-- koha borrower field to match upon -->
355    <mapping>
356      <userid is="eduPersonID"></userid> <!-- koha borrower field to shibboleth attribute mapping -->
357    </mapping>
358  </shibboleth>
359
360 Note: The minimum you need here is a <matchpoint> block, containing a valid column name from the koha borrowers table, and a <mapping> block containing a relation between the chosen matchpoint and the shibboleth attribute name.
361
362 =back
363
364 It should be as simple as that; you should now be able to login via shibboleth in the opac.
365
366 If you need more help configuring your B<S>ervice B<P>rovider to authenticate against a chosen B<Id>entity B<P>rovider then it might be worth taking a look at the community wiki L<page|http://wiki.koha-community.org/wiki/Shibboleth_Configuration>
367
368 =head1 FUNCTIONS
369
370 =head2 logout_shib
371
372 Sends a logout signal to the native shibboleth service provider and then logs out of koha.  Depending upon the native service provider configuration and identity provider capabilities this may or may not perform a single sign out action.
373
374   logout_shib($query);
375
376 =head2 login_shib_url
377
378 Given a query, this will return a shibboleth login url with return code to page with given given query.
379
380   my $shibLoginURL = login_shib_url($query);
381
382 =head2 get_login_shib
383
384 Returns the shibboleth login attribute should it be found present in the http session
385
386   my $shib_login = get_login_shib();
387
388 =head2 checkpw_shib
389
390 Given a shib_login attribute, this routine checks for a matching local user and if found returns true, their cardnumber and their userid.  If a match is not found, then this returns false.
391
392   my ( $retval, $retcard, $retuserid ) = C4::Auth_with_shibboleth::checkpw_shib( $shib_login );
393
394 =head2 _get_uri
395
396   _get_uri();
397
398 A sugar function to that simply returns the current page URI with appropriate protocol attached
399
400 This routine is NOT exported
401
402 =head2 _get_shib_config
403
404   my $config = _get_shib_config();
405
406 A sugar function that checks for a valid shibboleth configuration, and if found returns a hashref of it's contents
407
408 This routine is NOT exported
409
410 =head2 _autocreate
411
412   my ( $retval, $retcard, $retuserid ) = _autocreate( $config, $match );
413
414 Given a shibboleth attribute reference and a userid this internal routine will add the given user to Koha and return their user credentials.
415
416 This routine is NOT exported
417
418 =head1 SEE ALSO
419
420 =cut