View | Details | Raw Unified | Return to bug 30988
Collapse All | Expand All

(-)a/C4/Auth.pm (+7 lines)
Lines 1478-1483 sub checkauth { Link Here
1478
        }
1478
        }
1479
    }
1479
    }
1480
1480
1481
    if (C4::Context->preference('OpenIDConnect')) {
1482
        if ($query->param("OpenIDConnectFailed")) {
1483
            my $reason = $query->param('OpenIDConnectFailed');
1484
            $template->param(invalidOpenIDConnectLogin => $reason);
1485
        }
1486
    }
1487
1481
    $template->param(
1488
    $template->param(
1482
        LibraryName => C4::Context->preference("LibraryName"),
1489
        LibraryName => C4::Context->preference("LibraryName"),
1483
    );
1490
    );
(-)a/installer/data/mysql/atomicupdate/openidconnect.sql (+10 lines)
Line 0 Link Here
1
INSERT INTO systempreferences (variable,value,explanation,options,type)
2
VALUES
3
('OpenIDConnect','0',NULL,'if ON, allows the use of OpenID Connect for login','YesNo'),
4
('OpenIDConfigURL','',NULL,"URL to identity provider's OpenID config",'Free'),
5
('OpenIDOAuth2ClientID','', NULL,'Client ID for the web app','Free'),
6
('OpenIDOAuth2ClientSecret','',NULL,'Client Secret for the web app','Free'),
7
('OpenIDConnectAutoRegister','0',NULL,'OpenID Connect logins to auto-register patrons','YesNo'),
8
('OpenIDConnectDefaultBranch',  '','','This branch code will be used to create OpenID Connect patrons.','Textarea'),
9
('OpenIDConnectDefaultCategory','','','This category code will be used to create OpenID Connect patrons.','Textarea'),
10
('OpenIDConnectDomain', '', NULL, 'Restrict OpenID Connect to this domain (or subdomains of this domain). Leave blank for all domains', 'Free');
(-)a/installer/data/mysql/mandatory/sysprefs.sql (+8 lines)
Lines 497-502 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
497
('OPACXSLTDetailsDisplay','default','','Enable XSL stylesheet control over details page display on OPAC','Free'),
497
('OPACXSLTDetailsDisplay','default','','Enable XSL stylesheet control over details page display on OPAC','Free'),
498
('OPACXSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on OPAC','Free'),
498
('OPACXSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on OPAC','Free'),
499
('OPACXSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on OPAC','Free'),
499
('OPACXSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on OPAC','Free'),
500
('OpenIDConfigURL','',NULL,"URL to identity provider's OpenID config",'Free'),
501
('OpenIDConnect','0',NULL,'if ON, allows the use of OpenID Connect for login','YesNo'),
502
('OpenIDConnectAutoRegister','0',NULL,'OpenID Connect logins to auto-register patrons','YesNo'),
503
('OpenIDConnectDefaultBranch',  '','','This branch code will be used to create OpenID Connect patrons.','Textarea'),
504
('OpenIDConnectDefaultCategory','','','This category code will be used to create OpenID Connect patrons.','Textarea'),
505
('OpenIDConnectDomain', '', NULL, 'Restrict OpenID Connect to this domain (or subdomains of this domain). Leave blank for all domains', 'Free'),
506
('OpenIDOAuth2ClientID', '', NULL, 'Client ID for the web app', 'Free'),
507
('OpenIDOAuth2ClientSecret', '', NULL, 'Client Secret for the web app', 'Free'),
500
('OpenLibraryCovers','0',NULL,'If ON Openlibrary book covers will be show','YesNo'),
508
('OpenLibraryCovers','0',NULL,'If ON Openlibrary book covers will be show','YesNo'),
501
('OpenLibrarySearch','0',NULL,'If Yes Open Library search results will show in OPAC','YesNo'),
509
('OpenLibrarySearch','0',NULL,'If Yes Open Library search results will show in OPAC','YesNo'),
502
('OpenURLResolverURL', '', NULL, 'URL of OpenURL Resolver', 'Free'),
510
('OpenURLResolverURL', '', NULL, 'URL of OpenURL Resolver', 'Free'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref (+39 lines)
Lines 171-176 Administration: Link Here
171
                  None: "None"
171
                  None: "None"
172
                  Common Name: Common Name
172
                  Common Name: Common Name
173
                  emailAddress: emailAddress
173
                  emailAddress: emailAddress
174
    OpenID Connect:
175
        -
176
            - "Use OpenID Connect login in the OPAC: "
177
            - pref: OpenIDConnect
178
              choices:
179
                1: "Yes"
180
                0: "No"
181
            - You will need to create an app in your chosen identity provider and activate OAuth2. Requires <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConfigURL">OpenIDConfigURL</a>, <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OAuth2ClientID">OAuth2ClientID</a> and <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OAuth2ClientSecret">OAuth2ClientSecret</a>
182
        -
183
            - "URL to your identity provider's OpenID configuration: "
184
            - pref: OpenIDConfigURL
185
            - Along with <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OAuth2ClientID">OAuth2ClientID</a> and <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OAuth2ClientSecret">OAuth2ClientSecret</a>. Enables <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect.</a>
186
        -
187
            - "OAuth2 Client ID: "
188
            - pref: OpenIDOAuth2ClientID
189
            - Along with <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConfigURL">OpenIDConfigURL</a> and <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OAuth2ClientSecret">OAuth2ClientSecret</a>. Enables <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect.</a>
190
        -
191
            - "OAuth2 Client Secret: "
192
            - pref: OpenIDOAuth2ClientSecret
193
            - Along with <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConfigURL">OpenIDConfigURL</a> and <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OAuth2ClientID">OAuth2ClientID</a>. Enables <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect.</a>
194
        -
195
            - pref: OpenIDConnectAutoRegister
196
              choices:
197
                1: Allow
198
                0: "Don't allow"
199
            - patrons logging in with OpenID to automatically register. Requires <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect</a>.
200
        -
201
            - "Use this category code when automatically registering a patron using OpenID: "
202
            - pref: OpenIDConnectDefaultCategory
203
              choices: patron-categories
204
            - Requires <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect</a>.
205
        -
206
            - "Use this branch code when automatically registering a patron using OpenID: "
207
            - pref: OpenIDConnectDefaultBranch
208
            - Requires <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect</a>.
209
        -
210
            - "Restrict OpenID Connect to this domain (or subdomain of this domain): "
211
            - pref: OpenIDConnectDomain
212
            - Leave blank for all domains. Requires <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpenIDConnect">OpenIDConnect</a>.
174
    Google OpenID Connect:
213
    Google OpenID Connect:
175
        -
214
        -
176
            - "Use Google OpenID Connect login in the OPAC: "
215
            - "Use Google OpenID Connect login in the OPAC: "
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+2 lines)
Lines 99-104 Link Here
99
                                [% IF Koha.Preference('casAuthentication') %]
