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

Last change on this file since 36023 was 36023, checked in by cstephen, 2 years ago

Migrated the GoogleSigninJDBCRealm to use a DataSourceRealm as a backing source.

The tomcat context file, greenstone3.xml, has been updated accordingly to setup the Realm correctly.

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