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.twitter; 017 018import java.io.IOException; 019import java.security.InvalidKeyException; 020import java.security.NoSuchAlgorithmException; 021import java.util.Map.Entry; 022import java.util.SortedMap; 023import java.util.TreeMap; 024 025import javax.crypto.Mac; 026import javax.crypto.spec.SecretKeySpec; 027import javax.servlet.http.HttpSession; 028 029import org.apache.commons.codec.DecoderException; 030import org.apache.commons.codec.EncoderException; 031import org.apache.commons.codec.binary.Base64; 032import org.apache.commons.codec.net.URLCodec; 033import org.apache.commons.lang3.BooleanUtils; 034import org.apache.commons.lang3.RandomStringUtils; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.http.client.methods.HttpGet; 037import org.apache.http.client.methods.HttpPost; 038import org.apache.http.entity.ContentType; 039import org.apache.http.entity.StringEntity; 040import org.apache.http.impl.client.BasicResponseHandler; 041import org.apache.http.impl.client.CloseableHttpClient; 042import org.apache.http.impl.client.HttpClients; 043import org.apache.log4j.Logger; 044import org.springframework.context.ApplicationListener; 045 046import service.tut.pori.users.UserCore; 047import service.tut.pori.users.UserCore.Registration; 048import service.tut.pori.users.UserCore.RegistrationStatus; 049import service.tut.pori.users.UserServiceEvent; 050import twitter4j.auth.AccessToken; 051import core.tut.pori.context.ServiceInitializer; 052import core.tut.pori.http.RedirectResponse; 053import core.tut.pori.http.Response; 054import core.tut.pori.http.Response.Status; 055import core.tut.pori.properties.NonceProperties; 056import core.tut.pori.users.ExternalAccountConnection; 057import core.tut.pori.users.ExternalAccountConnection.UserServiceType; 058import core.tut.pori.users.UserEvent; 059import core.tut.pori.users.UserEvent.EventType; 060import core.tut.pori.users.UserIdentity; 061import core.tut.pori.utils.JSONFormatter; 062 063/** 064 * Twitter User Service core methods. 065 * 066 * 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} : 067 * <ul> 068 * <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_GIVEN} for new user account authorizations.</li> 069 * <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_REVOKED} for removed user account authorizations.</li> 070 * </ul> 071 */ 072public final class TwitterUserCore { 073 private static final String ERROR_MESSAGE_TWITTER = "An error occurred while connecting Twitter service."; 074 private static final Logger LOGGER = Logger.getLogger(TwitterUserCore.class); 075 private static final int NONCE_LENGTH = 32; 076 private static final String PARAMETER_VALUE_FALSE = "false"; 077 private static final String PARAMETER_VALUE_OAUTH = "OAuth "; 078 private static final String PARAMETER_VALUE_SIGNATURE_METHOD = "HMAC-SHA1"; 079 private static final String PARAMETER_VALUE_TRUE = "true"; 080 private static final String PARAMETER_VALUE_VERSION = "1.0"; 081 private static final String QUOTATION_MARK = "\""; 082 private static final String SIGNING_ALGORITHM = "HmacSHA1"; 083 084 /** 085 * 086 */ 087 private TwitterUserCore(){ 088 // nothing needed 089 } 090 091 /** 092 * 093 * @param userIdentity 094 * @return response 095 */ 096 public static Response removeAuthorization(UserIdentity userIdentity) { 097 if(!UserIdentity.isValid(userIdentity)){ 098 LOGGER.warn("Invalid user identity."); 099 return new Response(Status.INTERNAL_SERVER_ERROR); 100 } 101 102 ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class).removeTokens(userIdentity); 103 104 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(TwitterUserCore.class, userIdentity, EventType.USER_AUTHORIZATION_REVOKED)); // send revoked event, this should trigger clean up on all relevant services 105 return new Response(); // default OK 106 } 107 108 /** 109 * 110 * @param session 111 * @param token 112 * @param verifier 113 * @return response 114 */ 115 public static Response processOAuthLoginCallback(HttpSession session, String token, String verifier) { 116 RequestToken requestToken = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class).getRequestToken(token); 117 if(requestToken == null){ 118 LOGGER.warn("The token does not exist: "+token); 119 return new Response(Status.BAD_REQUEST); 120 } 121 requestToken.setVerifier(verifier); 122 AccessToken accessToken = getAccessToken(requestToken); 123 if(accessToken == null){ 124 LOGGER.warn("Failed to get access token for request token: "+token); 125 return new Response(Status.BAD_REQUEST); 126 } 127 128 TwitterCredential tc = getCredential(accessToken); 129 if(tc == null){ 130 LOGGER.warn("Failed to resolve twitter credentials with access token: "+accessToken.getToken()); 131 return new Response(Status.INTERNAL_SERVER_ERROR); 132 } 133 134 UserIdentity userId = UserCore.getUserId(new ExternalAccountConnection(tc.getId(), UserServiceType.TWITTER)); 135 if(!UserIdentity.isValid(userId)){ 136 return new Response(Status.FORBIDDEN, "The given Facebook user is not registered with this service, please register before login."); 137 } 138 139 ServiceInitializer.getSessionHandler().registerAndAuthenticate(session.getId(), userId); 140 return new Response(); 141 } 142 143 /** 144 * 145 * @param token 146 * @param verifier 147 * @return response 148 */ 149 public static Response processOAuthAuthorizeCallback(String token, String verifier) { 150 TwitterUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class); 151 RequestToken requestToken = dao.getRequestToken(token); 152 if(requestToken == null){ 153 LOGGER.warn("The token does not exist: "+token); 154 return new Response(Status.BAD_REQUEST); 155 } 156 requestToken.setVerifier(verifier); 157 AccessToken accessToken = getAccessToken(requestToken); 158 if(accessToken == null){ 159 LOGGER.warn("Failed to get access token for request token: "+token); 160 return new Response(Status.BAD_REQUEST); 161 } 162 163 TwitterCredential tc = getCredential(accessToken); 164 if(tc == null){ 165 LOGGER.warn("Failed to resolve twitter credentials with access token: "+accessToken.getToken()); 166 return new Response(Status.INTERNAL_SERVER_ERROR); 167 } 168 169 UserIdentity userId = requestToken.getUserId(); 170 if(!dao.setAccessToken(tc.getId(), accessToken, userId)){ 171 LOGGER.warn("Failed to set new token."); 172 return new Response(Status.BAD_REQUEST); 173 } 174 175 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(TwitterUserCore.class, userId, EventType.USER_AUTHORIZATION_GIVEN)); 176 177 String redirectUri = requestToken.getRedirectUri(); 178 if(StringUtils.isBlank(redirectUri)){ 179 return new Response(); 180 }else{ 181 return new RedirectResponse(redirectUri); 182 } 183 } 184 185 /** 186 * 187 * @param token 188 * @param verifier 189 * @return response 190 */ 191 public static Response processOAuthRegisterCallback(String token, String verifier) { 192 RequestToken requestToken = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class).getRequestToken(token); 193 if(requestToken == null){ 194 LOGGER.warn("The token does not exist: "+token); 195 return new Response(Status.BAD_REQUEST); 196 } 197 requestToken.setVerifier(verifier); 198 AccessToken accessToken = getAccessToken(requestToken); 199 if(accessToken == null){ 200 LOGGER.warn("Failed to get access token for request token: "+token); 201 return new Response(Status.BAD_REQUEST); 202 } 203 204 TwitterCredential tc = getCredential(accessToken); 205 if(tc == null){ 206 LOGGER.warn("Failed to resolve twitter credentials with access token: "+accessToken.getToken()); 207 return new Response(Status.INTERNAL_SERVER_ERROR); 208 } 209 210 String twitterUserId = tc.getId(); 211 ExternalAccountConnection connection = new ExternalAccountConnection(twitterUserId, UserServiceType.TWITTER); 212 UserIdentity userId = UserCore.getUserId(connection); 213 if(userId != null){ 214 return new Response(Status.BAD_REQUEST, "The user is already registered with this service."); 215 } 216 217 Registration registration = new Registration(); 218 registration.setUsername(UserServiceType.TWITTER.name()+twitterUserId); // use prefix to prevent collisions with other services 219 registration.setPassword(RandomStringUtils.randomAlphanumeric(50)); // create a random password for this account 220 221 RegistrationStatus status = UserCore.createUser(registration); // register as new user 222 if(status != RegistrationStatus.OK){ 223 return new Response(Status.BAD_REQUEST, "Failed to create new user: "+status.name()); 224 } 225 226 connection.setExternalId(twitterUserId); 227 UserCore.insertExternalAccountConnection(connection, registration.getRegisteredUserId()); // link the created user to the given account 228 return new Response(); 229 } 230 231 /** 232 * 233 * @param userId 234 * @return TwitterCredential for the requested userId or null if none available 235 */ 236 public static TwitterCredential getCredential(UserIdentity userId){ 237 if(!UserIdentity.isValid(userId)){ 238 LOGGER.warn("Given userId was not valid."); 239 return null; 240 } 241 242 AccessToken token = getToken(userId); 243 if(token == null){ 244 LOGGER.debug("User does not have valid Twitter credentials."); 245 return null; 246 } 247 248 TwitterCredential credential = getCredential(token); 249 if(credential == null){ 250 LOGGER.debug("Failed to resolve credentials."); 251 }else{ 252 credential.setUserId(userId); 253 } 254 return credential; 255 } 256 257 /** 258 * 259 * @param authorizedUser 260 * @return the token or null if not found 261 */ 262 public static AccessToken getToken(UserIdentity authorizedUser) { 263 if(!UserIdentity.isValid(authorizedUser)){ 264 LOGGER.warn("Invalid user identity."); 265 return null; 266 } 267 return ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class).getAccessToken(authorizedUser); // there is no way to refresh twitter's token, nor do we really know if it has been invalidated, so simply return the token 268 } 269 270 /** 271 * Helper method for retrieving credentials from twitter servers 272 * 273 * https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials 274 * 275 * @param accessToken 276 * @return credentials for the given token or null if none was found 277 */ 278 private static TwitterCredential getCredential(AccessToken accessToken){ 279 TwitterCredential credential = null; 280 try(CloseableHttpClient client = HttpClients.createDefault()){ 281 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 282 String requestUri = tp.getUserInfoUri(); 283 284 URLCodec codec = new URLCodec(core.tut.pori.http.Definitions.ENCODING_UTF8); 285 SortedMap<String, String> encodedParameters = new TreeMap<>(); 286 encodedParameters.put(Definitions.PARAMETER_OAUTH_CONSUMER_KEY, tp.getEncodedApiKey()); 287 encodedParameters.put(Definitions.PARAMETER_OAUTH_NONCE, RandomStringUtils.randomAlphanumeric(NONCE_LENGTH)); 288 encodedParameters.put(Definitions.PARAMETER_OAUTH_SIGNATURE_METHOD, PARAMETER_VALUE_SIGNATURE_METHOD); 289 encodedParameters.put(Definitions.PARAMETER_OAUTH_TIMESTAMP, String.valueOf(System.currentTimeMillis()/1000)); 290 encodedParameters.put(Definitions.PARAMETER_OAUTH_VERSION, PARAMETER_VALUE_VERSION); 291 encodedParameters.put(Definitions.PARAMETER_OAUTH_TOKEN, codec.encode(accessToken.getToken())); 292 293 encodedParameters.put(Definitions.PARAMETER_TWITTER_INCLUDE_ENTITIES, PARAMETER_VALUE_FALSE); // add temporarily for signature generation 294 encodedParameters.put(Definitions.PARAMETER_TWITTER_SKIP_STATUS, PARAMETER_VALUE_TRUE); // add temporarily for signature generation 295 296 encodedParameters.put(Definitions.PARAMETER_OAUTH_SIGNATURE, createSignature(tp.getEncodedClientSecret(), encodedParameters, accessToken.getTokenSecret(), core.tut.pori.http.Definitions.METHOD_GET, requestUri)); 297 298 encodedParameters.remove(Definitions.PARAMETER_TWITTER_INCLUDE_ENTITIES); // remove from header, these are part of the query uri 299 encodedParameters.remove(Definitions.PARAMETER_TWITTER_SKIP_STATUS); // remove from header, these are part of the query uri 300 301 HttpGet get = new HttpGet(requestUri+core.tut.pori.http.Definitions.SEPARATOR_URI_METHOD_PARAMS+Definitions.PARAMETER_TWITTER_INCLUDE_ENTITIES+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR+PARAMETER_VALUE_FALSE+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS+Definitions.PARAMETER_TWITTER_SKIP_STATUS+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR+PARAMETER_VALUE_TRUE); 302 get.setHeader(Definitions.HEADER_OAUTH_AUTHORIZATION, createOAuthHeader(encodedParameters)); 303 304 credential = JSONFormatter.createGsonSerializer().fromJson(client.execute(get, new BasicResponseHandler()), TwitterCredential.class); 305 } catch (IOException | EncoderException | InvalidKeyException | NoSuchAlgorithmException ex) { 306 LOGGER.error(ex, ex); 307 } 308 309 return credential; 310 } 311 312 /** 313 * Note: this will NOT save the access token to the database, but this WILL remove the given request token from the database whether the request was successful or not 314 * This will automatically discard expired tokens. 315 * 316 * @param token 317 * @return the access token or null if failed to retrieve one 318 */ 319 private static final AccessToken getAccessToken(RequestToken token){ 320 TwitterUserDAO dao = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class); 321 if(token.getUpdated().getTime()+ServiceInitializer.getPropertyHandler().getSystemProperties(NonceProperties.class).getNonceExpiresIn() < (System.currentTimeMillis())){ 322 LOGGER.warn("The request token has expired: "+token); 323 dao.removeRequestToken(token); // remove the expired token 324 return null; 325 } 326 327 AccessToken at = null; 328 try (CloseableHttpClient client = HttpClients.createDefault()){ 329 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 330 331 URLCodec codec = new URLCodec(core.tut.pori.http.Definitions.ENCODING_UTF8); 332 String requestUri = tp.getoAuthUri()+Definitions.METHOD_TWITTER_ACCESS_TOKEN; 333 String encodedVerifier = codec.encode(token.getVerifier()); 334 335 SortedMap<String, String> encodedParameters = new TreeMap<>(); 336 encodedParameters.put(Definitions.PARAMETER_OAUTH_CONSUMER_KEY, tp.getEncodedApiKey()); 337 encodedParameters.put(Definitions.PARAMETER_OAUTH_NONCE, RandomStringUtils.randomAlphanumeric(NONCE_LENGTH)); 338 encodedParameters.put(Definitions.PARAMETER_OAUTH_SIGNATURE_METHOD, PARAMETER_VALUE_SIGNATURE_METHOD); 339 encodedParameters.put(Definitions.PARAMETER_OAUTH_TIMESTAMP, String.valueOf(System.currentTimeMillis()/1000)); 340 encodedParameters.put(Definitions.PARAMETER_OAUTH_VERSION, PARAMETER_VALUE_VERSION); 341 encodedParameters.put(Definitions.PARAMETER_OAUTH_VERIFIER, encodedVerifier); // for signature 342 encodedParameters.put(Definitions.PARAMETER_OAUTH_TOKEN, codec.encode(token.getToken())); 343 encodedParameters.put(Definitions.PARAMETER_OAUTH_SIGNATURE, createSignature(tp.getEncodedClientSecret(), encodedParameters, token.getSecret(), core.tut.pori.http.Definitions.METHOD_POST, requestUri)); 344 encodedParameters.remove(Definitions.PARAMETER_OAUTH_VERIFIER); // do not add it to the header 345 346 HttpPost post = new HttpPost(requestUri); 347 post.setHeader(Definitions.HEADER_OAUTH_AUTHORIZATION, createOAuthHeader(encodedParameters)); 348 post.setEntity(new StringEntity(Definitions.PARAMETER_OAUTH_VERIFIER+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR+encodedVerifier, ContentType.APPLICATION_FORM_URLENCODED)); 349 350 String[] responseParams = StringUtils.split(client.execute(post, new BasicResponseHandler()), core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS); 351 if(responseParams == null || responseParams.length < 2){ 352 LOGGER.warn("Failed to retrieve token."); 353 }else{ 354 String accessToken = null; 355 String accessTokenSecret = null; 356 for(int i=0;i<responseParams.length;++i){ 357 String[] parts = StringUtils.split(responseParams[i], core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR); 358 if(parts.length != 2){ 359 LOGGER.warn("Invalid response parameter: "+responseParams[i]); 360 break; 361 }else if(Definitions.PARAMETER_OAUTH_TOKEN.equals(parts[0])){ 362 accessToken = codec.decode(parts[1]); 363 }else if(Definitions.PARAMETER_OAUTH_TOKEN_SECRET.equals(parts[0])){ 364 accessTokenSecret = codec.decode(parts[1]); 365 } // else ignore everything else 366 } 367 368 if(StringUtils.isBlank(accessToken) || StringUtils.isBlank(accessTokenSecret)){ 369 LOGGER.warn("Invalid "+Definitions.PARAMETER_OAUTH_TOKEN+" or "+Definitions.PARAMETER_OAUTH_TOKEN_SECRET); 370 }else{ 371 at = new AccessToken(accessToken, accessTokenSecret); 372 } // else 373 } // else 374 375 } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | EncoderException | DecoderException ex) { 376 LOGGER.error(ex, ex); 377 } 378 379 dao.removeRequestToken(token); // remove the request token to prevent further authentication attempts using this token 380 381 return at; 382 } 383 384 /** 385 * 386 * @return redirection response 387 */ 388 public static Response createLoginRedirection() { 389 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 390 return createRedirection(tp.getEncodedOAuthLoginRedirectUri(), tp.getoAuthUri()+Definitions.METHOD_TWITTER_AUTHENTICATE, null, null); 391 } 392 393 /** 394 * 395 * @return redirection response 396 */ 397 public static Response createRegisterRedirection() { 398 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 399 return createRedirection(tp.getEncodedOAuthRegisterRedirectUri(), tp.getoAuthUri()+Definitions.METHOD_TWITTER_AUTHENTICATE, null, null); 400 } 401 402 /** 403 * 404 * @param authenticatedUser 405 * @param redirectUri 406 * @return redirection response 407 */ 408 public static Response createAuthorizationRedirection(UserIdentity authenticatedUser, String redirectUri) { 409 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 410 return createRedirection(tp.getEncodedOAuthAuthorizeRedirectUri(), tp.getoAuthUri()+Definitions.METHOD_TWITTER_AUTHORIZE, redirectUri, authenticatedUser); 411 } 412 413 /** 414 * 415 * @param encodedClientSecret 416 * @param encodedParameters non-empty, non-null list of parameters 417 * @param encodedTokenSecret 418 * @param httpMethod 419 * @param requestUri 420 * @return signature string created from the given values 421 * @throws EncoderException 422 * @throws NoSuchAlgorithmException 423 * @throws InvalidKeyException 424 */ 425 private static final String createSignature(String encodedClientSecret, SortedMap<String, String> encodedParameters, String encodedTokenSecret, String httpMethod, String requestUri) throws EncoderException, NoSuchAlgorithmException, InvalidKeyException{ 426 StringBuilder baseString = new StringBuilder(); 427 for(Entry<String, String> e : encodedParameters.entrySet()){ 428 baseString.append(e.getKey()); 429 baseString.append(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR); 430 baseString.append(e.getValue()); 431 baseString.append(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS); 432 } 433 434 URLCodec codec = new URLCodec(core.tut.pori.http.Definitions.ENCODING_UTF8); 435 String signature = httpMethod+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS+codec.encode(requestUri)+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS+ 436 codec.encode(baseString.substring(0, baseString.length()-1)); // chop the last character 437 438 Mac mac = Mac.getInstance(SIGNING_ALGORITHM); 439 mac.init(new SecretKeySpec((encodedClientSecret+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS+(StringUtils.isBlank(encodedTokenSecret) ? "" : encodedTokenSecret)).getBytes(), SIGNING_ALGORITHM)); 440 return codec.encode(new String(Base64.encodeBase64(mac.doFinal(signature.getBytes()))).trim()); 441 } 442 443 /** 444 * 445 * @param encodedParameters non-null, non-empty list of parameters 446 * @return authentication header created from the given values 447 */ 448 private static final String createOAuthHeader(SortedMap<String, String> encodedParameters){ 449 StringBuilder header = new StringBuilder(PARAMETER_VALUE_OAUTH); 450 for(Entry<String, String> e : encodedParameters.entrySet()){ 451 header.append(e.getKey()); 452 header.append(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR); 453 header.append(QUOTATION_MARK); 454 header.append(e.getValue()); 455 header.append(QUOTATION_MARK); 456 header.append(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES); 457 } 458 459 return header.substring(0, header.length()-1); // chop the last character 460 } 461 462 /** 463 * Generates a redirection request. Stores the given user identity and target URL into database (if given AND the operation finished successfully). 464 * This is a helper method for the create*Redirection methods. 465 * 466 * As defined in: 467 * https://dev.twitter.com/docs/auth/implementing-sign-twitter 468 * and 469 * https://dev.twitter.com/docs/auth/authorizing-request 470 * and 471 * https://dev.twitter.com/docs/auth/3-legged-authorization 472 * and 473 * https://dev.twitter.com/docs/auth/creating-signature 474 * 475 * @param callbackUrl the local method where the request should return, this MUST be URL encoded 476 * @param serviceUrl where the initial request will be redirected to, ie. the Twitter endpoint (e.g. https://api.twitter.com/oauth/authenticate), without trailing / or uri parameters 477 * @param targetUrl the user provided final target, only stored to database. Can be null. 478 * @param userId the requester, only stored to database 479 * @return redirection response 480 */ 481 private static Response createRedirection(String callbackUrl, String serviceUrl, String targetUrl, UserIdentity userId){ 482 try(CloseableHttpClient client = HttpClients.createDefault()){ 483 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 484 485 String requestUri = tp.getoAuthUri()+Definitions.METHOD_TWITTER_REQUEST_TOKEN; 486 487 SortedMap<String, String> encodedParameters = new TreeMap<>(); 488 encodedParameters.put(Definitions.PARAMETER_OAUTH_CALLBACK, callbackUrl); 489 encodedParameters.put(Definitions.PARAMETER_OAUTH_CONSUMER_KEY, tp.getEncodedApiKey()); 490 encodedParameters.put(Definitions.PARAMETER_OAUTH_NONCE, RandomStringUtils.randomAlphanumeric(NONCE_LENGTH)); 491 encodedParameters.put(Definitions.PARAMETER_OAUTH_SIGNATURE_METHOD, PARAMETER_VALUE_SIGNATURE_METHOD); 492 encodedParameters.put(Definitions.PARAMETER_OAUTH_TIMESTAMP, String.valueOf(System.currentTimeMillis()/1000)); 493 encodedParameters.put(Definitions.PARAMETER_OAUTH_VERSION, PARAMETER_VALUE_VERSION); 494 encodedParameters.put(Definitions.PARAMETER_OAUTH_SIGNATURE, createSignature(tp.getEncodedClientSecret(), encodedParameters, null, core.tut.pori.http.Definitions.METHOD_POST, requestUri)); 495 496 HttpPost post = new HttpPost(requestUri); 497 post.setHeader(Definitions.HEADER_OAUTH_AUTHORIZATION, createOAuthHeader(encodedParameters)); 498 499 String[] responseParams = StringUtils.split(client.execute(post, new BasicResponseHandler()), core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAMS); 500 if(responseParams == null || responseParams.length < 3){ 501 LOGGER.warn("Did not receive the required confirmation parameters."); 502 return new Response(Status.INTERNAL_SERVER_ERROR, ERROR_MESSAGE_TWITTER); 503 } 504 505 URLCodec codec = new URLCodec(core.tut.pori.http.Definitions.ENCODING_UTF8); 506 String requestToken = null; 507 String encodedRequestToken = null; 508 String tokenSecret = null; 509 boolean callbackConfirmed = false; 510 for(int i=0;i<responseParams.length;++i){ 511 String[] parts = StringUtils.split(responseParams[i], core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR); 512 if(parts.length != 2){ 513 LOGGER.warn("Bad response parameter: "+responseParams[i]); 514 return new Response(Status.INTERNAL_SERVER_ERROR, ERROR_MESSAGE_TWITTER); 515 }else if(Definitions.PARAMETER_OAUTH_TOKEN.equals(parts[0])){ 516 requestToken = codec.decode(parts[1]); 517 encodedRequestToken = parts[1]; // hard to say whether the tokens need to be decoded/encoded or not, but let's do so just in case 518 }else if(Definitions.PARAMETER_OAUTH_TOKEN_SECRET.equals(parts[0])){ 519 tokenSecret = codec.decode(parts[1]); 520 }else if(Definitions.PARAMETER_OAUTH_CALLBACK_CONFIRMED.equals(parts[0])){ 521 callbackConfirmed = BooleanUtils.toBoolean(parts[1]); 522 } // else ignore everything else 523 } 524 525 if(!callbackConfirmed){ 526 LOGGER.warn(Definitions.PARAMETER_OAUTH_CALLBACK+" was not confirmed: "+callbackUrl); 527 return new Response(Status.INTERNAL_SERVER_ERROR); 528 } 529 if(StringUtils.isBlank(requestToken) || StringUtils.isBlank(tokenSecret)){ 530 LOGGER.warn("Invalid "+Definitions.PARAMETER_OAUTH_TOKEN+" : "+requestToken+" or "+Definitions.PARAMETER_OAUTH_TOKEN_SECRET+" : "+tokenSecret); 531 return new Response(Status.INTERNAL_SERVER_ERROR); 532 } 533 534 ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class).setRequestToken(new RequestToken(targetUrl, requestToken, tokenSecret, userId)); 535 536 return new RedirectResponse(serviceUrl+core.tut.pori.http.Definitions.SEPARATOR_URI_METHOD_PARAMS+Definitions.PARAMETER_OAUTH_TOKEN+core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR+encodedRequestToken); 537 } catch (IOException ex) { 538 LOGGER.error(ex, ex); 539 return new Response(Status.INTERNAL_SERVER_ERROR, ERROR_MESSAGE_TWITTER); 540 } catch (DecoderException | EncoderException | NoSuchAlgorithmException | InvalidKeyException ex){ 541 LOGGER.error(ex, ex); 542 return new Response(Status.INTERNAL_SERVER_ERROR); 543 } 544 } 545 546 /** 547 * Event listener for user related events. 548 * 549 * Automatically instantiated by Spring as a bean. 550 */ 551 @SuppressWarnings("unused") 552 private static class UserEventListener implements ApplicationListener<UserServiceEvent>{ 553 554 @Override 555 public void onApplicationEvent(UserServiceEvent event) { 556 EventType type = event.getType(); 557 if(type == EventType.USER_REMOVED || (type == EventType.USER_AUTHORIZATION_REVOKED && event.getSource().equals(UserCore.class) && UserServiceType.TWITTER.equals(event.getUserServiceType()))){ 558 UserIdentity userId = event.getUserId(); 559 LOGGER.debug("Detected event of type "+type.name()+", removing tokens for user, id: "+userId.getUserId()); 560 ServiceInitializer.getDAOHandler().getSQLDAO(TwitterUserDAO.class).removeTokens(userId); 561 562 ServiceInitializer.getEventHandler().publishEvent(new UserEvent(TwitterUserCore.class, userId, EventType.USER_AUTHORIZATION_REVOKED)); // send revoked event, this should trigger clean up on all relevant services 563 } 564 } 565 } // class UserEventListener 566}