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

(-)a/C4/Auth.pm (+7 lines)
Lines 1291-1296 sub checkauth { Link Here
1291
        );
1291
        );
1292
    }
1292
    }
1293
1293
1294
    if (C4::Context->preference('GoogleOpenIDConnect')) {
1295
        if ($query->param("OpenIDConnectFailed")) {
1296
            my $reason = $query->param('OpenIDConnectFailed');
1297
            $template->param(invalidGoogleOpenIDConnectLogin => $reason);
1298
        }
1299
    }
1300
1294
    $template->param(
1301
    $template->param(
1295
        LibraryName => C4::Context->preference("LibraryName"),
1302
        LibraryName => C4::Context->preference("LibraryName"),
1296
    );
1303
    );
(-)a/installer/data/mysql/atomicupdate/googleopenidconnect.sql (+5 lines)
Line 0 Link Here
1
INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES
2
	('GoogleOpenIDConnect', '0', NULL, 'if ON, allows the use of Google OpenID Connect for login', 'YesNo'),
3
        ('GoogleOAuth2ClientID', '', NULL, 'Client ID for the web app registered with Google', 'Free'),
4
        ('GoogleOAuth2ClientSecret', '', NULL, 'Client Secret for the web app registered with Google', 'Free'),
5
        ('GoogleOpenIDConnectDomain', '', NULL, 'Restrict OpenID Connect to this domain (or subdomains of this domain). Leave blank for all Google domains', 'Free');
(-)a/installer/data/mysql/sysprefs.sql (+4 lines)
Lines 153-158 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
153
('gist','0','','Default Goods and Services tax rate NOT in %, but in numeric form (0.12 for 12%), set to 0 to disable GST','Integer'),
153
('gist','0','','Default Goods and Services tax rate NOT in %, but in numeric form (0.12 for 12%), set to 0 to disable GST','Integer'),
154
('GoogleIndicTransliteration','0','','GoogleIndicTransliteration on the OPAC.','YesNo'),
154
('GoogleIndicTransliteration','0','','GoogleIndicTransliteration on the OPAC.','YesNo'),
155
('GoogleJackets','0',NULL,'if ON, displays jacket covers from Google Books API','YesNo'),
155
('GoogleJackets','0',NULL,'if ON, displays jacket covers from Google Books API','YesNo'),
156
('GoogleOpenIDConnect', '0', NULL, 'if ON, allows the use of Google OpenID Connect for login', 'YesNo'),
157
('GoogleOAuth2ClientID', '', NULL, 'Client ID for the web app registered with Google', 'Free'),
158
('GoogleOAuth2ClientSecret', '', NULL, 'Client Secret for the web app registered with Google', 'Free'),
159
('GoogleOpenIDConnectDomain', '', NULL, 'Restrict Google OpenID Connect to this domain (or subdomains of this domain). Leave blank for all Google domains', 'Free'),
156
('hidelostitems','0','','If ON, disables display of\"lost\" items in OPAC.','YesNo'),
160
('hidelostitems','0','','If ON, disables display of\"lost\" items in OPAC.','YesNo'),
157
('HidePatronName','0','','If this is switched on, patron\'s cardnumber will be shown instead of their name on the holds and catalog screens','YesNo'),
161
('HidePatronName','0','','If this is switched on, patron\'s cardnumber will be shown instead of their name on the holds and catalog screens','YesNo'),
158
('hide_marc','0',NULL,'If ON, disables display of MARC fields, subfield codes & indicators (still shows data)','YesNo'),
162
('hide_marc','0',NULL,'If ON, disables display of MARC fields, subfield codes & indicators (still shows data)','YesNo'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref (+18 lines)
Lines 143-148 Administration: Link Here
143
                  Common Name: the Common Name
143
                  Common Name: the Common Name
144
                  emailAddress: the emailAddress
144
                  emailAddress: the emailAddress
145
            - field for SSL client certificate authentication
145
            - field for SSL client certificate authentication
146
    Google OpenID Connect:
147
        -
148
            - pref: GoogleOpenIDConnect
149
              choices:
150
                yes: Use
151
                no: "Don't Use"
152
            - Google OpenID Connect login.
153
            - You will need to select OAuth2 when creating an app in the google cloud console, and set the web origin to your_opac_url and the redirect url to your_opac_url/cgi-bin/koha/svc/auth/googleopenidconnect .
154
        -
155
            - Google OAuth2 Client ID
156
            - pref: GoogleOAuth2ClientID
157
        -
158
            - Google OAuth2 Client Secret
159
            - pref: GoogleOAuth2ClientSecret
160
        -
161
            - Google OpenID Connect Restrict to domain (or subdomain of this domain)
162
            - pref: GoogleOpenIDConnectDomain
163
            - . Leave blank for all google domains
146
    Mozilla Persona:
164
    Mozilla Persona:
147
        -
165
        -
148
            - pref: Persona
166
            - pref: Persona
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+2 lines)
Lines 90-95 Link Here
90
                                        [% IF Koha.Preference('casAuthentication') %]
