From 9fe15b0e3fbab37dccf96354c8eb3e6311aea9b9 Mon Sep 17 00:00:00 2001 From: Shi Yao Wang Date: Tue, 21 Jun 2022 10:59:26 -0400 Subject: [PATCH] Bug 30988: Moving id token validation code into a module Signed-off-by: David Cook --- Koha/Auth/Client/OIDC.pm | 88 ++++++++++++++++++++ opac/svc/auth/openidconnect | 162 +++++++++++++----------------------- 2 files changed, 145 insertions(+), 105 deletions(-) create mode 100644 Koha/Auth/Client/OIDC.pm diff --git a/Koha/Auth/Client/OIDC.pm b/Koha/Auth/Client/OIDC.pm new file mode 100644 index 0000000000..0372e4e384 --- /dev/null +++ b/Koha/Auth/Client/OIDC.pm @@ -0,0 +1,88 @@ +package Koha::Auth::Client::OIDC; + +# 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 . + +use Modern::Perl; + +use JSON; +use MIME::Base64 qw{ decode_base64url }; + +use vars qw(@ISA @EXPORT); +BEGIN { + require Exporter; + @ISA = qw( Exporter ); + push @EXPORT, qw( ValidateIdToken ); +}; + +sub ValidateIdToken { + my ($json, $issuer, $clientid) = @_; + + if ( ! exists( $json->{'id_token'} ) ) { + return ( undef, 'Failed to get proper credentials.' ); + } + if ( lc( $json->{'token_type'} ) ne 'bearer' ) { + return ( undef, + 'Authentication failed. Incorrect token type.' ); + } + my $idtoken = $json->{'id_token'}; + + # need to validate the token here + + my @segments = split( '\.', $idtoken ); + unless ( scalar(@segments) == 3 ) { + return ( undef, + '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 ( $issuer eq q{} ) { + return ( undef, 'Unable to discover issuer.' ); + } + if ( ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) ) + && ( $claims_json->{'iss'} ne $issuer ) ) + { + return ( undef, + "Authentication failed. Issuer of authentication is different from what we expected." + ); + } + 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 + return ( undef, + "Authentication failed. Unexpected audience provided." ); + } + } + if ( ( $claims_json->{'aud'} ne $clientid ) ) { + return ( undef, + "Authentication failed. Unexpected audience." ); + } + if ( $claims_json->{'exp'} < time() ) { + return ( undef, 'Sorry, your authentication has timed out.' ); + } + + if ( ! exists( $claims_json->{'email'} ) ) { + return ( undef, +'Unexpectedly, no email seems to be associated with that account.' + ); + } + + return ($claims_json, undef); +} + +1; diff --git a/opac/svc/auth/openidconnect b/opac/svc/auth/openidconnect index 5ce9931ae3..7318980dad 100755 --- a/opac/svc/auth/openidconnect +++ b/opac/svc/auth/openidconnect @@ -34,6 +34,7 @@ use C4::Auth qw{ checkauth get_session get_template_and_user }; use C4::Context; use C4::Output; use Koha::Patrons; +use Koha::Auth::Client::OIDC qw ( ValidateIdToken ); use LWP::UserAgent; use HTTP::Request::Common qw{ POST }; @@ -127,124 +128,75 @@ elsif ( defined $query->param('code') ) { ); 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'}; + my ($claims_json, $message) = ValidateIdToken($json, $issuer, $clientid); - # need to validate the token here - - 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 ( $issuer eq q{} ) { - loginfailed( $query, 'Unable to discover issuer.' ); - } - if ( ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) ) - && ( $claims_json->{'iss'} ne $issuer ) ) + if ($claims_json) { + my $email = $claims_json->{'email'}; + if ( ( $restricttodomain ne q{} ) + && ( index( $email, $restricttodomain ) < 0 ) ) { loginfailed( $query, - "Authentication failed. Issuer of authentication is different from what we expected." - ); + 'The email you have used is not valid for this library. Email addresses should conclude with ' + . $restricttodomain + . ' .' ); } - 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 ) ) - { - 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('OIDCAutoRegister') // 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{}; - if ($firstname && $surname) { - my $delimiter = $firstname ? q{.} : q{}; - my $userid = $firstname . $delimiter . $surname; - my $categorycode = C4::Context->preference('OIDCDefaultCategory') // q{}; - my $patron_category = Koha::Patron::Categories->find( $categorycode ); - my $branchcode = C4::Context->preference('OIDCDefaultBranch') // 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 OIDCDefaultBranch or OIDCDefaultCategory system preferences are not configured properly. Please contact the library with this error message.'; - } + 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('OIDCAutoRegister') // 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{}; + if ($firstname && $surname) { + my $delimiter = $firstname ? q{.} : q{}; + my $userid = $firstname . $delimiter . $surname; + my $categorycode = C4::Context->preference('OIDCDefaultCategory') // q{}; + my $patron_category = Koha::Patron::Categories->find( $categorycode ); + my $branchcode = C4::Context->preference('OIDCDefaultBranch') // 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 = 'We could not find given name and/or family name.' + $error_feedback = 'The OIDCDefaultBranch or OIDCDefaultCategory system preferences are not configured properly. Please contact the library with this error message.'; } + } else { + $error_feedback = 'We could not find given name and/or family name.' } - my ( $userid, $cookie, $session_id ) = - checkauth( $query, 1, {}, 'opac', $email ); - if ($userid) { # A user with this email is registered in koha + } + 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 ); + #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.' ); + loginfailed( $query, $message); } } else { -- 2.25.1