99
                                [% IF Koha.Preference('casAuthentication') %]
100
                                    [%# CAS authentication is too complicated for modal window %]
100
                                    [%# CAS authentication is too complicated for modal window %]
101
                                        <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>
101
                                        <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>
102
                                [% ELSIF ( Koha.Preference('OpenIDConnect') == 1 ) %]
103
                                    <a class="nav-link login-link" href="/cgi-bin/koha/svc/auth/openidconnect"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a>
102
                                [% ELSIF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %]
104
                                [% ELSIF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %]
103
                                    <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" 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>
105
                                    <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" 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>
104
                                [% ELSE %]
106
                                [% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt (+9 lines)
Lines 165-170 Link Here
165
                                <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" class="btn btn-light" id="openid_connect"><i class="fa fa-google" aria-hidden="true"></i> Log in with Google</a>
165
                                <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" class="btn btn-light" id="openid_connect"><i class="fa fa-google" aria-hidden="true"></i> Log in with Google</a>
166
                                <p>If you do not have a Google account, but do have a local account, you can still log in: </p>
166
                                <p>If you do not have a Google account, but do have a local account, you can still log in: </p>
167
                            [% END # /IF GoogleOpenIDConnect %]
167
                            [% END # /IF GoogleOpenIDConnect %]
168
                            [% IF ( Koha.Preference('OpenIDConnect') == 1 ) %]
169
                                [% IF ( invalidOpenIDConnectLogin ) %]
170
                                    <h2>OpenID login</h2>
171
                                    <p>Sorry, your login using OpenID Connect failed. <span class="error">[% invalidOpenIDConnectLogin | html %]</span></p>
172
                                    <p>Please note that the login will only work if you are using the e-mail address registered with this library.</p>
173
                                [% END %]
174
                                <a href="/cgi-bin/koha/svc/auth/openidconnect" class="btn btn-primary" id="openid_connect">Log in with OpenID</a>
175
                                <p>If you do not have an OpenID account from the provider specified in this library, but do have a local account, you can still log in: </p>
176
                            [% END # /IF OpenIDConnect %]
168
                        [% END # /UNLESS OPACShibOnly %]
177
                        [% END # /UNLESS OPACShibOnly %]
169
178
170
                        [% IF !(invalid_username_or_password || too_many_login_attempts) and password_has_expired %]
179
                        [% IF !(invalid_username_or_password || too_many_login_attempts) and password_has_expired %]
(-)a/opac/svc/auth/openidconnect (-1 / +270 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
#
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
#
18
#
19
# Basic OAuth2/OpenID Connect authentication goes like this
20
#
21
# The first thing that happens when this script is called is
22
# that one gets redirected to an authentication url
23
#
24
# If successful, that then redirects back to this script, setting
25
# a CODE parameter which we use to look up a json authentication
26
# token. This token includes an encrypted json id_token, which we
27
# round-trip back to decrypt. Finally, we can extract
28
# the email address from this.
29
#
30
31
use Modern::Perl;
32
use CGI qw ( -utf8 escape );
33
use C4::Auth qw{ checkauth get_session get_template_and_user };
34
use C4::Context;
35
use C4::Output;
36
use Koha::Patrons;
37
38
use LWP::UserAgent;
39
use HTTP::Request::Common qw{ POST };
40
use JSON;
41
use MIME::Base64 qw{ decode_base64url };
42
43
my $discoveryDocURL  = C4::Context->preference('OpenIDConfigURL');
44
my $issuer           = '';
45
my $authendpoint     = '';
46
my $tokenendpoint    = '';
47
my $scope            = 'openid email profile';
48
my $host             = C4::Context->preference('OPACBaseURL') // q{};
49
my $restricttodomain = C4::Context->preference('OpenIDConnectDomain')
50
  // q{};
51
52
# protocol is assumed in OPACBaseURL see bug 5010.
53
my $redirecturl  = $host . '/cgi-bin/koha/svc/auth/openidconnect';
54
my $clientid     = C4::Context->preference('OpenIDOAuth2ClientID');
55
my $clientsecret = C4::Context->preference('OpenIDOAuth2ClientSecret');
56
57
my $ua       = LWP::UserAgent->new();
58
my $response = $ua->get($discoveryDocURL);
59
if ( $response->is_success ) {
60
    my $json = decode_json( $response->decoded_content );
61
    if ( exists( $json->{'issuer'} ) ) {
62
        $issuer = $json->{'issuer'};
63
    }
64
    if ( exists( $json->{'authorization_endpoint'} ) ) {
65
        $authendpoint = $json->{'authorization_endpoint'};
66
    }
67
    if ( exists( $json->{'token_endpoint'} ) ) {
68
        $tokenendpoint = $json->{'token_endpoint'};
69
    }
70
}
71
72
my $query = CGI->new;
73
74
sub loginfailed {
75
    my $cgi_query = shift;
76
    my $reason    = shift;
77
    $cgi_query->delete('code');
78
    $cgi_query->param( 'OpenIDConnectFailed' => $reason );
79
    my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
80
        {
81
            template_name   => 'opac-user.tt',
82
            query           => $cgi_query,
83
            type            => 'opac',
84
        }
85
    );
86
    $template->param( 'invalidOpenIDConnectLogin' => $reason );
87
    $template->param( 'loginprompt'                     => 1 );
88
    output_html_with_http_headers $cgi_query, $cookie, $template->output, undef, { force_no_caching => 1 };
89
    return;
90
}
91
92
if ( defined $query->param('error') ) {
93
    loginfailed( $query,
94
            'An authentication error occurred. (Error:'
95
          . $query->param('error')
96
          . ')' );
97
}
98
elsif ( defined $query->param('code') ) {
99
    my $stateclaim = $query->param('state');
100
    my $session    = get_session( $query->cookie('CGISESSID') );
101
    if ( $session->param('openid-state') ne $stateclaim ) {
102
        $session->clear( ["openid-state"] );
103
        $session->flush();
104
        loginfailed( $query,
105
            'Authentication failed. Your session has an unexpected state.' );
106
    }
107
    $session->clear( ["openid-state"] );
108
    $session->flush();
109
110
    my $code = $query->param('code');
111
    my $ua   = LWP::UserAgent->new();
112
    if ( $tokenendpoint eq q{} ) {
113
        loginfailed( $query, 'Unable to discover token endpoint.' );
114
    }
115
    my $request = POST(
116
        $tokenendpoint,
117
        [
118
            code          => $code,
119
            client_id     => $clientid,
120
            client_secret => $clientsecret,
121
            redirect_uri  => $redirecturl,
122
            grant_type    => 'authorization_code',
123
            $scope        => $scope
124
        ]
125
    );
126
    my $response = $ua->request($request)->decoded_content;
127
    my $json     = decode_json($response);
128
    if ( exists( $json->{'id_token'} ) ) {
129
        if ( lc( $json->{'token_type'} ) ne 'bearer' ) {
130
            loginfailed( $query,
131
                'Authentication failed. Incorrect token type.' );
132
        }
133
        my $idtoken = $json->{'id_token'};
134
135
        # need to validate the token here
136
137
        my @segments = split( '\.', $idtoken );
138
        unless ( scalar(@segments) == 3 ) {
139
            loginfailed( $query,
140
                'Login token broken: either too many or too few segments.' );
141
        }
142
        my ( $header, $claims, $validation ) = @segments;
143
        $claims = decode_base64url($claims);
144
        my $claims_json = decode_json($claims);
145
        if ( $issuer eq q{} ) {
146
            loginfailed( $query, 'Unable to discover issuer.' );
147
        }
148
        if (   ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) )
149
            && ( $claims_json->{'iss'} ne $issuer ) )
150
        {
151
            loginfailed( $query,
152
                "Authentication failed. Issuer of authentication is different from what we expected."
153
            );
154
        }
155
        if ( ref( $claims_json->{'aud'} ) eq 'ARRAY' ) {
156
            warn "Audience is an array of size: "
157
              . scalar( @$claims_json->{'aud'} );
158
            if ( scalar( @$claims_json->{'aud'} ) > 1 )
159
            {    # We don't want any other audiences
160
                loginfailed( $query,
161
                    "Authentication failed. Unexpected audience provided." );
162
            }
163
        }
164
        if (   ( $claims_json->{'aud'} ne $clientid ) )
165
        {
166
            loginfailed( $query,
167
                "Authentication failed. Unexpected audience." );
168
        }
169
        if ( $claims_json->{'exp'} < time() ) {
170
            loginfailed( $query, 'Sorry, your authentication has timed out.' );
171
        }
172
173
        if ( exists( $claims_json->{'email'} ) ) {
174
            my $email = $claims_json->{'email'};
175
            if (   ( $restricttodomain ne q{} )
176
                && ( index( $email, $restricttodomain ) < 0 ) )
177
            {
178
                loginfailed( $query,
179
'The email you have used is not valid for this library. Email addresses should conclude with '
180
                      . $restricttodomain
181
                      . ' .' );
182
            }
183
            else {
184
                my $error_feedback =
185
'The email address you are trying to use is not associated with a borrower at this library.';
186
                my $auto_registration = C4::Context->preference('OpenIDConnectAutoRegister') // q{0};
187
                my $borrower = Koha::Patrons->find( { email => $email } );
188
                if (! $borrower && $auto_registration==1) {
189
                    my $firstname = $claims_json->{'given_name'} // q{};
190
                    my $surname = $claims_json->{'family_name'} // q{};
191
                    my $delimiter = $firstname ? q{.} : q{};
192
                    my $userid = $firstname . $delimiter . $surname;
193
                    my $categorycode = C4::Context->preference('OpenIDConnectDefaultCategory') // q{};
194
                    my $patron_category = Koha::Patron::Categories->find( $categorycode );
195
                    my $branchcode = C4::Context->preference('OpenIDConnectDefaultBranch') // q{};
196
                    my $library = Koha::Libraries->find( $branchcode );
197
                    if (defined $patron_category && defined $library) {
198
                        my $password = undef;
199
                        # TODO errors handling!
200
                        my $borrower = Koha::Patron->new({
201
                            firstname    => $firstname,
202
                            surname      => $surname,
203
                            email        => $email,
204
                            categorycode => $categorycode,
205
                            branchcode   => $branchcode,
206
                            userid       => $userid,
207
                            password     => $password
208
                        })->store;
209
                    } else {
210
                        $error_feedback = 'The OpenIDConnectDefaultBranch or OpenIDConnectDefaultCategory system preferences are not configured properly. Please contact the library with this error message.';
211
                    }
212
                }
213
                my ( $userid, $cookie, $session_id ) =
214
                  checkauth( $query, 1, {}, 'opac', $email );
215
                if ($userid) {    # A user with this email is registered in koha
216
217
                    #handle redirect to main.pl, for private opac
218
                    my $uri;
219
                    if (C4::Context->preference('OpacPublic') ) {
220
                        $uri    =  '/cgi-bin/koha/opac-user.pl';
221
                    } else {
222
                        $uri    =  '/cgi-bin/koha/opac-main.pl';
223
                    }
224
                    print $query->redirect(
225
                        -uri    => $uri,
226
                        -cookie => $cookie
227
                    );
228
                }
229
                else {
230
                    loginfailed( $query, $error_feedback );
231
                }
232
            }
233
        }
234
        else {
235
            loginfailed( $query,
236
'Unexpectedly, no email seems to be associated with that acccount.'
237
            );
238
        }
239
    }
240
    else {
241
        loginfailed( $query, 'Failed to get proper credentials.' );
242
    }
243
}
244
else {
245
    my $session     = get_session( $query->cookie('CGISESSID') );
246
    my $openidstate = 'auth_';
247
    $openidstate .= sprintf( "%x", rand 16 ) for 1 .. 32;
248
    $session->param( 'openid-state', $openidstate );
249
    $session->flush();
250
251
    my $prompt = $query->param('reauthenticate') // q{};
252
    if ( $authendpoint eq q{} ) {
253
        loginfailed( $query, 'Unable to discover authorisation endpoint.' );
254
    }
255
    my $authorisationurl =
256
        $authendpoint . '?'
257
      . 'response_type=code&'
258
      . 'redirect_uri='
259
      . escape($redirecturl) . q{&}
260
      . 'client_id='
261
      . escape($clientid) . q{&}
262
      . 'scope='
263
      . escape($scope) . q{&}
264
      . 'state='
265
      . escape($openidstate);
266
    if ( $prompt || ( defined $prompt && length $prompt > 0 ) ) {
267
        $authorisationurl .= '&prompt=' . escape($prompt);
268
    }
269
    print $query->redirect($authorisationurl);
270
}

Return to bug 30988