90
                                        [% IF Koha.Preference('casAuthentication') %]
91
                                            [%# CAS authentication is too complicated for modal window %]
91
                                            [%# CAS authentication is too complicated for modal window %]
92
                                            <li><a href="/cgi-bin/koha/opac-user.pl">Log in to your account</a></li>
92
                                            <li><a href="/cgi-bin/koha/opac-user.pl">Log in to your account</a></li>
93
                                        [% ELSIF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %]
94
                                            <li><a href="/cgi-bin/koha/svc/auth/googleopenidconnect" role="button" data-toggle="modal">Log in to your account</a></li>
93
                                        [% ELSE %]
95
                                        [% ELSE %]
94
                                            <li><a href="/cgi-bin/koha/opac-user.pl" class="loginModal-trigger" role="button" data-toggle="modal">Log in to your account</a></li>
96
                                            <li><a href="/cgi-bin/koha/opac-user.pl" class="loginModal-trigger" role="button" data-toggle="modal">Log in to your account</a></li>
95
                                        [% END %]
97
                                        [% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt (+11 lines)
Lines 137-142 Link Here
137
                                [% END %]
137
                                [% END %]
138
138
139
                            [% END # / IF casAuthentication %]
139
                            [% END # / IF casAuthentication %]
140
														[% IF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %]
141
	                          [% IF ( invalidGoogleOpenIDConnectLogin ) %]
142
                                <h4>Google login</h4>
143
                                <p>Sorry, your Google login failed. <span class="error">[% invalidGoogleOpenIDConnectLogin %]</span></p>
144
                                <p>Please note that the Google login will only work if you are using the e-mail address registered with this library.</p>
145
                                <p>If you want to, you can try to <a href="/cgi-bin/koha/svc/auth/googleopenidconnect?reauthenticate=select_account">log in using a different account</a>
146
	                            [% END %]
147
															<a href="/cgi-bin/koha/svc/auth/googleopenidconnect" class="btn btn-primary" id="openid_connect">Log in with Google</a>
148
                              <p>If you don not have a Google account, but do have a local account, you can still l
149
og in: </p>
150
														[% END %]
140
151
141
                            <form action="/cgi-bin/koha/opac-user.pl" name="auth" id="auth" method="post">
152
                            <form action="/cgi-bin/koha/opac-user.pl" name="auth" id="auth" method="post">
142
                                <input type="hidden" name="koha_login_context" value="opac" />
153
                                <input type="hidden" name="koha_login_context" value="opac" />
(-)a/opac/svc/auth/googleopenidconnect (-1 / +240 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
# Copyright vanoudt@gmail.com 2014
3
# Based on persona code from chris@bigballofwax.co.nz 2013
4
#
5
# This file is part of Koha.
6
#
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.
11
#
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.
16
#
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>.
19
#
20
#
21
# Basic OAuth2/OpenID Connect authentication for google goes like this
22
# First:
23
# get your clientid, clientsecret from google. At this stage, tell
24
# google that your redirect url is /cgi-bin/koha/svc/oauthlogin
25
#
26
# The first thing that happens when this script is called is
27
# that one gets redirected to an authentication url from google
28
#
29
# If successful, that then redirects back to this script, setting
30
# a CODE parameter which we use to look up a json authentication
31
# token. This token includes an encrypted json id_token, which we
32
# round-trip back to google to decrypt. Finally, we can extract
33
# the email address from this.
34
#
35
# There is some room for improvement here.  In particular, Google
36
# recommends verifying and decrypting the id_token locally, which
37
# means caching some information and updating it daily. But that
38
# would make things a lot faster
39
40
use Modern::Perl;
41
use CGI qw ( -utf8 escape );
42
use C4::Auth qw{ checkauth get_session get_template_and_user };
43
use C4::Context;
44
use C4::Output;
45
46
use LWP::UserAgent;
47
use HTTP::Request::Common qw{ POST };
48
use JSON;
49
use MIME::Base64 qw{ decode_base64url };
50
51
my $discoveryDocURL =
52
  'https://accounts.google.com/.well-known/openid-configuration';
53
my $authendpoint     = '';
54
my $tokenendpoint    = '';
55
my $scope            = 'openid email profile';
56
my $host             = C4::Context->preference('OPACBaseURL') // q{};
57
my $restricttodomain = C4::Context->preference('GoogleOpenIDConnectDomain')
58
  // q{};
59
60
# protocol is assumed in OPACBaseURL see bug 5010.
61
my $redirecturl  = $host . '/cgi-bin/koha/svc/auth/googleopenidconnect';
62
my $issuer       = 'accounts.google.com';
63
my $clientid     = C4::Context->preference('GoogleOAuth2ClientID');
64
my $clientsecret = C4::Context->preference('GoogleOAuth2ClientSecret');
65
66
my $ua       = LWP::UserAgent->new();
67
my $response = $ua->get($discoveryDocURL);
68
if ( $response->is_success ) {
69
    my $json = decode_json( $response->decoded_content );
70
    if ( exists( $json->{'authorization_endpoint'} ) ) {
71
        $authendpoint = $json->{'authorization_endpoint'};
72
    }
73
    if ( exists( $json->{'token_endpoint'} ) ) {
74
        $tokenendpoint = $json->{'token_endpoint'};
75
    }
76
}
77
78
my $query = CGI->new;
79
80
sub loginfailed {
81
    my $cgi_query = shift;
82
    my $reason    = shift;
83
    $cgi_query->delete('code');
84
    $cgi_query->param( 'OpenIDConnectFailed' => $reason );
85
    my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
86
        {
87
            template_name   => 'opac-user.tt',
88
            query           => $cgi_query,
89
            type            => 'opac',
90
            authnotrequired => 0,
91
        }
92
    );
93
    $template->param( 'invalidGoogleOpenIDConnectLogin' => $reason );
94
    $template->param( 'loginprompt'                     => 1 );
95
    output_html_with_http_headers $cgi_query, $cookie, $template->output;
96
    return;
97
}
98
99
if ( defined $query->param('error') ) {
100
    loginfailed( $query,
101
            'An authentication error occurred. (Error:'
102
          . $query->param('error')
103
          . ')' );
104
}
105
elsif ( defined $query->param('code') ) {
106
    my $stateclaim = $query->param('state');
107
    my $session    = get_session( $query->cookie('CGISESSID') );
108
    if ( $session->param('google-openid-state') ne $stateclaim ) {
109
        $session->clear( ["google-openid-state"] );
110
        $session->flush();
111
        loginfailed( $query,
112
            'Authentication failed. Your session has an unexpected state.' );
113
    }
114
    $session->clear( ["google-openid-state"] );
115
    $session->flush();
116
117
    my $code = $query->param('code');
118
    my $ua   = LWP::UserAgent->new();
119
    if ( $tokenendpoint eq q{} ) {
120
        loginfailed( $query, 'Unable to discover token endpoint.' );
121
    }
122
    my $request = POST(
123
        $tokenendpoint,
124
        [
125
            code          => $code,
126
            client_id     => $clientid,
127
            client_secret => $clientsecret,
128
            redirect_uri  => $redirecturl,
129
            grant_type    => 'authorization_code',
130
            $scope        => $scope
131
        ]
132
    );
133
    my $response = $ua->request($request)->decoded_content;
134
    my $json     = decode_json($response);
135
    if ( exists( $json->{'id_token'} ) ) {
136
        if ( lc( $json->{'token_type'} ) ne 'bearer' ) {
137
            loginfailed( $query,
138
                'Authentication failed. Incorrect token type.' );
139
        }
140
        my $idtoken = $json->{'id_token'};
141
142
# Normally we'd have to validate the token - but google says not to worry here (Avoids another library!)
143
# See https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo for rationale
144
        my @segments = split( '\.', $idtoken );
145
        unless ( scalar(@segments) == 3 ) {
146
            loginfailed( $query,
147
                'Login token broken: either too many or too few segments.' );
148
        }
149
        my ( $header, $claims, $validation ) = @segments;
150
        $claims = decode_base64url($claims);
151
        my $claims_json = decode_json($claims);
152
        if (   ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) )
153
            && ( $claims_json->{'iss'} ne $issuer ) )
154
        {
155
            loginfailed( $query,
156
                "Authentication failed. Issuer of authentication isn't Google."
157
            );
158
        }
159
        if ( ref( $claims_json->{'aud'} ) eq 'ARRAY' ) {
160
            warn "Audience is an array of size: "
161
              . scalar( @$claims_json->{'aud'} );
162
            if ( scalar( @$claims_json->{'aud'} ) > 1 )
163
            {    # We don't want any other audiences
164
                loginfailed( $query,
165
                    "Authentication failed. Unexpected audience provided." );
166
            }
167
        }
168
        if (   ( $claims_json->{'aud'} ne $clientid )
169
            || ( $claims_json->{'azp'} ne $clientid ) )
170
        {
171
            loginfailed( $query,
172
                "Authentication failed. Unexpected audience." );
173
        }
174
        if ( $claims_json->{'exp'} < time() ) {
175
            loginfailed( $query, 'Sorry, your authentication has timed out.' );
176
        }
177
178
        if ( exists( $claims_json->{'email'} ) ) {
179
            my $email = $claims_json->{'email'};
180
            if (   ( $restricttodomain ne q{} )
181
                && ( index( $email, $restricttodomain ) < 0 ) )
182
            {
183
                loginfailed( $query,
184
'The email you have used is not valid for this library. Email addresses should conclude with '
185
                      . $restricttodomain
186
                      . ' .' );
187
            }
188
            else {
189
                my ( $userid, $cookie, $session_id ) =
190
                  checkauth( $query, 1, {}, 'opac', $email );
191
                if ($userid) {    # A user with this email is registered in koha
192
                    print $query->redirect(
193
                        -uri    => '/cgi-bin/koha/opac-user.pl',
194
                        -cookie => $cookie
195
                    );
196
                }
197
                else {
198
                    loginfailed( $query,
199
'The email address you are trying to use is not associated with a borrower at this library.'
200
                    );
201
                }
202
            }
203
        }
204
        else {
205
            loginfailed( $query,
206
'Unexpectedly, no email seems to be associated with that acccount.'
207
            );
208
        }
209
    }
210
    else {
211
        loginfailed( $query, 'Failed to get proper credentials from Google.' );
212
    }
213
}
214
else {
215
    my $session     = get_session( $query->cookie('CGISESSID') );
216
    my $openidstate = 'auth_';
217
    $openidstate .= sprintf( "%x", rand 16 ) for 1 .. 32;
218
    $session->param( 'google-openid-state', $openidstate );
219
    $session->flush();
220
221
    my $prompt = $query->param('reauthenticate') // q{};
222
    if ( $authendpoint eq q{} ) {
223
        loginfailed( $query, 'Unable to discover authorisation endpoint.' );
224
    }
225
    my $authorisationurl =
226
        $authendpoint . '?'
227
      . 'response_type=code&'
228
      . 'redirect_uri='
229
      . escape($redirecturl) . q{&}
230
      . 'client_id='
231
      . escape($clientid) . q{&}
232
      . 'scope='
233
      . escape($scope) . q{&}
234
      . 'state='
235
      . escape($openidstate);
236
    if ( $prompt || ( defined $prompt && length $prompt > 0 ) ) {
237
        $authorisationurl .= '&prompt=' . escape($prompt);
238
    }
239
    print $query->redirect($authorisationurl);
240
}

Return to bug 10988