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}