001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package service.tut.pori.users.google; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.List; 021 022import javax.servlet.http.HttpSession; 023 024import org.apache.commons.lang3.RandomStringUtils; 025import org.apache.commons.lang3.StringUtils; 026import org.apache.http.client.entity.UrlEncodedFormEntity; 027import org.apache.http.client.methods.HttpGet; 028import org.apache.http.client.methods.HttpPost; 029import org.apache.http.impl.client.BasicResponseHandler; 030import org.apache.http.impl.client.CloseableHttpClient; 031import org.apache.http.impl.client.HttpClients; 032import org.apache.http.message.BasicNameValuePair; 033import org.apache.log4j.Logger; 034import org.springframework.context.ApplicationListener; 035 036import service.tut.pori.users.UserCore; 037import service.tut.pori.users.UserCore.Registration; 038import service.tut.pori.users.UserCore.RegistrationStatus; 039import service.tut.pori.users.UserServiceEvent; 040import service.tut.pori.users.google.Definitions.OAuth2GrantType; 041import core.tut.pori.context.ServiceInitializer; 042import core.tut.pori.http.RedirectResponse; 043import core.tut.pori.http.Response; 044import core.tut.pori.http.Response.Status; 045import core.tut.pori.users.ExternalAccountConnection.UserServiceType; 046import core.tut.pori.users.UserEvent.EventType; 047import core.tut.pori.users.ExternalAccountConnection; 048import core.tut.pori.users.UserEvent; 049import core.tut.pori.users.UserIdentity; 050 051/** 052 * Google User core methods. 053 * 054 * Google scope apis: 055 * - https://gist.github.com/comewalk/5457791 056 * - http://www.subinsb.com/2013/04/list-google-oauth-scopes.html 057 * 058 * Implemented according to: https://developers.google.com/accounts/docs/OAuth2WebServer 059 * 060 * This requires that matching (valid) parameters have been set in the system properties file, 061 * and in the Google API Console. 062 * 063 * This class emits events of type {@link core.tut.pori.users.UserEvent} for user account modifications with one of the listed {@link core.tut.pori.users.UserEvent.EventType} : 064 * <ul> 065 * <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_GIVEN} for new user account authorizations.</li> 066 * <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_REVOKED} for removed user account authorizations.</li> 067 * </ul> 068 */ 069public final class GoogleUserCore { 070 private static final Logger LOGGER = Logger.getLogger(GoogleUserCore.class); 071 private static final String PARAMETER_VALUE_ACCESS_TYPE = "offline"; 072 private static final String PARAMETER_VALUE_APPROVAL_PROMPT = "force"; 073 private static final String PARAMETER_VALUE_RESPONSE_TYPE = "code"; 074 075 /** 076 * 077 */ 078 private GoogleUserCore(){ 079 // nothing needed 080 } 081 082 /** 083 * 084 * @param authorizationCode 085 * @param errorCode 086 * @param nonce 087 * @return response 088 */ 089 public static Response processOAuth2Callback(String authorizationCode, String errorCode, String nonce){ 090 if(StringUtils.isBlank(nonce)){ 091 LOGGER.debug("nonce is missing."); 092 return new Response(Status.BAD_REQUEST); 093 } 094 095 GoogleUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(GoogleUserDAO.class); 096 097 if(!StringUtils.isEmpty(errorCode)){ 098 dao.removeNonce(nonce); // in any case, do not allow to use this nonce again 099 LOGGER.debug("Received callback request with errorCode: "+errorCode); 100 return new Response(Status.OK, errorCode); 101 } 102 103 UserIdentity userId = dao.getUser(nonce); 104 if(userId == null){ // the nonce do not exist or has expired 105 LOGGER.debug("Received callback request with expired or invalid nonce: "+nonce); 106 return new Response(Status.BAD_REQUEST); 107 } 108 109 dao.removeNonce(nonce); // in any case, do not allow to use this nonce again 110 111 if(StringUtils.isBlank(authorizationCode)){ 112 LOGGER.debug("no authorization code."); 113 return new Response(Status.BAD_REQUEST); 114 } 115 116 try (CloseableHttpClient client = HttpClients.createDefault()) { 117 GoogleProperties gp = ServiceInitializer.getPropertyHandler().getSystemProperties(GoogleProperties.class); 118 119 List<BasicNameValuePair> params = new ArrayList<>(); 120 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_AUTHORIZATION_CODE, authorizationCode)); 121 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_CLIENT_ID, gp.getClientId())); 122 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_CLIENT_SECRET, gp.getClientSecret())); 123 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_GRANT_TYPE, OAuth2GrantType.authorization_code.toOAuth2GrantType())); 124 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_REDIRECT_URI, gp.getoAuth2RedirectUri())); 125 126 HttpPost post = new HttpPost(gp.getoAuth2Uri()+Definitions.METHOD_GOOGLE_TOKEN); 127 post.setEntity(new UrlEncodedFormEntity(params)); 128 129 OAuth2Token newToken = (OAuth2Token.getTokenGSONSerializer()).fromJson(client.execute(post, new BasicResponseHandler()), OAuth2Token.class); 130 if(newToken != null && newToken.isValid()){ 131 GoogleCredential gc = getCredential(newToken.getAccessToken()); 132 if(gc == null){ 133 LOGGER.error("Failed to resolve credentials for the new token."); 134 return new Response(Status.INTERNAL_SERVER_ERROR); 135 } 136 137 if(!dao.setToken(gc.getId(), newToken, userId)){ 138 LOGGER.warn("Failed to set new token."); 139 return new Response(Status.BAD_REQUEST); 140 } 141 142 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(GoogleUserCore.class, userId, EventType.USER_AUTHORIZATION_GIVEN)); 143 }else{ 144 LOGGER.debug("Did not receive a valid token."); 145 } 146 } catch (IOException ex) { 147 LOGGER.error(ex, ex); 148 } 149 150 return new Response(); 151 } 152 153 /** 154 * Return token for the given userId, refreshing the old token if necessary 155 * 156 * Note: if the token expires, you should use this method to retrieve a new one, refreshing the token 157 * manually may cause race condition with other services using the tokens (there can be only one active 158 * and valid access token at any time) 159 * 160 * @param authorizedUser 161 * @return the token or null if none 162 */ 163 public static OAuth2Token getToken(UserIdentity authorizedUser){ 164 GoogleUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(GoogleUserDAO.class); 165 OAuth2Token token = dao.getToken(authorizedUser); 166 if(token == null){ 167 return null; 168 } 169 String googleUserId = dao.getGoogleUserId(authorizedUser); 170 if(googleUserId == null){ 171 LOGGER.warn("Failed to resolve google user id."); 172 return null; 173 } 174 175 if(token.isExpired()){ 176 token = refreshToken(token); 177 if(token != null){ 178 dao.setToken(googleUserId, token, authorizedUser); 179 }else{ 180 dao.removeToken(authorizedUser); 181 } 182 } 183 return token; 184 } 185 186 /** 187 * 188 * @param token 189 * @return the refreshed token or null on failure, Note: this is not the same token as was passed as a parameter 190 */ 191 private static OAuth2Token refreshToken(OAuth2Token token){ 192 String refreshToken = token.getRefreshToken(); 193 if(refreshToken == null){ 194 LOGGER.debug("No refresh_token provided."); 195 return null; 196 } 197 198 try (CloseableHttpClient client = HttpClients.createDefault()) { 199 GoogleProperties gp = ServiceInitializer.getPropertyHandler().getSystemProperties(GoogleProperties.class); 200 201 List<BasicNameValuePair> params = new ArrayList<>(); 202 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_CLIENT_ID, gp.getClientId())); 203 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_CLIENT_SECRET, gp.getClientSecret())); 204 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_GRANT_TYPE, OAuth2GrantType.refresh_token.toOAuth2GrantType())); 205 params.add(new BasicNameValuePair(Definitions.PARAMETER_OAUTH2_REFRESH_TOKEN, refreshToken)); 206 207 HttpPost post = new HttpPost(gp.getoAuth2Uri()+Definitions.METHOD_GOOGLE_TOKEN); 208 post.setEntity(new UrlEncodedFormEntity(params)); 209 210 OAuth2Token newToken = (OAuth2Token.getTokenGSONSerializer()).fromJson(client.execute(post, new BasicResponseHandler()), OAuth2Token.class); 211 if(!newToken.isExpired()){ 212 newToken.setRefreshToken(refreshToken); // set the used refresh token to the new token 213 return newToken; 214 } 215 } catch (IOException ex) { 216 LOGGER.error(ex, ex); 217 } 218 return null; 219 } 220 221 /** 222 * revokes the access_token and refresh_token if any are present and sets the member values to null (on success) 223 * 224 * This will not remove the token from the database, use the overloaded version if for that. 225 * 226 * @param token 227 */ 228 private static void revokeToken(OAuth2Token token){ 229 if(token == null){ 230 LOGGER.debug("null token."); 231 return; 232 } 233 String accessToken = token.getAccessToken(); 234 String refreshToken = token.getRefreshToken(); 235 if(accessToken == null && refreshToken == null){ 236 LOGGER.debug("No token values."); 237 return; 238 } 239 try (CloseableHttpClient client = HttpClients.createDefault()) { 240 GoogleProperties gp = ServiceInitializer.getPropertyHandler().getSystemProperties(GoogleProperties.class); 241 BasicResponseHandler h = new BasicResponseHandler(); 242 if(accessToken != null){ 243 LOGGER.debug("Server responded :"+client.execute(new HttpGet(gp.getoAuth2Uri()+Definitions.METHOD_GOOGLE_REVOKE+"?"+Definitions.PARAMETER_GOOGLE_TOKEN+"="+accessToken),h)); 244 } 245 token.setAccessToken(null); 246 if(refreshToken != null){ 247 LOGGER.debug("Server responded :"+client.execute(new HttpGet(gp.getoAuth2Uri()+Definitions.METHOD_GOOGLE_REVOKE+"?"+Definitions.PARAMETER_GOOGLE_TOKEN+"="+refreshToken),h)); 248 } 249 token.setRefreshToken(null); 250 } catch (IOException ex) { // simply catch the exception, unfortunately we cannot do much if revoke fails 251 LOGGER.error(ex, ex); 252 } 253 } 254 255 /** 256 * 257 * @param authorizedUser valid user id 258 * @return redirection response 259 */ 260 public static Response createAuthorizationRedirection(UserIdentity authorizedUser){ 261 GoogleProperties gp = ServiceInitializer.getPropertyHandler().getSystemProperties(GoogleProperties.class); 262 StringBuilder uri = new StringBuilder(gp.getoAuth2Uri()); 263 uri.append(Definitions.METHOD_GOOGLE_AUTH+"?"+Definitions.PARAMETER_OAUTH2_SCOPE+"="); 264 uri.append(gp.getoAuth2Scope()); 265 266 uri.append("&"+Definitions.PARAMETER_OAUTH2_STATE+"="); 267 268 uri.append(ServiceInitializer.getDAOHandler().getSQLDAO(GoogleUserDAO.class).generateNonce(authorizedUser)); 269 270 uri.append("&"+Definitions.PARAMETER_OAUTH2_REDIRECT_URI+"="); 271 uri.append(gp.getEncodedOAuth2RedirectUri()); 272 273 uri.append("&"+Definitions.PARAMETER_OAUTH2_CLIENT_ID+"="); 274 uri.append(gp.getClientId()); 275 276 uri.append("&"+Definitions.PARAMETER_GOOGLE_ACCESS_TYPE+"="+PARAMETER_VALUE_ACCESS_TYPE+"&"+Definitions.PARAMETER_OAUTH2_RESPONSE_TYPE+"="+PARAMETER_VALUE_RESPONSE_TYPE+"&"+Definitions.PARAMETER_GOOGLE_APPROVAL_PROMPT+"="+PARAMETER_VALUE_APPROVAL_PROMPT); 277 278 String redirectUri = uri.toString(); 279 LOGGER.debug("Redirecting authorization request to: "+redirectUri); 280 return new RedirectResponse(redirectUri); 281 } 282 283 /** 284 * 285 * @param userIdentity 286 * @return response 287 */ 288 public static Response removeAuthorization(UserIdentity userIdentity) { 289 Response r = revoke(userIdentity); 290 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(GoogleUserCore.class, userIdentity, EventType.USER_AUTHORIZATION_REVOKED)); // send revoked event, this should trigger clean up on all relevant services 291 return r; 292 } 293 294 /** 295 * 296 * @param userId 297 * @return response 298 */ 299 public static Response revoke(UserIdentity userId) { 300 if(!UserIdentity.isValid(userId)){ // should not be called with invalid userId 301 LOGGER.warn("Invalid "+UserIdentity.class.toString()); 302 return new Response(Status.INTERNAL_SERVER_ERROR); 303 } 304 GoogleUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(GoogleUserDAO.class); 305 306 OAuth2Token token = dao.getToken(userId); 307 if(token == null){ 308 LOGGER.debug("No token found for user, id: "+userId.getUserId()); 309 }else{ 310 revokeToken(token); 311 dao.removeToken(userId); 312 } 313 314 return new Response(); 315 } 316 317 /** 318 * 319 * @param userId 320 * @return GoogleCredential for the requested userId or null if none available 321 */ 322 public static GoogleCredential getCredential(UserIdentity userId){ 323 if(!UserIdentity.isValid(userId)){ 324 LOGGER.warn("Given userId was not valid."); 325 return null; 326 } 327 328 OAuth2Token token = GoogleUserCore.getToken(userId); 329 if(token == null){ 330 LOGGER.debug("User does not have valid Google credentials."); 331 return null; 332 } 333 334 GoogleCredential credential = getCredential(token.getAccessToken()); 335 if(credential == null){ 336 LOGGER.debug("Failed to resolve credentials."); 337 }else{ 338 credential.setUserId(userId); 339 } 340 return credential; 341 } 342 343 /** 344 * Helper method for retrieving credentials from google servers 345 * 346 * @param accessToken 347 * @return credentials for the given token or null if none was found 348 */ 349 private static GoogleCredential getCredential(String accessToken){ 350 GoogleCredential credential = null; 351 try(CloseableHttpClient client = HttpClients.createDefault()){ 352 GoogleProperties gp = ServiceInitializer.getPropertyHandler().getSystemProperties(GoogleProperties.class); 353 credential = (OAuth2Token.getTokenGSONSerializer()).fromJson(client.execute(new HttpGet(gp.getoAuth2UserInfoUri()+Definitions.METHOD_GOOGLE_USER_INFO+"?alt=json&"+Definitions.PARAMETER_OAUTH2_ACCESS_TOKEN+"="+accessToken), new BasicResponseHandler()), GoogleCredential.class); 354 } catch (IOException ex) { 355 LOGGER.error(ex, ex); 356 } 357 358 return credential; 359 } 360 361 /** 362 * 363 * @param session 364 * @param accessToken 365 * @return response 366 */ 367 public static Response login(HttpSession session, String accessToken) { 368 GoogleCredential credential = getCredential(accessToken); 369 if(credential == null){ 370 return new Response(Status.BAD_REQUEST, "Failed to process the given token."); 371 } 372 373 UserIdentity userId = UserCore.getUserId(new ExternalAccountConnection(credential.getId(), UserServiceType.GOOGLE)); 374 if(!UserIdentity.isValid(userId)){ 375 return new Response(Status.FORBIDDEN, "The given Google user is not registered with this service, please register before login."); 376 } 377 378 ServiceInitializer.getSessionHandler().registerAndAuthenticate(session.getId(), userId); 379 return new Response(); 380 } 381 382 /** 383 * 384 * @param accessToken 385 * @return response 386 */ 387 public static Response register(String accessToken) { 388 GoogleCredential credential = getCredential(accessToken); 389 if(credential == null){ 390 return new Response(Status.BAD_REQUEST, "Failed to process the given token."); 391 } 392 393 ExternalAccountConnection connection = new ExternalAccountConnection(credential.getId(), UserServiceType.GOOGLE); 394 UserIdentity userId = UserCore.getUserId(connection); 395 if(userId != null){ 396 return new Response(Status.BAD_REQUEST, "The user is already registered with this service."); 397 } 398 399 Registration registration = new Registration(); 400 String googleUserId = credential.getId(); 401 registration.setUsername(UserServiceType.GOOGLE.name()+googleUserId); // use prefix to prevent collisions with other services 402 registration.setPassword(RandomStringUtils.randomAlphanumeric(50)); // create a random password for this account 403 404 RegistrationStatus status = UserCore.createUser(registration); // register as new user 405 if(status != RegistrationStatus.OK){ 406 return new Response(Status.BAD_REQUEST, "Failed to create new user: "+status.name()); 407 } 408 409 connection.setExternalId(googleUserId); 410 UserCore.insertExternalAccountConnection(connection, registration.getRegisteredUserId()); // link the created user to the given account 411 return new Response(); 412 } 413 414 /** 415 * Event listener for user related events. 416 * 417 * Automatically instantiated by Spring as a bean. 418 */ 419 @SuppressWarnings("unused") 420 private static class UserEventListener implements ApplicationListener<UserServiceEvent>{ 421 422 @Override 423 public void onApplicationEvent(UserServiceEvent event) { 424 EventType type = event.getType(); 425 if(type == EventType.USER_REMOVED || (type == EventType.USER_AUTHORIZATION_REVOKED && event.getSource().equals(UserCore.class) && UserServiceType.GOOGLE.equals(event.getUserServiceType()))){ 426 UserIdentity userId = event.getUserId(); 427 LOGGER.debug("Detected event of type "+type.name()+", removing tokens for user, id: "+userId.getUserId()); 428 ServiceInitializer.getDAOHandler().getSQLDAO(GoogleUserDAO.class).removeToken(userId); 429 430 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(GoogleUserCore.class, userId, EventType.USER_AUTHORIZATION_REVOKED)); // send revoked event, this should trigger clean up on all relevant services 431 } 432 } 433 } // class UserEventListener 434}