2 # Copyright vanoudt@gmail.com 2014
3 # Based on persona code from chris@bigballofwax.co.nz 2013
5 # This file is part of Koha.
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.
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.
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>.
21 # Basic OAuth2/OpenID Connect authentication for google goes like this
23 # The first thing that happens when this script is called is
24 # that one gets redirected to an authentication url from google
26 # If successful, that then redirects back to this script, setting
27 # a CODE parameter which we use to look up a json authentication
28 # token. This token includes an encrypted json id_token, which we
29 # round-trip back to google to decrypt. Finally, we can extract
30 # the email address from this.
34 use CGI qw
( -utf8 escape
);
35 use C4
::Auth
qw{ checkauth get_session get_template_and_user
};
41 use HTTP
::Request
::Common
qw{ POST
};
43 use MIME
::Base64
qw{ decode_base64url
};
46 'https://accounts.google.com/.well-known/openid-configuration';
47 my $authendpoint = '';
48 my $tokenendpoint = '';
49 my $scope = 'openid email profile';
50 my $host = C4
::Context
->preference('OPACBaseURL') // q{};
51 my $restricttodomain = C4
::Context
->preference('GoogleOpenIDConnectDomain')
54 # protocol is assumed in OPACBaseURL see bug 5010.
55 my $redirecturl = $host . '/cgi-bin/koha/svc/auth/googleopenidconnect';
56 my $issuer = 'accounts.google.com';
57 my $clientid = C4
::Context
->preference('GoogleOAuth2ClientID');
58 my $clientsecret = C4
::Context
->preference('GoogleOAuth2ClientSecret');
60 my $ua = LWP
::UserAgent
->new();
61 my $response = $ua->get($discoveryDocURL);
62 if ( $response->is_success ) {
63 my $json = decode_json
( $response->decoded_content );
64 if ( exists( $json->{'authorization_endpoint'} ) ) {
65 $authendpoint = $json->{'authorization_endpoint'};
67 if ( exists( $json->{'token_endpoint'} ) ) {
68 $tokenendpoint = $json->{'token_endpoint'};
75 my $cgi_query = shift;
77 $cgi_query->delete('code');
78 $cgi_query->param( 'OpenIDConnectFailed' => $reason );
79 my ( $template, $borrowernumber, $cookie ) = get_template_and_user
(
81 template_name
=> 'opac-user.tt',
87 $template->param( 'invalidGoogleOpenIDConnectLogin' => $reason );
88 $template->param( 'loginprompt' => 1 );
89 output_html_with_http_headers
$cgi_query, $cookie, $template->output, undef, { force_no_caching
=> 1 };
93 if ( defined $query->param('error') ) {
95 'An authentication error occurred. (Error:'
96 . $query->param('error')
99 elsif ( defined $query->param('code') ) {
100 my $stateclaim = $query->param('state');
101 my $session = get_session
( $query->cookie('CGISESSID') );
102 if ( $session->param('google-openid-state') ne $stateclaim ) {
103 $session->clear( ["google-openid-state"] );
106 'Authentication failed. Your session has an unexpected state.' );
108 $session->clear( ["google-openid-state"] );
111 my $code = $query->param('code');
112 my $ua = LWP
::UserAgent
->new();
113 if ( $tokenendpoint eq q{} ) {
114 loginfailed
( $query, 'Unable to discover token endpoint.' );
120 client_id
=> $clientid,
121 client_secret
=> $clientsecret,
122 redirect_uri
=> $redirecturl,
123 grant_type
=> 'authorization_code',
127 my $response = $ua->request($request)->decoded_content;
128 my $json = decode_json
($response);
129 if ( exists( $json->{'id_token'} ) ) {
130 if ( lc( $json->{'token_type'} ) ne 'bearer' ) {
132 'Authentication failed. Incorrect token type.' );
134 my $idtoken = $json->{'id_token'};
136 # Normally we'd have to validate the token - but google says not to worry here (Avoids another library!)
137 # See https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo for rationale
138 my @segments = split( '\.', $idtoken );
139 unless ( scalar(@segments) == 3 ) {
141 'Login token broken: either too many or too few segments.' );
143 my ( $header, $claims, $validation ) = @segments;
144 $claims = decode_base64url
($claims);
145 my $claims_json = decode_json
($claims);
146 if ( ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) )
147 && ( $claims_json->{'iss'} ne $issuer ) )
150 "Authentication failed. Issuer of authentication isn't Google."
153 if ( ref( $claims_json->{'aud'} ) eq 'ARRAY' ) {
154 warn "Audience is an array of size: "
155 . scalar( @
$claims_json->{'aud'} );
156 if ( scalar( @
$claims_json->{'aud'} ) > 1 )
157 { # We don't want any other audiences
159 "Authentication failed. Unexpected audience provided." );
162 if ( ( $claims_json->{'aud'} ne $clientid )
163 || ( $claims_json->{'azp'} ne $clientid ) )
166 "Authentication failed. Unexpected audience." );
168 if ( $claims_json->{'exp'} < time() ) {
169 loginfailed
( $query, 'Sorry, your authentication has timed out.' );
172 if ( exists( $claims_json->{'email'} ) ) {
173 my $email = $claims_json->{'email'};
174 if ( ( $restricttodomain ne q{} )
175 && ( index( $email, $restricttodomain ) < 0 ) )
178 'The email you have used is not valid for this library. Email addresses should conclude with '
184 'The email address you are trying to use is not associated with a borrower at this library.';
185 my $auto_registration = C4
::Context
->preference('GoogleOpenIDConnectAutoRegister') // q{0};
186 my $borrower = Koha
::Patrons
->find( { email
=> $email } );
187 if (! $borrower && $auto_registration==1) {
188 my $firstname = $claims_json->{'given_name'} // q{};
189 my $surname = $claims_json->{'family_name'} // q{};
190 my $delimiter = $firstname ?
q{.} : q{};
191 my $userid = $firstname . $delimiter . $surname;
192 my $categorycode = C4
::Context
->preference('GoogleOpenIDConnectDefaultCategory') // q{};
193 my $patron_category = Koha
::Patron
::Categories
->find( $categorycode );
194 my $branchcode = C4
::Context
->preference('GoogleOpenIDConnectDefaultBranch') // q{};
195 my $library = Koha
::Libraries
->find( $branchcode );
196 if (defined $patron_category && defined $library) {
197 my $password = undef;
198 # TODO errors handling!
199 my $borrower = Koha
::Patron
->new({
200 firstname
=> $firstname,
203 categorycode
=> $categorycode,
204 branchcode
=> $branchcode,
206 password
=> $password
209 $error_feedback = 'The GoogleOpenIDConnectDefaultBranch or GoogleOpenIDConnectDefaultCategory system preferences are not configured properly. Please contact the library with this error message.';
212 my ( $userid, $cookie, $session_id ) =
213 checkauth
( $query, 1, {}, 'opac', $email );
214 if ($userid) { # A user with this email is registered in koha
216 #handle redirect to main.pl, for private opac
218 if (C4
::Context
->preference('OpacPublic') ) {
219 $uri = '/cgi-bin/koha/opac-user.pl';
221 $uri = '/cgi-bin/koha/opac-main.pl';
223 print $query->redirect(
229 loginfailed
( $query, $error_feedback );
235 'Unexpectedly, no email seems to be associated with that acccount.'
240 loginfailed
( $query, 'Failed to get proper credentials from Google.' );
244 my $session = get_session
( $query->cookie('CGISESSID') );
245 my $openidstate = 'auth_';
246 $openidstate .= sprintf( "%x", rand 16 ) for 1 .. 32;
247 $session->param( 'google-openid-state', $openidstate );
250 my $prompt = $query->param('reauthenticate') // q{};
251 if ( $authendpoint eq q{} ) {
252 loginfailed
( $query, 'Unable to discover authorisation endpoint.' );
254 my $authorisationurl =
256 . 'response_type=code&'
258 . escape
($redirecturl) . q{&}
260 . escape
($clientid) . q{&}
262 . escape
($scope) . q{&}
264 . escape
($openidstate);
265 if ( $prompt || ( defined $prompt && length $prompt > 0 ) ) {
266 $authorisationurl .= '&prompt=' . escape
($prompt);
268 print $query->redirect($authorisationurl);