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.facebook; 017 018import java.io.IOException; 019 020import javax.servlet.http.HttpSession; 021 022import org.apache.commons.lang3.RandomStringUtils; 023import org.apache.commons.lang3.StringUtils; 024import org.apache.commons.lang3.tuple.Pair; 025import org.apache.http.client.methods.HttpGet; 026import org.apache.http.impl.client.BasicResponseHandler; 027import org.apache.http.impl.client.CloseableHttpClient; 028import org.apache.http.impl.client.HttpClients; 029import org.apache.log4j.Logger; 030import org.springframework.context.ApplicationListener; 031 032import service.tut.pori.users.UserCore; 033import service.tut.pori.users.UserCore.Registration; 034import service.tut.pori.users.UserCore.RegistrationStatus; 035import service.tut.pori.users.UserServiceEvent; 036import service.tut.pori.users.google.OAuth2Token; 037import core.tut.pori.context.ServiceInitializer; 038import core.tut.pori.http.RedirectResponse; 039import core.tut.pori.http.Response; 040import core.tut.pori.http.Response.Status; 041import core.tut.pori.users.ExternalAccountConnection; 042import core.tut.pori.users.UserEvent; 043import core.tut.pori.users.ExternalAccountConnection.UserServiceType; 044import core.tut.pori.users.UserEvent.EventType; 045import core.tut.pori.users.UserIdentity; 046 047/** 048 * Facebook User Service Core methods. 049 * 050 * This implementation follows: 051 * 052 * refreshing tokens: 053 * http://developers.facebook.com/docs/facebook-login/access-tokens/#extending 054 * http://stackoverflow.com/questions/16563692/refresh-token-and-access-token-in-facebook-api 055 * 056 * http://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/ 057 * 058 * 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} : 059 * <ul> 060 * <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_GIVEN} for new user account authorizations.</li> 061 * <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_REVOKED} for removed user account authorizations.</li> 062 * </ul> 063 */ 064public final class FacebookUserCore { 065 private static final Logger LOGGER = Logger.getLogger(FacebookUserCore.class); 066 private static final char DELIMITER_PARAMETER_VALUE = '='; 067 private static final char DELIMITER_PARAMETERS = '&'; 068 private static final String PARAMETER_ACCESS_TOKEN = Definitions.PARAMETER_OAUTH2_ACCESS_TOKEN; // facebook specific body-parameter 069 private static final String PARAMETER_EXPIRES = "expires"; // facebook specific body parameter 070 private static final String PARAMETER_VALUE_REFRESH_GRANT_TYPE = "fb_exchange_token"; 071 private static final String PARAMETER_VALUE_RESPONSE_TYPE = "code"; 072 073 074 /** 075 * 076 */ 077 private FacebookUserCore(){ 078 // nothing needed 079 } 080 081 /** 082 * 083 * @param authorizationCode 084 * @param errorCode 085 * @param nonce 086 * @return response 087 */ 088 public static Response processOAuth2Callback(String authorizationCode, String errorCode, String nonce) { 089 if(StringUtils.isBlank(nonce)){ 090 LOGGER.debug("nonce is missing."); 091 return new Response(Status.BAD_REQUEST); 092 } 093 094 //Try to split nonce for a redirection URL 095 Pair<String, String> nonceAndUrl = UserCore.getNonceAndRedirectUri(nonce); 096 String redirectUri = null; 097 if(nonceAndUrl != null){ 098 nonce = nonceAndUrl.getLeft(); 099 redirectUri = nonceAndUrl.getRight(); 100 }else{ 101 redirectUri = null; 102 } 103 104 FacebookUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookUserDAO.class); 105 106 if(!StringUtils.isEmpty(errorCode)){ 107 dao.removeNonce(nonce); // in any case, do not allow to use this nonce again 108 LOGGER.debug("Received callback request with errorCode: "+errorCode); 109 if(StringUtils.isEmpty(redirectUri)){ 110 return new Response(Status.OK, errorCode); 111 }else if(redirectUri.contains("?")){ 112 return new RedirectResponse(redirectUri+"&error="+errorCode); 113 }else{ 114 return new RedirectResponse(redirectUri+"?error="+errorCode); 115 } 116 } 117 118 UserIdentity userId = dao.getUser(nonce); 119 if(userId == null){ // the nonce do not exist or has expired 120 LOGGER.debug("Received callback request with expired or invalid nonce: "+nonce); 121 return new Response(Status.BAD_REQUEST); 122 } 123 124 dao.removeNonce(nonce); // in any case, do not allow to use this nonce again 125 126 if(StringUtils.isBlank(authorizationCode)){ 127 LOGGER.debug("no authorization code."); 128 return new Response(Status.BAD_REQUEST); 129 } 130 131 try (CloseableHttpClient client = HttpClients.createDefault()) { 132 FacebookProperties fp = ServiceInitializer.getPropertyHandler().getSystemProperties(FacebookProperties.class); 133 StringBuilder uri = new StringBuilder(fp.getoAuth2TokenUri()); 134 uri.append(Definitions.METHOD_FACEBOOK_ACCESS_TOKEN+"?"+Definitions.PARAMETER_OAUTH2_CLIENT_ID+"="); 135 uri.append(fp.getApplicationId()); 136 137 uri.append("&"+Definitions.PARAMETER_OAUTH2_REDIRECT_URI+"="); 138 uri.append(fp.getEncodedOAuth2RedirectUri()); 139 140 uri.append("&"+Definitions.PARAMETER_OAUTH2_CLIENT_SECRET+"="); 141 uri.append(fp.getSharedKey()); 142 143 uri.append("&"+Definitions.PARAMETER_OAUTH2_AUTHORIZATION_CODE+"="); 144 uri.append(authorizationCode); 145 146 OAuth2Token newToken = fromResponse(client.execute(new HttpGet(uri.toString()), new BasicResponseHandler())); 147 if(newToken != null && newToken.isValid()){ 148 FacebookCredential fc = getCredential(newToken.getAccessToken()); 149 if(fc == null){ 150 LOGGER.error("Failed to resolve credentials for the new token."); 151 return new Response(Status.INTERNAL_SERVER_ERROR); 152 } 153 154 if(!dao.setToken(fc.getId(), newToken, userId)){ 155 LOGGER.warn("Failed to set new token."); 156 return new Response(Status.BAD_REQUEST); 157 } 158 159 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(FacebookUserCore.class, userId, EventType.USER_AUTHORIZATION_GIVEN)); 160 }else{ 161 LOGGER.debug("Did not receive a valid token."); 162 } 163 } catch (IOException ex) { 164 LOGGER.error(ex, ex); 165 } 166 167 if(StringUtils.isEmpty(redirectUri)){ 168 return new Response(); 169 }else{ 170 return new RedirectResponse(redirectUri); 171 } 172 } 173 174 /** 175 * parses a facebook token response of format: access_token=ACCESS_TOKEN&expires=SECONDS_TILL_EXPIRATION 176 * 177 * @param response 178 * @return the token or null if invalid string 179 */ 180 private static OAuth2Token fromResponse(String response){ 181 String[] params = StringUtils.split(response, DELIMITER_PARAMETERS); 182 if(params == null || params.length < 2){ 183 LOGGER.debug("Invalid response: "+response); 184 return null; 185 } 186 String accessToken = null; 187 Long expiresIn = null; 188 try{ 189 for(int i=0;i<params.length;++i){ 190 String[] parts = StringUtils.split(params[i],DELIMITER_PARAMETER_VALUE); 191 if(parts.length != 2){ 192 LOGGER.debug("Ignored invalid parameter: "+parts[i]); 193 }else if(PARAMETER_EXPIRES.equalsIgnoreCase(parts[0])){ 194 expiresIn = Long.valueOf(parts[1]); 195 }else if(PARAMETER_ACCESS_TOKEN.equalsIgnoreCase(parts[0])){ 196 accessToken = parts[1]; 197 } 198 } 199 } catch (NumberFormatException ex){ 200 LOGGER.error(ex, ex); 201 } 202 203 if(expiresIn == null || accessToken == null){ 204 LOGGER.debug("Did not receive valid token. "+PARAMETER_ACCESS_TOKEN+": "+accessToken+", "+PARAMETER_EXPIRES+": "+expiresIn); 205 return null; 206 } 207 208 OAuth2Token token = new OAuth2Token(); 209 token.setAccessToken(accessToken); 210 token.setExpiresIn(expiresIn); 211 return token; 212 } 213 214 /** 215 * 216 * @param authorizedUser valid user id 217 * @param redirectUri 218 * @return redirection response 219 */ 220 public static Response createAuthorizationRedirection(UserIdentity authorizedUser, String redirectUri) { 221 FacebookProperties fp = ServiceInitializer.getPropertyHandler().getSystemProperties(FacebookProperties.class); 222 StringBuilder uri = new StringBuilder(fp.getoAuth2AuthUri()); 223 224 uri.append(Definitions.METHOD_FACEBOOK_AUTH+"?"+Definitions.PARAMETER_OAUTH2_SCOPE+"="); 225 uri.append(fp.getoAuth2Scope()); 226 227 uri.append("&"+Definitions.PARAMETER_OAUTH2_STATE+"="); 228 uri.append(UserCore.urlEncodedCombinedNonce(ServiceInitializer.getDAOHandler().getSQLDAO(FacebookUserDAO.class).generateNonce(authorizedUser), redirectUri)); 229 230 uri.append("&"+Definitions.PARAMETER_OAUTH2_REDIRECT_URI+"="); 231 uri.append(fp.getEncodedOAuth2RedirectUri()); 232 233 uri.append("&"+Definitions.PARAMETER_OAUTH2_CLIENT_ID+"="); 234 uri.append(fp.getApplicationId()); 235 236 uri.append("&"+Definitions.PARAMETER_OAUTH2_RESPONSE_TYPE+"="+PARAMETER_VALUE_RESPONSE_TYPE); 237 238 return new RedirectResponse(uri.toString()); 239 } 240 241 /** 242 * 243 * @param authenticatedUser 244 * @return response 245 */ 246 public static Response removeAuthorization(UserIdentity authenticatedUser) { 247 ServiceInitializer.getDAOHandler().getSQLDAO(FacebookUserDAO.class).removeToken(authenticatedUser); // simply remove the token, as it is not possible to revoke facebook tokens, they should auto-expire after a certain time 248 249 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(FacebookUserCore.class, authenticatedUser, EventType.USER_AUTHORIZATION_REVOKED)); // send revoked event, this should trigger clean up on all relevant services 250 return null; 251 } 252 253 /** 254 * Retrieves the current token for the user if one is available 255 * 256 * Note: if the token expires, you should use this method to retrieve a new one, refreshing the token 257 * manually may cause race condition with other services using the tokens (there can be only one active 258 * and valid access token at any time) 259 * 260 * @param authorizedUser 261 * @return token for the given user or null if not found 262 */ 263 public static OAuth2Token getToken(UserIdentity authorizedUser){ 264 FacebookUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookUserDAO.class); 265 OAuth2Token token = dao.getToken(authorizedUser); 266 if(token == null){ 267 return null; 268 } 269 270 String facebookUserId = dao.getFacebookUserId(authorizedUser); 271 if(facebookUserId == null){ 272 LOGGER.warn("Failed to resolve facebook user id."); 273 return null; 274 } 275 276 if(token.expiresIn(ServiceInitializer.getPropertyHandler().getSystemProperties(FacebookProperties.class).getTokenAutorefresh())){ // Facebook does not support refreshing tokens, so if a token has expired there really isn't much we can do 277 token = refreshToken(token); 278 if(token == null){ 279 dao.removeToken(authorizedUser); // remove the invalid token from the database 280 }else{ 281 dao.setToken(facebookUserId, token, authorizedUser); 282 } 283 } 284 return token; 285 } 286 287 /** 288 * 289 * @param token 290 * @return the refreshed token or null on failure, Note: the returned token may not be the same object as the passed object 291 */ 292 private static OAuth2Token refreshToken(OAuth2Token token){ 293 if(token.isExpired()){ 294 LOGGER.debug("Expired or invalid token given."); 295 return null; 296 } 297 try (CloseableHttpClient client = HttpClients.createDefault()) { 298 FacebookProperties fp = ServiceInitializer.getPropertyHandler().getSystemProperties(FacebookProperties.class); 299 StringBuilder uri = new StringBuilder(fp.getoAuth2TokenUri()); 300 uri.append(Definitions.METHOD_FACEBOOK_ACCESS_TOKEN+"?"+Definitions.PARAMETER_OAUTH2_GRANT_TYPE+"="+PARAMETER_VALUE_REFRESH_GRANT_TYPE); 301 302 uri.append("&"+Definitions.PARAMETER_OAUTH2_CLIENT_ID+"="); 303 uri.append(fp.getApplicationId()); 304 305 uri.append("&"+Definitions.PARAMETER_OAUTH2_CLIENT_SECRET+"="); 306 uri.append(fp.getSharedKey()); 307 308 uri.append("&"+Definitions.PARAMETER_FACEBOOK_EXCHANGE_TOKEN+"="); 309 uri.append(token.getAccessToken()); 310 311 OAuth2Token newToken = fromResponse(client.execute(new HttpGet(uri.toString()), new BasicResponseHandler())); 312 if(newToken != null && newToken.isValid()){ 313 return newToken; 314 } 315 } catch (IOException ex) { 316 LOGGER.error(ex, ex); 317 } 318 return null; 319 } 320 321 /** 322 * 323 * @param userId 324 * @return FacebookCredential for the requested userId or null if none available 325 */ 326 public static FacebookCredential getCredential(UserIdentity userId){ 327 if(!UserIdentity.isValid(userId)){ 328 LOGGER.warn("Given userId was not valid."); 329 return null; 330 } 331 332 OAuth2Token token = FacebookUserCore.getToken(userId); 333 if(token == null){ 334 LOGGER.debug("User does not have valid Facebook credentials."); 335 return null; 336 } 337 338 FacebookCredential credential = getCredential(token.getAccessToken()); 339 if(credential == null){ 340 LOGGER.debug("Failed to resolve credentials."); 341 }else{ 342 credential.setUserId(userId); 343 } 344 return credential; 345 } 346 347 /** 348 * Helper method for retrieving credentials from facebook servers 349 * 350 * @param accessToken 351 * @return the credential for the access token or null if not found 352 */ 353 private static FacebookCredential getCredential(String accessToken){ 354 FacebookCredential credential = null; 355 try(CloseableHttpClient client = HttpClients.createDefault()){ 356 FacebookProperties fp = ServiceInitializer.getPropertyHandler().getSystemProperties(FacebookProperties.class); 357 credential = (OAuth2Token.getTokenGSONSerializer()).fromJson(client.execute(new HttpGet(fp.getoAuth2UserInfoUri()+"?"+Definitions.PARAMETER_OAUTH2_ACCESS_TOKEN+"="+accessToken), new BasicResponseHandler()), FacebookCredential.class); 358 } catch (IOException ex) { 359 LOGGER.error(ex, ex); 360 } 361 362 return credential; 363 } 364 365 /** 366 * 367 * @param session 368 * @param accessToken 369 * @return response 370 */ 371 public static Response login(HttpSession session, String accessToken) { 372 FacebookCredential credential = getCredential(accessToken); 373 if(credential == null){ 374 return new Response(Status.BAD_REQUEST, "Failed to process the given token."); 375 } 376 377 UserIdentity userId = UserCore.getUserId(new ExternalAccountConnection(credential.getId(), UserServiceType.FACEBOOK)); 378 if(!UserIdentity.isValid(userId)){ 379 return new Response(Status.FORBIDDEN, "The given Facebook user is not registered with this service, please register before login."); 380 } 381 382 ServiceInitializer.getSessionHandler().registerAndAuthenticate(session.getId(), userId); 383 return new Response(); 384 } 385 386 /** 387 * 388 * @param accessToken 389 * @return response 390 */ 391 public static Response register(String accessToken) { 392 FacebookCredential credential = getCredential(accessToken); 393 if(credential == null){ 394 return new Response(Status.BAD_REQUEST, "Failed to process the given token."); 395 } 396 397 ExternalAccountConnection connection = new ExternalAccountConnection(credential.getId(), UserServiceType.FACEBOOK); 398 UserIdentity userId = UserCore.getUserId(connection); 399 if(userId != null){ 400 return new Response(Status.BAD_REQUEST, "The user is already registered with this service."); 401 } 402 403 Registration registration = new Registration(); 404 String facebookUserId = credential.getId(); 405 registration.setUsername(UserServiceType.FACEBOOK.name()+facebookUserId); // use prefix to prevent collisions with other services 406 registration.setPassword(RandomStringUtils.randomAlphanumeric(50)); // create a random password for this account 407 408 RegistrationStatus status = UserCore.createUser(registration); // register as new user 409 if(status != RegistrationStatus.OK){ 410 return new Response(Status.BAD_REQUEST, "Failed to create new user: "+status.name()); 411 } 412 413 connection.setExternalId(facebookUserId); 414 UserCore.insertExternalAccountConnection(connection, registration.getRegisteredUserId()); // link the created user to the given account 415 return new Response(); 416 } 417 418 /** 419 * Event listener for user related events. 420 * 421 * Automatically instantiated by Spring as a bean. 422 */ 423 @SuppressWarnings("unused") 424 private static class UserEventListener implements ApplicationListener<UserServiceEvent>{ 425 426 @Override 427 public void onApplicationEvent(UserServiceEvent event) { 428 EventType type = event.getType(); 429 if(type == EventType.USER_REMOVED || (type == EventType.USER_AUTHORIZATION_REVOKED && event.getSource().equals(UserCore.class) && UserServiceType.FACEBOOK.equals(event.getUserServiceType()))){ 430 UserIdentity userId = event.getUserId(); 431 LOGGER.debug("Detected event of type "+type.name()+", removing tokens for user, id: "+userId.getUserId()); 432 ServiceInitializer.getDAOHandler().getSQLDAO(FacebookUserDAO.class).removeToken(userId); 433 434 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(FacebookUserCore.class, userId, EventType.USER_AUTHORIZATION_REVOKED)); // send revoked event, this should trigger clean up on all relevant services 435 } 436 } 437 } // class UserEventListener 438}