Bugzilla – Attachment 121324 Details for
Bug 28420
Allow login via AzureAD OpenID-Connect
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
AzureAD OAuth2 implementation based on the existing implementation for Google
koha_azuread.patch (text/plain), 21.27 KB, created by
mark.jaroski
on 2021-05-24 12:00:27 UTC
(
hide
)
Description:
AzureAD OAuth2 implementation based on the existing implementation for Google
Filename:
MIME Type:
Creator:
mark.jaroski
Created:
2021-05-24 12:00:27 UTC
Size:
21.27 KB
patch
obsolete
>diff --git a/C4/Auth.pm b/C4/Auth.pm >index f281def2ce..8c8784317e 100644 >--- a/C4/Auth.pm >+++ b/C4/Auth.pm >@@ -1433,6 +1433,13 @@ sub checkauth { > } > } > >+ if (C4::Context->preference('AzureADOpenIDConnect')) { >+ if ($query->param("OpenIDConnectFailed")) { >+ my $reason = $query->param('OpenIDConnectFailed'); >+ $template->param(invalidAzureADOpenIDConnectLogin => $reason); >+ } >+ } >+ > $template->param( > LibraryName => C4::Context->preference("LibraryName"), > ); >diff --git a/Koha.pm b/Koha.pm >index a2653023f5..59a5ba5db5 100644 >--- a/Koha.pm >+++ b/Koha.pm >@@ -29,7 +29,7 @@ use vars qw{ $VERSION }; > # - #4 : the developer version. The 4th number is the database subversion. > # used by developers when the database changes. updatedatabase take care of the changes itself > # and is automatically called by Auth.pm when needed. >-$VERSION = "20.12.00.050"; >+$VERSION = "20.12.00.051"; > > sub version { > return $VERSION; >diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql >index 0a86168ee0..90cd59b9f9 100644 >--- a/installer/data/mysql/mandatory/sysprefs.sql >+++ b/installer/data/mysql/mandatory/sysprefs.sql >@@ -86,6 +86,14 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('AutoSelfCheckID','','','Staff ID with circulation rights to be used for automatic web-based self-check. Only applies if AutoSelfCheckAllowed syspref is turned on.','free'), > ('AutoSelfCheckPass','','','Password to be used for automatic web-based self-check. Only applies if AutoSelfCheckAllowed syspref is turned on.','free'), > ('AutoSwitchPatron', '0', '', 'Auto switch to patron', 'YesNo'), >+('AzureADOpenIDConnect', '0', NULL, 'if ON, allows the use of AzureAD OpenID Connect for login', 'YesNo'), >+('AzureADOpenIDConnectAutoRegister', '0', NULL, 'If ON autoprovisions new users coming from AzureAD', 'YesNo'), >+('AzureADOAuth2TenantID', '', NULL, 'Tenant ID for the web app registered with AzureAD', 'Free'), >+('AzureADOAuth2ClientID', '', NULL, 'Client ID for the web app registered with AzureAD', 'Free'), >+('AzureADOAuth2ClientSecret', '', NULL, 'Client Secret for the web app registered with AzureAD', 'Free'), >+('AzureADOpenIDConnectAutoRegister', '0', NULL, 'if ON autoprovisions users arriving from AzureAD', 'YesNo'), >+('AzureADOpenIDConnectDefaultCategory', '', '', 'This category code will be used to create Google OpenID Connect patrons.', 'Textarea'), >+('AzureADOpenIDConnectDefaultBranch', '', '', 'This branch code will be used to create Google OpenID Connect patrons.', 'Textarea'), > ('Babeltheque','0','','Turn ON Babeltheque content - See babeltheque.com to subscribe to this service','YesNo'), > ('Babeltheque_url_js','','','Url for Babeltheque javascript (e.g. http://www.babeltheque.com/bw_XX.js)','Free'), > ('Babeltheque_url_update','','','Url for Babeltheque update (E.G. http://www.babeltheque.com/.../file.csv.bz2)','Free'), >diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl >index 6b0a6b53fe..b783345b77 100755 >--- a/installer/data/mysql/updatedatabase.pl >+++ b/installer/data/mysql/updatedatabase.pl >@@ -24274,6 +24274,23 @@ if ( CheckVersion($DBversion) ) { > NewVersion( $DBversion, 28108, "Add new systempreference OpacHiddenItemsHidesRecord" ); > } > >+$DBversion = '20.12.00.051'; >+if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { >+ $dbh->do(q{ >+ INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES >+ ('AzureADOpenIDConnect', '0', NULL, 'if ON, allows the use of AzureAD OpenID Connect for login', 'YesNo'), >+ ('AzureADOAuth2TenantID', '', NULL, 'Tenant ID for the web app registered with AzureAD', 'Free'), >+ ('AzureADOAuth2ClientID', '', NULL, 'Client ID for the web app registered with AzureAD', 'Free'), >+ ('AzureADOpenIDConnectDefaultCategory', '', '', 'This category code will be used to create Google OpenID Connect patrons.', 'Textarea'), >+ ('AzureADOpenIDConnectDefaultBranch', '', '', 'This branch code will be used to create Google OpenID Connect patrons.', 'Textarea'), >+ ('AzureADOpenIDConnectAutoRegister', '0', NULL, 'if ON autoprovisions users arriving from AzureAD', 'YesNo'), >+ ('AzureADOAuth2ClientSecret', '', NULL, 'Client Secret for the web app registered with AzureAD', 'Free'); >+ }); >+ >+ print "Upgrade to $DBversion done (Bug 28420 - Allow login via AzureAD OAuth2 (OpenID-Connect))\n"; >+ SetVersion($DBversion); >+} >+ > # SEE bug 13068 > # if there is anything in the atomicupdate, read and execute it. > my $update_dir = C4::Context->config('intranetdir') . '/installer/data/mysql/atomicupdate/'; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref >index a858b1d0da..f11fb2af70 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref >@@ -196,6 +196,39 @@ Administration: > - "Use this branch code when automatically registering a Google Open ID patron: " > - pref: GoogleOpenIDConnectDefaultBranch > - "." >+ AzureAD OpenID Connect: >+ - >+ - "Use AzureAD OpenID-Connect login: " >+ - pref: AzureADOpenIDConnect >+ choices: >+ 1: "Yes" >+ 0: "No" >+ - You will need to create a new app in https://portal.azure.com and a new secret for this integration >+ - >+ - "" >+ - pref: AzureADOpenIDConnectAutoRegister >+ choices: >+ 1: "Allow" >+ 0: "Don't allow" >+ - Patrons logging in with AzureAD OpenID to automatically register >+ - >+ - "AzureAD Tenant ID: " >+ - pref: AzureADOAuth2TenantID >+ - >+ - "AzureAD Client ID: " >+ - pref: AzureADOAuth2ClientID >+ - >+ - "AzureAD Client Secret: " >+ - pref: AzureADOAuth2ClientSecret >+ - >+ - "Use this category code when automatically registering a AzureAD OpenID patron: " >+ - pref: AzureADOpenIDConnectDefaultCategory >+ choices: patron-categories >+ - "." >+ - >+ - "Use this branch code when automatically registering a AzureAD OpenID patron: " >+ - pref: AzureADOpenIDConnectDefaultBranch >+ - "." > Share anonymous usage statistics: > - > - "Share anonymous Koha usage data with the Koha community: " >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >index b58703b8d0..27d90f1a20 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >@@ -99,6 +99,8 @@ > <a class="nav-link login-link" href="/cgi-bin/koha/opac-user.pl"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a> > [% ELSIF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %] > <a class="nav-link login-link" href="/cgi-bin/koha/svc/auth/googleopenidconnect" role="button" data-toggle="modal"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a> >+ [% ELSIF ( Koha.Preference('AzureADOpenIDConnect') == 1 ) %] >+ <a class="nav-link login-link" href="/cgi-bin/koha/svc/auth/azureadopenidconnect" role="button" data-toggle="modal"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a> > [% ELSE %] > <a href="/cgi-bin/koha/opac-user.pl" class="nav-link login-link loginModal-trigger"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a> > [% END %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt >index f15beea5fc..dc9dd4f709 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt >@@ -165,6 +165,18 @@ > <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" class="btn btn-primary" id="openid_connect">Log in with Google</a> > <p>If you do not have a Google account, but do have a local account, you can still log in: </p> > [% END # /IF GoogleOpenIDConnect %] >+ >+ [% IF ( Koha.Preference('AzureADOpenIDConnect') == 1 ) %] >+ [% IF ( invalidAzureADOpenIDConnectLogin ) %] >+ <h2>AzureAD login</h2> >+ <p>Sorry, your AzureAD login failed. <span class="error">[% invalidAzureADOpenIDConnectLogin | html %]</span></p> >+ <p>Please note that the AzureAD login will only work if you are using the e-mail address registered with this library.</p> >+ <p>If you want to, you can try to <a href="/cgi-bin/koha/svc/auth/AzureADopenidconnect?reauthenticate=select_account">log in using a different account</a> >+ [% END %] >+ <a href="/cgi-bin/koha/svc/auth/AzureADopenidconnect" class="btn btn-primary" id="openid_connect">Log in with AzureAD</a> >+ <p>If you do not have a Microsoft account, but do have a local account, you can still log in: </p> >+ [% END # /IF AzureADOpenIDConnect %] >+ > [% END # /UNLESS OPACShibOnly %] > > [% IF !Koha.Preference('OPACShibOnly') or SCO_login or SCI_login %] >diff --git a/opac/svc/auth/azureadopenidconnect b/opac/svc/auth/azureadopenidconnect >new file mode 100755 >index 0000000000..1c89819836 >--- /dev/null >+++ b/opac/svc/auth/azureadopenidconnect >@@ -0,0 +1,272 @@ >+#!/usr/bin/perl >+# Copyright mark.jaroski@gmail.com, jaroskim@who.int 2021 >+# based on the google Oauth code from vanoudt@gmail.com 2014 >+# >+# 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 AzureAD goes like this >+# >+# The first thing that happens when this script is called is >+# that one gets redirected to an authentication url from AzureAD >+# >+# 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 AzureAD to decrypt. Finally, we can extract >+# the email address from this. >+# >+# NOTE: This is not in fact the OpenID-Connect workflow, but a different >+# workflow workflow called the Code Grant flow. >+ >+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 }; >+ >+use Data::Dumper; # FIXME >+ >+my $tenantID = C4::Context->preference('AzureADOAuth2TenantID'); >+my $authority= "https://login.microsoftonline.com/${tenantID}/v2.0"; >+my $discoveryDocURL = "${authority}/.well-known/openid-configuration"; >+warn $discoveryDocURL; >+my $authendpoint = ''; >+my $tokenendpoint = ''; >+my $scope = 'openid email profile'; >+my $host = C4::Context->preference('OPACBaseURL') // q{}; >+# TODO either implement or delete $restricttodomain >+# normally handled in AzureAD with user/group assignment >+my $restricttodomain = ""; >+ >+# protocol is assumed in OPACBaseURL see bug 5010. >+my $redirecturl = $host . '/cgi-bin/koha/svc/auth/azureadopenidconnect'; >+my $clientid = C4::Context->preference('AzureADOAuth2ClientID'); >+my $clientsecret = C4::Context->preference('AzureADOAuth2ClientSecret'); >+ >+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( 'invalidAzureOpenIDConnectLogin' => $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('azuread-openid-state') ne $stateclaim ) { >+ $session->clear( ["azuread-openid-state"] ); >+ $session->flush(); >+ loginfailed( $query, >+ 'Authentication failed. Your session has an unexpected state.' ); >+ } >+ $session->clear( ["azuread-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'}; >+ >+ 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://' . $authority) ) >+ && ( $claims_json->{'iss'} ne $authority) ) >+ { >+ loginfailed( $query, >+ "Authentication failed. Issuer of authentication : " . $claims_json->{'iss'} . " does not match ${authority}" >+ ); >+ } >+ 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: -" . $claims_json->{'aud'} . "- : -" . $clientid . "-"); >+ } >+ 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.'; >+ $error_feedback = Dumper($claims_json); # FIXME >+ my $auto_registration = C4::Context->preference('AzureADOpenIDConnectAutoRegister') // 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('AzureADOpenIDConnectDefaultCategory') // q{}; >+ my $patron_category = Koha::Patron::Categories->find( $categorycode ); >+ my $branchcode = C4::Context->preference('AzureADOpenIDConnectDefaultBranch') // 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 AzureADOpenIDConnectDefaultBranch or AzureADOpenIDConnectDefaultCategory 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 AzureAD.' ); >+ } >+} >+else { >+ my $session = get_session( $query->cookie('CGISESSID') ); >+ my $openidstate = 'auth_'; >+ $openidstate .= sprintf( "%x", rand 16 ) for 1 .. 32; >+ $session->param( 'azuread-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); >+}
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 28420
:
121315
|
121320
|
121324
|
121513
|
121514