Line 0
Link Here
|
0 |
- |
1 |
#!/usr/bin/perl |
|
|
2 |
|
3 |
# Copyright Prosentient Systems 2014 |
4 |
# |
5 |
# This file is part of Koha. |
6 |
# |
7 |
# Koha is free software; you can redistribute it and/or modify it under the |
8 |
# terms of the GNU General Public License as published by the Free Software |
9 |
# Foundation; either version 3 of the License, or (at your option) any later |
10 |
# version. |
11 |
# |
12 |
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY |
13 |
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
14 |
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
15 |
# |
16 |
# You should have received a copy of the GNU General Public License along |
17 |
# with Koha; if not, write to the Free Software Foundation, Inc., |
18 |
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
19 |
|
20 |
use CGI; |
21 |
use Modern::Perl; |
22 |
use URI::Escape; |
23 |
use Data::Dumper; |
24 |
|
25 |
use C4::Auth; |
26 |
use C4::Context; |
27 |
use C4::Members; |
28 |
use Koha::Prosentient::Auth::OpenIDConnect; |
29 |
use Koha::Prosentient::Auth::OpenIDConnect::UserInfo; |
30 |
use Koha::Prosentient::Borrowers; |
31 |
|
32 |
=head1 DESCRIPTION |
33 |
|
34 |
This script adds an authentication option using OpenID Connect 1.0. |
35 |
|
36 |
It relies on having the following elements set in koha-conf.xml: |
37 |
|
38 |
<prosentient> |
39 |
<OpenIdConnect> |
40 |
<provider id="foo"> |
41 |
<hack_username></hack_username> |
42 |
<issuer>https://xxxxxxxxxxxxxxx/</issuer> |
43 |
<client_id>xxxxxxxxxxxxxxxxxxxxxxx</client_id> |
44 |
<client_secret></client_secret> |
45 |
<auth_url>https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</auth_url> |
46 |
<token_url>https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</token_url> |
47 |
<userinfo_url>https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</userinfo_url> |
48 |
<response_type>code</response_type> |
49 |
<scope>openid profile email</scope> |
50 |
<redirect_url>http://xxxxxxxxxxxxxxxxx/cgi-bin/koha/svc/login_openidc</redirect_url> |
51 |
<!-- Supply default categorycode and branchcode for new patrons from server --> |
52 |
<categorycode>XXXXX</categorycode> |
53 |
<branchcode>XXXXX</branchcode> |
54 |
<mapping> |
55 |
<!-- mapping keys match Koha::Prosentient::Auth::OpenIDConnect::UserInfo accessors --> |
56 |
<firstname>given_name</firstname> |
57 |
<surname>family_name</surname> |
58 |
<email>email</email> |
59 |
<phone>phone_number</phone> |
60 |
<dateofbirth>birthdate</dateofbirth> |
61 |
<address>address.street_address</address> |
62 |
<city>address.locality</city> |
63 |
<state>address.region</state> |
64 |
<zipcode>address.postal_code</zipcode> |
65 |
<country>address.country</country> |
66 |
<verified_email>email_verified</verified_email> |
67 |
<nickname></nickname> |
68 |
</mapping> |
69 |
</provider> |
70 |
</OpenIdConnect> |
71 |
</prosentient> |
72 |
|
73 |
=cut |
74 |
|
75 |
################################################################################################## |
76 |
#DECLARE VARIABLES |
77 |
|
78 |
my ($scope, $client_id, $response_type, $issuer, $redirect_uri, $state, $auth_url, $token_url, $userinfo_url); #Send to OpenID Connect server |
79 |
my ($http_username, $http_password, $client_secret); #Send to OpenID Connect server |
80 |
my ($categorycode, $branchcode, $mapping); #For processing UserInfo && passing data to Koha APIs |
81 |
my ($errors, $koha_user, $koha_identifier, $return); #Send to Auth.pm |
82 |
my ($openid); |
83 |
my $client_authentication_method = "client_secret_basic"; |
84 |
my $bearer_access_token_method = "header"; |
85 |
|
86 |
my $query = CGI->new(); |
87 |
|
88 |
#Restore or create new Koha session... |
89 |
my $koha_sessionID = $query->cookie('CGISESSID') // ''; |
90 |
my $koha_session = C4::Auth::get_session($koha_sessionID); |
91 |
|
92 |
#We need a session cookie so that we know which endpoints to use... |
93 |
my $session_cookie = $query->cookie( |
94 |
-name => 'CGISESSID', |
95 |
-value => $koha_session->id, |
96 |
-HttpOnly => 1 |
97 |
); |
98 |
|
99 |
my $cgi_pid = $query->param("pid"); |
100 |
my $oidc_login_pid = $koha_session->param("oidc_login_pid"); |
101 |
my $provider_id = $cgi_pid ? $cgi_pid : $oidc_login_pid; |
102 |
|
103 |
my $code = $query->param("code"); #Authorization code from OpenID Connect server |
104 |
my $server_error = $query->param("error"); |
105 |
my $server_error_description = $query->param("error_description"); |
106 |
|
107 |
if ($provider_id){ |
108 |
my $context = new C4::Context; |
109 |
my $providers = $context->{prosentient}->{OpenIdConnect}->{provider}; |
110 |
if ( ($providers->{id}) && ($providers->{id} eq $provider_id) ){ |
111 |
#NOTE: This scenario happens when there is only one provider in koha-conf.xml |
112 |
$openid = $providers; |
113 |
} elsif ( $context->{prosentient}->{OpenIdConnect}->{provider}->{$provider_id} ) { |
114 |
#NOTE: This scenario happens when there is more than one provider in koha-conf.xml |
115 |
$openid = $context->{prosentient}->{OpenIdConnect}->{provider}->{$provider_id}; |
116 |
} |
117 |
} |
118 |
|
119 |
if ($openid){ |
120 |
if ($provider_id){ |
121 |
#Store the provider id in the user's Koha session, so that we know which provider to use to process the Authentication Response! |
122 |
#NOTE: Alternatively, we could include the provider_id in the redirect_url that we issue to the Authentication endpoint... |
123 |
$koha_session->param("oidc_login_pid",$provider_id); |
124 |
} |
125 |
|
126 |
#FIXME: This $hack_username shouldn't be necessary in a standard OpenID Connect implementation... it's an unfortunate requirement for one server's non-standard implementation. |
127 |
$http_username = $openid->{hack_username} ? $openid->{hack_username} : $openid->{client_id}; |
128 |
$http_password = $openid->{client_secret}; |
129 |
|
130 |
$client_id = $openid->{client_id}; |
131 |
$client_secret = $openid->{client_secret}; |
132 |
$client_authentication_method = $openid->{client_authentication_method} if $openid->{client_authentication_method}; |
133 |
$bearer_access_token_method = $openid->{bearer_access_token_method} if $openid->{bearer_access_token_method}; |
134 |
|
135 |
$issuer = $openid->{issuer}; |
136 |
|
137 |
$auth_url = $openid->{auth_url}; |
138 |
$token_url = $openid->{token_url}; |
139 |
$userinfo_url = $openid->{userinfo_url}; |
140 |
$scope = uri_escape($openid->{scope}); #escape scope |
141 |
$response_type = uri_escape($openid->{response_type}); #escape response_type |
142 |
$redirect_uri = uri_escape($openid->{redirect_url}); #escape uri (including reserved characters) |
143 |
|
144 |
$categorycode = $openid->{categorycode}; #Categorycode for borrowers added via OpenID Connect... |
145 |
$branchcode = $openid->{branchcode}; #Branchcode for bororwers added via OpenID Connect... |
146 |
$mapping = $openid->{mapping}; #Map canonical hash keys to the hash keys returned in a UserInfo response |
147 |
} |
148 |
|
149 |
my $insecure = 0; #FIXME: Change this to 1 if you want to authenticate using unverified email addresses |
150 |
my $debugging = 0; #NOTE: This should be 0 in production |
151 |
|
152 |
################################################################################################## |
153 |
#SCRIPT ACTION |
154 |
|
155 |
unless ( $cgi_pid || $code || $server_error ){ |
156 |
print $query->redirect(-uri => "/cgi-bin/koha/opac-user.pl"); |
157 |
exit; |
158 |
} |
159 |
|
160 |
|
161 |
|
162 |
#Create an OpenIDC object using values we set in koha-conf.xml |
163 |
my $OpenIDC = Koha::Prosentient::Auth::OpenIDConnect->new({ |
164 |
debugging => $debugging, |
165 |
auth_url => $auth_url, |
166 |
token_url => $token_url, |
167 |
userinfo_url => $userinfo_url, |
168 |
response_type => $response_type, |
169 |
scope => $scope, |
170 |
client_id => $client_id, |
171 |
client_secret => $client_secret, |
172 |
client_authentication_method => $client_authentication_method, |
173 |
bearer_access_token_method => $bearer_access_token_method, |
174 |
redirect_uri => $redirect_uri, |
175 |
issuer => $issuer, |
176 |
http_username => $http_username, |
177 |
http_password => $http_password, |
178 |
state => $state, |
179 |
}); |
180 |
|
181 |
if ($OpenIDC->auth_url && $OpenIDC->response_type && $OpenIDC->scope && $OpenIDC->client_id && $OpenIDC->redirect_uri && $categorycode && $branchcode && $OpenIDC->issuer){ |
182 |
if (!$code && $cgi_pid){ |
183 |
#Step One: Send an Authentication Request |
184 |
my $authentication_request = $OpenIDC->GenerateAuthenticationRequest(); |
185 |
if ($authentication_request){ |
186 |
print $query->redirect(-uri => $authentication_request, -cookie => $session_cookie); |
187 |
#NOTE: At this stage, the authorization server "MUST" ask the user if they're OK sharing their details with Koha http://openid.net/specs/openid-connect-core-1_0.html#Consent |
188 |
} else { |
189 |
$errors->{auth_fail} = 1; |
190 |
} |
191 |
} elsif ($code){ |
192 |
|
193 |
#Step Two: Send a Token Request |
194 |
my $token_response = $OpenIDC->GetTokenResponse({code => $code,}); |
195 |
if ($token_response){ |
196 |
my ($token,$header) = $OpenIDC->ParseIdToken({ id_token => $token_response->{id_token}, }); |
197 |
my $token_type = $token_response->{token_type} if $token_response->{token_type}; |
198 |
|
199 |
my $token_response_valid = $OpenIDC->ValidateToken({ |
200 |
token => $token, |
201 |
token_type => $token_type, |
202 |
}); |
203 |
|
204 |
my $access_token = $token_response->{access_token} if $token_response->{access_token}; |
205 |
my $subject = $token->{sub} if $token && $token->{sub}; |
206 |
|
207 |
if ($token_response_valid && $access_token && $subject){ |
208 |
#Step Three: Send a UserInfo Request |
209 |
my $userinfo_response = $OpenIDC->SendUserInfoRequest({ |
210 |
access_token => $access_token, |
211 |
}); |
212 |
|
213 |
my $userinfo_response_valid = $OpenIDC->ValidateUserInfoResponse({ |
214 |
userinfo_response => $userinfo_response, |
215 |
token_subject => $subject, |
216 |
}); |
217 |
|
218 |
#Step Four: Process the UserInfo and find/create borrower in Koha |
219 |
if ($userinfo_response && $userinfo_response_valid){ |
220 |
|
221 |
my $userinfo = Koha::Prosentient::Auth::OpenIDConnect::UserInfo->new({ |
222 |
debugging => $debugging, |
223 |
}); |
224 |
|
225 |
#Process the UserInfo into a canonical data structure using a 1 to 1 hash mapping |
226 |
$userinfo->map_userinfo({ |
227 |
userinfo => $userinfo_response, |
228 |
mapping => $mapping, |
229 |
}); |
230 |
|
231 |
#If we have at least a userinfo email, then we try to find them in Koha or we add them to Koha |
232 |
if ( $userinfo->email && ( ($userinfo->verified_email) || ($insecure) ) ){ |
233 |
|
234 |
($koha_user,$errors) = _process_user({ |
235 |
userinfo => $userinfo, |
236 |
categorycode => $categorycode, |
237 |
branchcode => $branchcode, |
238 |
errors => $errors, |
239 |
}); |
240 |
if ($koha_user->{koha_identifier}){ |
241 |
#NOTE: This section essentially says that it's OK to log this user into Koha |
242 |
$koha_identifier = $koha_user->{koha_identifier}; |
243 |
$return = 1; |
244 |
} else { |
245 |
$errors->{no_koha_identifier} = 1; |
246 |
} |
247 |
|
248 |
} else { |
249 |
$errors->{no_email} = 1 if !$userinfo->email; #Tell user |
250 |
$errors->{not_verified} = 1 if !$userinfo->verified_email && !$insecure; #Tell user |
251 |
} |
252 |
|
253 |
} else { |
254 |
$errors->{no_user} = 1; #Tell user |
255 |
} |
256 |
|
257 |
} else { |
258 |
$errors->{token_fail} = 1 if !$access_token; |
259 |
$errors->{token_invalid} = 1 if !$token_response_valid; |
260 |
$errors->{no_subject} = 1 if !$subject; |
261 |
} |
262 |
|
263 |
} else { |
264 |
$errors->{token_fail} = 1; |
265 |
} |
266 |
|
267 |
} elsif ($server_error){ |
268 |
$errors->{server_error} = 1; #Tell user |
269 |
warn "OpenID Connect Server Error = $server_error"; |
270 |
warn "OpenID Connect Server Error Description = $server_error_description" if $server_error_description; |
271 |
#NOTE: For more information, consult http://openid.net/specs/openid-connect-core-1_0.html#AuthError |
272 |
} |
273 |
} else { |
274 |
$errors->{config_fail} = 1; #Tell user |
275 |
if ($OpenIDC->debugging){ |
276 |
warn "DEBUGGING OpenID Connect object = ".Dumper($OpenIDC); |
277 |
} |
278 |
} |
279 |
|
280 |
if ($OpenIDC->debugging){ |
281 |
warn "DEBUGGING OpenIDConnect1.0 errors = ".Dumper($errors); |
282 |
} |
283 |
|
284 |
my $external_authen = { |
285 |
"OpenIDConnect1.0" => { |
286 |
errors => $errors, |
287 |
return => $return, |
288 |
koha_identifier => $koha_identifier, |
289 |
}, |
290 |
}; |
291 |
|
292 |
my ( $userid, $cookie, $sessionID ) = checkauth( $query, 0, {}, 'opac', '', $external_authen ); |
293 |
|
294 |
if ($provider_id){ |
295 |
my $post_auth_session = C4::Auth::get_session($sessionID); |
296 |
#Set the oidc provider id in the user's active logged-in session |
297 |
$post_auth_session->param("oidc_pid",$provider_id); |
298 |
} |
299 |
|
300 |
|
301 |
#NOTE: If we are unable to authenticate into Koha, we'll be shown the opac-auth.tt page but have the service URL in the address bar |
302 |
print $query->redirect(-uri => "/cgi-bin/koha/opac-user.pl", -cookie => $cookie); |
303 |
exit; |
304 |
|
305 |
################################################################################################## |
306 |
#FUNCTIONS |
307 |
|
308 |
sub _process_user { |
309 |
my ($args) = @_; |
310 |
my ( $koha_identifier ); |
311 |
my $userinfo = $args->{userinfo}; #This should be a Koha::Prosentient::Auth::OpenIDConnect::UserInfo object |
312 |
my $categorycode = $args->{categorycode}; #This should be set in koha-conf.xml |
313 |
my $branchcode = $args->{branchcode}; #This should be set in koha-conf.xml |
314 |
my $errors = $args->{errors}; |
315 |
|
316 |
#Get borrowers |
317 |
my $ProsentientBorrowers = Koha::Prosentient::Borrowers->new(); |
318 |
my $borrowers = $ProsentientBorrowers->FindBorrowerByEmails({ |
319 |
email => $userinfo->{email}, |
320 |
}); |
321 |
|
322 |
if (scalar @$borrowers == 1){ |
323 |
$koha_identifier = $borrowers->[0]->{cardnumber} if $borrowers->[0]->{cardnumber}; |
324 |
$koha_identifier = $borrowers->[0]->{userid} if $borrowers->[0]->{userid}; |
325 |
} elsif (scalar @$borrowers > 1) { |
326 |
$errors->{email_not_unique} = 1; |
327 |
} else { |
328 |
if ($categorycode && $branchcode && $userinfo){ |
329 |
my %memberdata = ( |
330 |
firstname => $userinfo->{firstname}, |
331 |
surname => $userinfo->{surname}, |
332 |
email => $userinfo->{email}, |
333 |
phone => $userinfo->{phone}, |
334 |
dateofbirth => $userinfo->{dateofbirth}, |
335 |
categorycode => $categorycode, |
336 |
branchcode => $branchcode, |
337 |
#dateexpiry |
338 |
#dateenrolled |
339 |
address => $userinfo->{address}, |
340 |
city => $userinfo->{city} // '', |
341 |
state => $userinfo->{state}, |
342 |
zipcode => $userinfo->{zipcode}, |
343 |
country => $userinfo->{country}, |
344 |
#TODO: I wonder if we should be using Nickname/nickname for the cardnumber... |
345 |
#Nickname appears to be the same as their memberID... |
346 |
#cardnumber => $userinfo->{nickname}, |
347 |
); |
348 |
#<koha_sort1>company_name</koha_sort1> |
349 |
$memberdata{sort1} = $userinfo->{koha_sort1} if $userinfo->{koha_sort1}; |
350 |
|
351 |
|
352 |
my $borrowernumber = C4::Members::AddMember(%memberdata); |
353 |
if ($borrowernumber){ |
354 |
my $new_borrowers = $ProsentientBorrowers->FindBorrowerByEmails({ |
355 |
email => $userinfo->{email}, |
356 |
}); |
357 |
if (scalar @$new_borrowers == 1){ |
358 |
$koha_identifier = $new_borrowers->[0]->{cardnumber} if $new_borrowers->[0]->{cardnumber}; |
359 |
$koha_identifier = $new_borrowers->[0]->{userid} if $new_borrowers->[0]->{userid}; |
360 |
} else { |
361 |
$errors->{email_not_unique} = 1; |
362 |
} |
363 |
} |
364 |
} |
365 |
} |
366 |
|
367 |
return ( {koha_identifier => $koha_identifier}, $errors ); |
368 |
} |