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}