Jonathan Druart
638786e719
It defaults to 0 in get_template_and_user Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com> Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
268 lines
10 KiB
Perl
Executable file
268 lines
10 KiB
Perl
Executable file
#!/usr/bin/perl
|
|
# Copyright vanoudt@gmail.com 2014
|
|
# Based on persona code from chris@bigballofwax.co.nz 2013
|
|
#
|
|
# This file is part of Koha.
|
|
#
|
|
# Koha is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Koha is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Koha; if not, see <http://www.gnu.org/licenses>.
|
|
#
|
|
#
|
|
# Basic OAuth2/OpenID Connect authentication for google goes like this
|
|
#
|
|
# The first thing that happens when this script is called is
|
|
# that one gets redirected to an authentication url from google
|
|
#
|
|
# If successful, that then redirects back to this script, setting
|
|
# a CODE parameter which we use to look up a json authentication
|
|
# token. This token includes an encrypted json id_token, which we
|
|
# round-trip back to google to decrypt. Finally, we can extract
|
|
# the email address from this.
|
|
#
|
|
|
|
use Modern::Perl;
|
|
use CGI qw ( -utf8 escape );
|
|
use C4::Auth qw{ checkauth get_session get_template_and_user };
|
|
use C4::Context;
|
|
use C4::Output;
|
|
use Koha::Patrons;
|
|
|
|
use LWP::UserAgent;
|
|
use HTTP::Request::Common qw{ POST };
|
|
use JSON;
|
|
use MIME::Base64 qw{ decode_base64url };
|
|
|
|
my $discoveryDocURL =
|
|
'https://accounts.google.com/.well-known/openid-configuration';
|
|
my $authendpoint = '';
|
|
my $tokenendpoint = '';
|
|
my $scope = 'openid email profile';
|
|
my $host = C4::Context->preference('OPACBaseURL') // q{};
|
|
my $restricttodomain = C4::Context->preference('GoogleOpenIDConnectDomain')
|
|
// q{};
|
|
|
|
# protocol is assumed in OPACBaseURL see bug 5010.
|
|
my $redirecturl = $host . '/cgi-bin/koha/svc/auth/googleopenidconnect';
|
|
my $issuer = 'accounts.google.com';
|
|
my $clientid = C4::Context->preference('GoogleOAuth2ClientID');
|
|
my $clientsecret = C4::Context->preference('GoogleOAuth2ClientSecret');
|
|
|
|
my $ua = LWP::UserAgent->new();
|
|
my $response = $ua->get($discoveryDocURL);
|
|
if ( $response->is_success ) {
|
|
my $json = decode_json( $response->decoded_content );
|
|
if ( exists( $json->{'authorization_endpoint'} ) ) {
|
|
$authendpoint = $json->{'authorization_endpoint'};
|
|
}
|
|
if ( exists( $json->{'token_endpoint'} ) ) {
|
|
$tokenendpoint = $json->{'token_endpoint'};
|
|
}
|
|
}
|
|
|
|
my $query = CGI->new;
|
|
|
|
sub loginfailed {
|
|
my $cgi_query = shift;
|
|
my $reason = shift;
|
|
$cgi_query->delete('code');
|
|
$cgi_query->param( 'OpenIDConnectFailed' => $reason );
|
|
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
|
|
{
|
|
template_name => 'opac-user.tt',
|
|
query => $cgi_query,
|
|
type => 'opac',
|
|
}
|
|
);
|
|
$template->param( 'invalidGoogleOpenIDConnectLogin' => $reason );
|
|
$template->param( 'loginprompt' => 1 );
|
|
output_html_with_http_headers $cgi_query, $cookie, $template->output, undef, { force_no_caching => 1 };
|
|
return;
|
|
}
|
|
|
|
if ( defined $query->param('error') ) {
|
|
loginfailed( $query,
|
|
'An authentication error occurred. (Error:'
|
|
. $query->param('error')
|
|
. ')' );
|
|
}
|
|
elsif ( defined $query->param('code') ) {
|
|
my $stateclaim = $query->param('state');
|
|
my $session = get_session( $query->cookie('CGISESSID') );
|
|
if ( $session->param('google-openid-state') ne $stateclaim ) {
|
|
$session->clear( ["google-openid-state"] );
|
|
$session->flush();
|
|
loginfailed( $query,
|
|
'Authentication failed. Your session has an unexpected state.' );
|
|
}
|
|
$session->clear( ["google-openid-state"] );
|
|
$session->flush();
|
|
|
|
my $code = $query->param('code');
|
|
my $ua = LWP::UserAgent->new();
|
|
if ( $tokenendpoint eq q{} ) {
|
|
loginfailed( $query, 'Unable to discover token endpoint.' );
|
|
}
|
|
my $request = POST(
|
|
$tokenendpoint,
|
|
[
|
|
code => $code,
|
|
client_id => $clientid,
|
|
client_secret => $clientsecret,
|
|
redirect_uri => $redirecturl,
|
|
grant_type => 'authorization_code',
|
|
$scope => $scope
|
|
]
|
|
);
|
|
my $response = $ua->request($request)->decoded_content;
|
|
my $json = decode_json($response);
|
|
if ( exists( $json->{'id_token'} ) ) {
|
|
if ( lc( $json->{'token_type'} ) ne 'bearer' ) {
|
|
loginfailed( $query,
|
|
'Authentication failed. Incorrect token type.' );
|
|
}
|
|
my $idtoken = $json->{'id_token'};
|
|
|
|
# Normally we'd have to validate the token - but google says not to worry here (Avoids another library!)
|
|
# See https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo for rationale
|
|
my @segments = split( '\.', $idtoken );
|
|
unless ( scalar(@segments) == 3 ) {
|
|
loginfailed( $query,
|
|
'Login token broken: either too many or too few segments.' );
|
|
}
|
|
my ( $header, $claims, $validation ) = @segments;
|
|
$claims = decode_base64url($claims);
|
|
my $claims_json = decode_json($claims);
|
|
if ( ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) )
|
|
&& ( $claims_json->{'iss'} ne $issuer ) )
|
|
{
|
|
loginfailed( $query,
|
|
"Authentication failed. Issuer of authentication isn't Google."
|
|
);
|
|
}
|
|
if ( ref( $claims_json->{'aud'} ) eq 'ARRAY' ) {
|
|
warn "Audience is an array of size: "
|
|
. scalar( @$claims_json->{'aud'} );
|
|
if ( scalar( @$claims_json->{'aud'} ) > 1 )
|
|
{ # We don't want any other audiences
|
|
loginfailed( $query,
|
|
"Authentication failed. Unexpected audience provided." );
|
|
}
|
|
}
|
|
if ( ( $claims_json->{'aud'} ne $clientid )
|
|
|| ( $claims_json->{'azp'} ne $clientid ) )
|
|
{
|
|
loginfailed( $query,
|
|
"Authentication failed. Unexpected audience." );
|
|
}
|
|
if ( $claims_json->{'exp'} < time() ) {
|
|
loginfailed( $query, 'Sorry, your authentication has timed out.' );
|
|
}
|
|
|
|
if ( exists( $claims_json->{'email'} ) ) {
|
|
my $email = $claims_json->{'email'};
|
|
if ( ( $restricttodomain ne q{} )
|
|
&& ( index( $email, $restricttodomain ) < 0 ) )
|
|
{
|
|
loginfailed( $query,
|
|
'The email you have used is not valid for this library. Email addresses should conclude with '
|
|
. $restricttodomain
|
|
. ' .' );
|
|
}
|
|
else {
|
|
my $error_feedback =
|
|
'The email address you are trying to use is not associated with a borrower at this library.';
|
|
my $auto_registration = C4::Context->preference('GoogleOpenIDConnectAutoRegister') // q{0};
|
|
my $borrower = Koha::Patrons->find( { email => $email } );
|
|
if (! $borrower && $auto_registration==1) {
|
|
my $firstname = $claims_json->{'given_name'} // q{};
|
|
my $surname = $claims_json->{'family_name'} // q{};
|
|
my $delimiter = $firstname ? q{.} : q{};
|
|
my $userid = $firstname . $delimiter . $surname;
|
|
my $categorycode = C4::Context->preference('GoogleOpenIDConnectDefaultCategory') // q{};
|
|
my $patron_category = Koha::Patron::Categories->find( $categorycode );
|
|
my $branchcode = C4::Context->preference('GoogleOpenIDConnectDefaultBranch') // q{};
|
|
my $library = Koha::Libraries->find( $branchcode );
|
|
if (defined $patron_category && defined $library) {
|
|
my $password = undef;
|
|
# TODO errors handling!
|
|
my $borrower = Koha::Patron->new({
|
|
firstname => $firstname,
|
|
surname => $surname,
|
|
email => $email,
|
|
categorycode => $categorycode,
|
|
branchcode => $branchcode,
|
|
userid => $userid,
|
|
password => $password
|
|
})->store;
|
|
} else {
|
|
$error_feedback = 'The GoogleOpenIDConnectDefaultBranch or GoogleOpenIDConnectDefaultCategory system preferences are not configured properly. Please contact the library with this error message.';
|
|
}
|
|
}
|
|
my ( $userid, $cookie, $session_id ) =
|
|
checkauth( $query, 1, {}, 'opac', $email );
|
|
if ($userid) { # A user with this email is registered in koha
|
|
|
|
#handle redirect to main.pl, for private opac
|
|
my $uri;
|
|
if (C4::Context->preference('OpacPublic') ) {
|
|
$uri = '/cgi-bin/koha/opac-user.pl';
|
|
} else {
|
|
$uri = '/cgi-bin/koha/opac-main.pl';
|
|
}
|
|
print $query->redirect(
|
|
-uri => $uri,
|
|
-cookie => $cookie
|
|
);
|
|
}
|
|
else {
|
|
loginfailed( $query, $error_feedback );
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
loginfailed( $query,
|
|
'Unexpectedly, no email seems to be associated with that acccount.'
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
loginfailed( $query, 'Failed to get proper credentials from Google.' );
|
|
}
|
|
}
|
|
else {
|
|
my $session = get_session( $query->cookie('CGISESSID') );
|
|
my $openidstate = 'auth_';
|
|
$openidstate .= sprintf( "%x", rand 16 ) for 1 .. 32;
|
|
$session->param( 'google-openid-state', $openidstate );
|
|
$session->flush();
|
|
|
|
my $prompt = $query->param('reauthenticate') // q{};
|
|
if ( $authendpoint eq q{} ) {
|
|
loginfailed( $query, 'Unable to discover authorisation endpoint.' );
|
|
}
|
|
my $authorisationurl =
|
|
$authendpoint . '?'
|
|
. 'response_type=code&'
|
|
. 'redirect_uri='
|
|
. escape($redirecturl) . q{&}
|
|
. 'client_id='
|
|
. escape($clientid) . q{&}
|
|
. 'scope='
|
|
. escape($scope) . q{&}
|
|
. 'state='
|
|
. escape($openidstate);
|
|
if ( $prompt || ( defined $prompt && length $prompt > 0 ) ) {
|
|
$authorisationurl .= '&prompt=' . escape($prompt);
|
|
}
|
|
print $query->redirect($authorisationurl);
|
|
}
|