Bug 16892: Follow up use AddMember as per QA comment
[koha.git] / opac / svc / auth / googleopenidconnect
blobc08b3c1d70693154e3967c17fb31e8b6da4df47e
1 #!/usr/bin/perl
2 # Copyright vanoudt@gmail.com 2014
3 # Based on persona code from chris@bigballofwax.co.nz 2013
5 # This file is part of Koha.
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.
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.
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>.
21 # Basic OAuth2/OpenID Connect authentication for google goes like this
23 # The first thing that happens when this script is called is
24 # that one gets redirected to an authentication url from google
26 # If successful, that then redirects back to this script, setting
27 # a CODE parameter which we use to look up a json authentication
28 # token. This token includes an encrypted json id_token, which we
29 # round-trip back to google to decrypt. Finally, we can extract
30 # the email address from this.
33 use Modern::Perl;
34 use CGI qw ( -utf8 escape );
35 use C4::Auth qw{ checkauth get_session get_template_and_user };
36 use C4::Context;
37 use C4::Members;
38 use C4::Output;
39 use Koha::Patrons;
41 use LWP::UserAgent;
42 use HTTP::Request::Common qw{ POST };
43 use JSON;
44 use MIME::Base64 qw{ decode_base64url };
46 my $discoveryDocURL =
47 'https://accounts.google.com/.well-known/openid-configuration';
48 my $authendpoint = '';
49 my $tokenendpoint = '';
50 my $scope = 'openid email profile';
51 my $host = C4::Context->preference('OPACBaseURL') // q{};
52 my $restricttodomain = C4::Context->preference('GoogleOpenIDConnectDomain')
53 // q{};
55 # protocol is assumed in OPACBaseURL see bug 5010.
56 my $redirecturl = $host . '/cgi-bin/koha/svc/auth/googleopenidconnect';
57 my $issuer = 'accounts.google.com';
58 my $clientid = C4::Context->preference('GoogleOAuth2ClientID');
59 my $clientsecret = C4::Context->preference('GoogleOAuth2ClientSecret');
61 my $ua = LWP::UserAgent->new();
62 my $response = $ua->get($discoveryDocURL);
63 if ( $response->is_success ) {
64 my $json = decode_json( $response->decoded_content );
65 if ( exists( $json->{'authorization_endpoint'} ) ) {
66 $authendpoint = $json->{'authorization_endpoint'};
68 if ( exists( $json->{'token_endpoint'} ) ) {
69 $tokenendpoint = $json->{'token_endpoint'};
73 my $query = CGI->new;
75 sub loginfailed {
76 my $cgi_query = shift;
77 my $reason = shift;
78 $cgi_query->delete('code');
79 $cgi_query->param( 'OpenIDConnectFailed' => $reason );
80 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
82 template_name => 'opac-user.tt',
83 query => $cgi_query,
84 type => 'opac',
85 authnotrequired => 0,
88 $template->param( 'invalidGoogleOpenIDConnectLogin' => $reason );
89 $template->param( 'loginprompt' => 1 );
90 output_html_with_http_headers $cgi_query, $cookie, $template->output;
91 return;
94 if ( defined $query->param('error') ) {
95 loginfailed( $query,
96 'An authentication error occurred. (Error:'
97 . $query->param('error')
98 . ')' );
100 elsif ( defined $query->param('code') ) {
101 my $stateclaim = $query->param('state');
102 my $session = get_session( $query->cookie('CGISESSID') );
103 if ( $session->param('google-openid-state') ne $stateclaim ) {
104 $session->clear( ["google-openid-state"] );
105 $session->flush();
106 loginfailed( $query,
107 'Authentication failed. Your session has an unexpected state.' );
109 $session->clear( ["google-openid-state"] );
110 $session->flush();
112 my $code = $query->param('code');
113 my $ua = LWP::UserAgent->new();
114 if ( $tokenendpoint eq q{} ) {
115 loginfailed( $query, 'Unable to discover token endpoint.' );
117 my $request = POST(
118 $tokenendpoint,
120 code => $code,
121 client_id => $clientid,
122 client_secret => $clientsecret,
123 redirect_uri => $redirecturl,
124 grant_type => 'authorization_code',
125 $scope => $scope
128 my $response = $ua->request($request)->decoded_content;
129 my $json = decode_json($response);
130 if ( exists( $json->{'id_token'} ) ) {
131 if ( lc( $json->{'token_type'} ) ne 'bearer' ) {
132 loginfailed( $query,
133 'Authentication failed. Incorrect token type.' );
135 my $idtoken = $json->{'id_token'};
137 # Normally we'd have to validate the token - but google says not to worry here (Avoids another library!)
138 # See https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo for rationale
139 my @segments = split( '\.', $idtoken );
140 unless ( scalar(@segments) == 3 ) {
141 loginfailed( $query,
142 'Login token broken: either too many or too few segments.' );
144 my ( $header, $claims, $validation ) = @segments;
145 $claims = decode_base64url($claims);
146 my $claims_json = decode_json($claims);
147 if ( ( $claims_json->{'iss'} ne ( 'https://' . $issuer ) )
148 && ( $claims_json->{'iss'} ne $issuer ) )
150 loginfailed( $query,
151 "Authentication failed. Issuer of authentication isn't Google."
154 if ( ref( $claims_json->{'aud'} ) eq 'ARRAY' ) {
155 warn "Audience is an array of size: "
156 . scalar( @$claims_json->{'aud'} );
157 if ( scalar( @$claims_json->{'aud'} ) > 1 )
158 { # We don't want any other audiences
159 loginfailed( $query,
160 "Authentication failed. Unexpected audience provided." );
163 if ( ( $claims_json->{'aud'} ne $clientid )
164 || ( $claims_json->{'azp'} ne $clientid ) )
166 loginfailed( $query,
167 "Authentication failed. Unexpected audience." );
169 if ( $claims_json->{'exp'} < time() ) {
170 loginfailed( $query, 'Sorry, your authentication has timed out.' );
173 if ( exists( $claims_json->{'email'} ) ) {
174 my $email = $claims_json->{'email'};
175 if ( ( $restricttodomain ne q{} )
176 && ( index( $email, $restricttodomain ) < 0 ) )
178 loginfailed( $query,
179 'The email you have used is not valid for this library. Email addresses should conclude with '
180 . $restricttodomain
181 . ' .' );
183 else {
184 my $auto_registration = C4::Context->preference('GoogleOpenIDConnectAutoRegister') // q{0};
185 my $borrower = Koha::Patrons->find( { email => $email } );
186 if (! $borrower && $auto_registration==1) {
187 my $cardnumber = fixup_cardnumber();
188 my $firstname = $claims_json->{'given_name'} // q{};
189 my $surname = $claims_json->{'family_name'} // q{};
190 my $delimiter = $firstname ? q{.} : q{};
191 my $userid = $firstname . $delimiter . $surname;
192 my $categorycode = C4::Context->preference('GoogleOpenIDConnectDefaultCategory') // q{};
193 my $branchcode = C4::Context->preference('GoogleOpenIDConnectDefaultBranch') // q{};
194 my $password = undef;
195 my $borrowernumber = C4::Members::AddMember(
196 cardnumber => $cardnumber,
197 firstname => $firstname,
198 surname => $surname,
199 email => $email,
200 categorycode => $categorycode,
201 branchcode => $branchcode,
202 userid => $userid,
203 password => $password
205 $borrower = Koha::Patrons->find( {
206 borrowernumber => $borrowernumber } );
208 my ( $userid, $cookie, $session_id ) =
209 checkauth( $query, 1, {}, 'opac', $email );
210 if ($userid) { # A user with this email is registered in koha
211 print $query->redirect(
212 -uri => '/cgi-bin/koha/opac-user.pl',
213 -cookie => $cookie
216 else {
217 loginfailed( $query,
218 'The email address you are trying to use is not associated with a borrower at this library.'
223 else {
224 loginfailed( $query,
225 'Unexpectedly, no email seems to be associated with that acccount.'
229 else {
230 loginfailed( $query, 'Failed to get proper credentials from Google.' );
233 else {
234 my $session = get_session( $query->cookie('CGISESSID') );
235 my $openidstate = 'auth_';
236 $openidstate .= sprintf( "%x", rand 16 ) for 1 .. 32;
237 $session->param( 'google-openid-state', $openidstate );
238 $session->flush();
240 my $prompt = $query->param('reauthenticate') // q{};
241 if ( $authendpoint eq q{} ) {
242 loginfailed( $query, 'Unable to discover authorisation endpoint.' );
244 my $authorisationurl =
245 $authendpoint . '?'
246 . 'response_type=code&'
247 . 'redirect_uri='
248 . escape($redirecturl) . q{&}
249 . 'client_id='
250 . escape($clientid) . q{&}
251 . 'scope='
252 . escape($scope) . q{&}
253 . 'state='
254 . escape($openidstate);
255 if ( $prompt || ( defined $prompt && length $prompt > 0 ) ) {
256 $authorisationurl .= '&prompt=' . escape($prompt);
258 print $query->redirect($authorisationurl);