source: main/trunk/greenstone3/src/java/org/greenstone/gsdl3/GoogleSigninJDBCRealm.java@ 35350

Last change on this file since 35350 was 35350, checked in by davidb, 3 years ago

Code rewritten to avoid try-resource code pattern, as this is not available in JDKs before 1.8

File size: 15.8 KB
Line 
1/*
2 * GoogleSigninJDBCRealm.java
3 * Copyright (C) 2021 New Zealand Digital Library, http://www.nzdl.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but 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 this program; if not, write to the Free Software
17 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 */
19
20package org.greenstone.gsdl3;
21
22import java.security.Principal;
23import java.security.GeneralSecurityException;
24import java.security.SecureRandom;
25import java.sql.Connection;
26import java.sql.Driver;
27import java.sql.PreparedStatement;
28import java.sql.ResultSet;
29import java.sql.SQLException;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Collections;
33import java.util.Enumeration;
34import java.util.HashMap;
35import java.util.Hashtable;
36import java.util.Iterator;
37import java.util.List;
38import java.util.Map;
39
40import javax.servlet.http.HttpServletRequest;
41import javax.servlet.http.HttpServletResponse;
42import javax.servlet.http.HttpSession;
43
44import org.apache.catalina.realm.JDBCRealm;
45import org.apache.catalina.realm.GenericPrincipal;
46import org.apache.catalina.LifecycleException;
47import org.apache.juli.logging.Log;
48import org.apache.juli.logging.LogFactory;
49import org.apache.tomcat.util.ExceptionUtils;
50
51import com.google.api.client.http.HttpTransport;
52import com.google.api.client.http.javanet.NetHttpTransport;
53import com.google.api.client.json.JsonFactory;
54import com.google.api.client.json.gson.GsonFactory;
55import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
56import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
57import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
58
59
60import org.greenstone.gsdl3.util.GSParams;
61
62
63// Custome Realm class desgin loosely based off (in order) details in:
64// https://dzone.com/articles/how-to-implement-a-new-realm-in-tomcat
65// https://blog.krybot.com/a?ID=01300-14edb945-73b0-433b-8e80-c6870e350cf2
66// https://developers.redhat.com/blog/2017/06/20/how-to-implement-a-new-realm-in-tomcat
67
68// Example MBeans XML descriptor file, see:
69// https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/catalina/realm/mbeans-descriptors.xml.shtml
70// the one used in Greenstone is based off the JDBCRealm entry in mbeans-decriptors.xml found in the Tomcat source code
71//
72
73// In terms of addin in DEBUG statements, you need to trigger this through
74// tomcat/conf/logging.properies:
75// Otherwise even the 'old faithful' approach of printing all debug statements
76// to STDERR goes nowhere!
77//
78// Helpful details at:
79// https://stackoverflow.com/questions/30333709/how-to-debug-tomcat-ldap-realm-queries
80//
81// The key thing to add in to tomcat/conf/logging.properies is the following, and then
82// System.err.println() statements will turn up in tomcat/logs/catalina.out.
83// For simple debugging opting to use STDERR should be sufficient for most situations,
84// PLUS it has the added bonus of avoiding the issue of needing to add in the jar file(s)
85// for logging Tomcat uses into the Greenstone compile area
86
87/* Added to 'logging.properties'
88
89############################################################
90# Facility specific properties.
91# Provides extra control for each logger.
92############################################################
93# This would turn on trace-level for everything
94# the possible levels are: SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST or ALL
95#org.apache.catalina.level = ALL
96#org.apache.catalina.handlers = 2localhost.org.apache.juli.FileHandler
97org.apache.catalina.realm.level = ALL
98org.apache.catalina.realm.useParentHandlers = true
99org.apache.catalina.authenticator.level = ALL
100org.apache.catalina.authenticator.useParentHandlers = true
101
102org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
103org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.FileHandler
104
105*/
106
107
108public class GoogleSigninJDBCRealm extends JDBCRealm
109{
110
111 public static String GOOGLESIGNIN_USERNAME_BRIDGE = "googlesignin";
112
113 // MBean related components
114
115 /**
116 * The column name used for the user's email address
117 */
118 protected String userEmailCol = null;
119
120 /**
121 * @return the column name used for the user's email address
122 */
123 public String getUserEmailCol() {
124 return userEmailCol;
125 }
126
127 /**
128 * Set the column name used for the user's email address
129 *
130 * @param userEmailCol The column name used for the user's email address
131 */
132 public void setUserEmailCol( String userEmailCol ) {
133 this.userEmailCol = userEmailCol;
134 }
135
136 /**
137 * The Google Client API ID to use when trying verify users (e.g., via a Google ID Token)
138 */
139 protected String googlesigninClientId = null;
140
141 /**
142 * @return the Google Client API ID to use when trying verify users (e.g., via a Google ID Token)
143 */
144 public String getGooglesigninClientId() {
145 return googlesigninClientId;
146 }
147
148 /**
149 * Set the Google Client API ID to use when trying verify users (e.g., via a Google ID Token)
150 *
151 * @param googlesigninClientId The Google Client API ID
152 */
153 public void setGooglesigninClientId( String googlesigninClientId ) {
154 this.googlesigninClientId = googlesigninClientId;
155 }
156
157
158 /**
159 * The PreparedStatement to use for mapping an email address to a username
160 */
161 protected PreparedStatement preparedEmailToUsername = null;
162
163
164 protected static GoogleIdTokenVerifier google_id_token_verifier = null;
165
166 /**
167 * Prepare for the beginning of active use of the public methods of this
168 * component and implement the requirements of
169 * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
170 *
171 * @exception LifecycleException if this component detects a fatal error
172 * that prevents this component from being used
173 */
174 @Override
175 protected void startInternal() throws LifecycleException
176 {
177 super.startInternal();
178
179 initGoogleIdTokenVerifier(googlesigninClientId);
180 }
181
182 // **** XXXX !!!!
183 protected static void initGoogleIdTokenVerifier(String googlesignin_client_id)
184 {
185 // Based on:
186 // https://developers.google.com/identity/sign-in/web/backend-auth
187 // With some additional details cribbed from
188 // https://stackoverflow.com/questions/10835365/authenticate-programmatically-to-google-with-oauth2
189
190 //containerLog.debug("**** GoogleSigninJDBCRealm::initGoogleIdTokenVerifier():" + googlesignin_client_id);
191 System.err.println("**** GoogleSigninJDBCRealm::initGoogleIdTokenVerifier() googlesignin_client_id=" + googlesignin_client_id);
192 //GoogleSigninJDBCRealm.googlesignin_client_id = googlesignin_client_id;
193
194 HttpTransport transport = new NetHttpTransport();
195 JsonFactory jsonFactory = new GsonFactory();
196
197 List<String> googlesignin_client_ids = Collections.singletonList(googlesignin_client_id);
198
199 google_id_token_verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
200 //.setAudience(Collections.singletonList(googlesignin_client_id))
201 .setAudience(googlesignin_client_ids)
202 //.setAuthorizedParty(Collections.singletonList(googlesignin_clent_id))
203 .build();
204 }
205
206 /*
207 protected GoogleSigninJDBCRealm()
208 {
209 if (google_id_token_verifier == null) {
210 initGoogleIdTokenVerifier();
211 }
212 }
213 */
214
215
216 /**
217 * Return a PreparedStatement configured to perform the SELECT required
218 * to retrieve username for the specified user email address.
219 *
220 * @param dbConnection The database connection to be used
221 * @param emailAddress Email address for which username should be retrieved
222 * @return the prepared statement
223 * @exception SQLException if a database error occurs
224 */
225 protected PreparedStatement emailToUsername(Connection dbConnection,
226 String emailAddress)
227 throws SQLException
228 {
229 if (preparedEmailToUsername == null) {
230 StringBuilder sb = new StringBuilder("SELECT ");
231 sb.append(userNameCol);
232 sb.append(" FROM ");
233 sb.append(userTable);
234 sb.append(" WHERE ");
235 sb.append(userEmailCol);
236 sb.append(" = ?");
237
238 if(containerLog.isDebugEnabled()) {
239 containerLog.debug("emailToUsername query: " + sb.toString());
240 }
241
242 preparedEmailToUsername =
243 dbConnection.prepareStatement(sb.toString());
244 }
245
246 if (emailAddress == null) {
247 preparedEmailToUsername.setNull(1,java.sql.Types.VARCHAR);
248 } else {
249 preparedEmailToUsername.setString(1, emailAddress);
250 }
251
252 return preparedEmailToUsername;
253 }
254
255
256 /**
257 * Get the username for the specified email address
258 * @param email_address The email address
259 * @return the username associated with the given principal's email address
260 */
261 protected synchronized String getUsernameFromEmail(String email_address) {
262
263 // Look up the username
264 String dbUsername = null;
265
266 // Number of tries is the number of attempts to connect to the database
267 // during this login attempt (if we need to open the database)
268 // This needs rewritten with better pooling support, the existing code
269 // needs signature changes since the Prepared statements needs cached
270 // with the connections.
271 // The code below will try twice if there is an SQLException so the
272 // connection may try to be opened again. On normal conditions (including
273 // invalid login - the above is only used once.
274 int numberOfTries = 2;
275
276 // Note: The following code is based on that in JDBCRealm for running SQL queries,
277 // however, it has by changed from the try-resource code pattern to using
278 // to a more explictly laid out version so it is compatible with versions
279 // of JDK prior to 1.8
280
281 ResultSet rs = null;
282 while (numberOfTries > 0) {
283 try {
284 // Ensure that we have an open database connection
285 open();
286
287 PreparedStatement stmt = emailToUsername(dbConnection, email_address);
288 rs = stmt.executeQuery();
289
290 if (rs.next()) {
291 dbUsername = rs.getString(1);
292 }
293
294 dbConnection.commit();
295
296 if (dbUsername != null) {
297 dbUsername = dbUsername.trim();
298 }
299
300 rs.close();
301 rs = null;
302
303 return dbUsername;
304 }
305 catch (SQLException e) {
306 // Log the problem for posterity
307 containerLog.error(sm.getString("jdbcRealm.exception"), e);
308
309 }
310
311 if (rs != null) {
312 try {
313 rs.close();
314 }
315 catch (SQLException e) {
316 containerLog.error(sm.getString("jdbcRealm.exception trying to close() ResultSet"), e);
317 }
318 rs = null;
319 }
320
321 // Close the connection so that it gets reopened next time
322 if (dbConnection != null) {
323 close(dbConnection);
324 }
325
326 numberOfTries--;
327 }
328
329 return null;
330 }
331
332 protected String mapFromGoogleEmailToGreenstoneUser(String google_email)
333 {
334 String greenstone_username = null;
335
336 return greenstone_username;
337 }
338
339 public String getGreenstoneUsernameFromGoogleTokenId(String googlesignin_id_token)
340 {
341 System.err.println("**** GoogleSigninJDBCRealm::getGreenstoneUsernameFromGoogleTokenId():" + googlesignin_id_token);
342
343 String greenstone_username = null;
344
345 if (googlesignin_id_token != null) {
346 try {
347 GoogleIdToken idToken = google_id_token_verifier.verify(googlesignin_id_token);
348
349 if (idToken != null) {
350 Payload payload = idToken.getPayload();
351
352 // Get profile information from payload
353 String google_user = payload.getSubject();
354 String google_user_email = payload.getEmail();
355 boolean verified = Boolean.valueOf(payload.getEmailVerified());
356
357 //String name = (String) payload.get("name");
358 //String pictureUrl = (String) payload.get("picture");
359 //String locale = (String) payload.get("locale");
360 //String familyName = (String) payload.get("family_name");
361 //String givenName = (String) payload.get("given_name");
362
363
364 if (verified) {
365 greenstone_username = getUsernameFromEmail(google_user_email);
366 if (greenstone_username == null) {
367 System.err.println("Google login successful with verified email address '"+google_user_email+"' HOWEVER no matching email entry fround in Greenstone JDBC UserTable");
368 }
369 }
370 else {
371 System.err.println("Google login successful, but email address '"+google_user_email+"' not verified by Google => Reject login attempt for Greenstone");
372 }
373
374 }
375 else {
376 System.err.println("Could not verify Google ID token: '" + googlesignin_id_token + "'");
377 }
378 }
379 catch (Exception e) {
380 System.err.println("Exception thrown when verifing Google ID token: '" + googlesignin_id_token + "'");
381 e.printStackTrace();
382
383 }
384 }
385 else {
386 System.err.println("***** No googlesignin_id_token detected. No Google Signin check to do");
387 }
388
389 System.err.println("***** End of getGoogleSinginInfo()");
390
391 return greenstone_username;
392 }
393
394 // **** !!!! XXXX
395
396 @Override
397 public Principal authenticate(String username, String credentials)
398 {
399 Principal principal = null;
400
401 if (username.equals(GOOGLESIGNIN_USERNAME_BRIDGE)) {
402 System.err.println("GoogleSigninJDBCRealm::authenticate(): detected googlesignin");
403
404 // System.err.println("***** google_id_token_verifier = " + google_id_token_verifier);
405 //System.err.println("GoogleSigninJDBCReal::authenticate(): username=" + username);
406 //System.err.println("GoogleSigninJDBCReal::authenticate(): credentials=" + credentials);
407
408 // Google Client Token ID has been passed in as 'credentials'
409 String google_user_email = getGreenstoneUsernameFromGoogleTokenId(credentials);
410
411 if (google_user_email != null) {
412 String google_username = google_user_email.replaceAll("@.*$","");
413 System.err.println("**** Using the following username derived from verified Google email address as Greenstone3 username = '" + google_username + "'");
414
415 principal = super.getPrincipal(google_username);
416 }
417 else {
418 System.err.println("GoogleSigninJDBCRealm::authenticate(): failed to match 'google_id_token' to valid Greenstone user account");
419 }
420 }
421 else {
422 // Regular Greenstone3 User Login case
423 principal = super.authenticate(username,credentials);
424 }
425
426 return principal;
427 }
428
429
430 /**
431 * Close the specified database connection.
432 *
433 * @param dbConnection The connection to be closed
434 */
435 protected void close(Connection dbConnection) {
436
437 // Do nothing if the database connection is already closed
438 if (dbConnection == null) {
439 return;
440 }
441
442 // Close our prepared statements (if any)
443 try {
444 preparedEmailToUsername.close();
445 } catch (Throwable f) {
446 ExceptionUtils.handleThrowable(f);
447 }
448 this.preparedEmailToUsername = null;
449
450 super.close(dbConnection);
451 }
452
453 /*
454 @Override
455 protected Principal getPrincipal(String string)
456 {
457 List<String> roles = new ArrayList<String>();
458
459 roles.add("TomcatAdmin"); // Adding role "TomcatAdmin" role to the user
460 //logger.info("Realm: "+this);
461 System.err.println("Realm: "+this);
462
463 Principal principal = new GenericPrincipal(username, password, roles);
464 //logger.info("Principal: "+principal);
465 System.err.println("Principal: "+principal);
466
467 return principal;
468 }
469 */
470
471}
Note: See TracBrowser for help on using the repository browser.