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