/*
* Copyright 2014-2016 CyberVision, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kaaproject.kaa.server.verifiers.facebook.verifier;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.kaaproject.kaa.server.common.verifier.AbstractKaaUserVerifier;
import org.kaaproject.kaa.server.common.verifier.UserVerifierCallback;
import org.kaaproject.kaa.server.common.verifier.UserVerifierContext;
import org.kaaproject.kaa.server.verifiers.facebook.config.gen.FacebookAvroConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class FacebookUserVerifier extends AbstractKaaUserVerifier<FacebookAvroConfig> {
private static final Logger LOG = LoggerFactory.getLogger(FacebookUserVerifier.class);
private static final String FACEBOOK_URI_SCHEME = "https";
private static final String FACEBOOK_URI_AUTHORITY = "graph.facebook.com";
private static final String FACEBOOK_URI_PATH = "/debug_token";
private static final long MAX_SEC_FACEBOOK_REQUEST_TIME = 60;
private static final int HTTP_BAD_REQUEST = 400;
private static final int HTTP_OK = 200;
private static final String OAUTH_ERROR = "190";
private static final String TOKEN_EXPIRED = "463";
private static final String TOKEN_INVALID = "467";
private static final String DATA = "data";
private static final String USER_ID = "user_id";
private static final String ERROR = "error";
private static final String MESSAGE = "message";
private static final String CODE = "code";
private static final String ERRCODE = "errcode";
private static final String ERRROR_SUBCODE = "error_subcode";
private FacebookAvroConfig configuration;
private ExecutorService tokenVerifiersPool;
private CloseableHttpClient httpClient;
@Override
public void init(UserVerifierContext context, FacebookAvroConfig configuration) {
LOG.info("Initializing facebook user verifier with context {} and configuration {}", context, configuration);
this.configuration = configuration;
}
@Override
public void checkAccessToken(String userExternalId, String userAccessToken, UserVerifierCallback callback) {
tokenVerifiersPool.submit(new TokenVerifier(userExternalId, userAccessToken, callback, configuration));
}
@SuppressWarnings("unchecked")
private void handleResponse(CloseableHttpResponse connection, String userExternalId, UserVerifierCallback callback,
String userAccessToken) throws IOException {
Map<String, Object> responseMap = getResponseMap(connection.getEntity().getContent());
Map<String, Object> dataMap = (Map<String, Object>) responseMap.get(DATA);
String receivedUserId = String.valueOf(dataMap.get(USER_ID));
if (dataMap.containsKey(ERROR) || receivedUserId == null) {
Map<String, Object> errorMap = (Map<String, Object>) dataMap.get(ERROR);
LOG.warn("Bad input token: {}, errcode = {}", errorMap.get(MESSAGE), errorMap.get(CODE));
callback.onTokenInvalid();
} else if (!receivedUserId.equals(userExternalId)) {
LOG.warn("Input token doesn't belong to the user with {} id", userExternalId);
callback.onVerificationFailure("User access token " + userAccessToken + " doesn't belong to the user");
} else {
LOG.trace("Input token is confirmed and belongs to the user with {} id", userExternalId);
callback.onSuccess();
}
}
@SuppressWarnings("unchecked")
private void handleBadResponse(CloseableHttpResponse connection, UserVerifierCallback callback) throws IOException {
Map<String, Object> responseMap = getResponseMap(connection.getEntity().getContent());
Map<String, Object> errorMap = null;
// no error field in response
if (responseMap.get(ERROR) != null) {
errorMap = (Map<String, Object>) responseMap.get(ERROR);
}
// errors with OAuth
if (errorMap != null && String.valueOf(errorMap.get(CODE)).equals(OAUTH_ERROR)) {
if (errorMap.get(ERRROR_SUBCODE) == null) {
LOG.trace("OAuthException: [{}], errcode: [{}], errsubcode: [{}] ", errorMap.get(MESSAGE),
errorMap.get(ERRCODE), errorMap.get(ERRROR_SUBCODE));
callback.onVerificationFailure("OAuthException:" + errorMap.get(MESSAGE));
} else if (String.valueOf(errorMap.get(ERRROR_SUBCODE)).equals(TOKEN_EXPIRED)) { // access token has expired
LOG.trace("Access Token has expired");
callback.onTokenExpired();
} else if (String.valueOf(errorMap.get(ERRROR_SUBCODE)).equals(TOKEN_INVALID)) { // access token is invalid
LOG.trace("Access Token is invalid");
callback.onTokenInvalid();
} else {
LOG.trace("OAuthException: [{}], errcode: [{}], errsubcode: [{}] ", errorMap.get(MESSAGE),
errorMap.get(ERRCODE), errorMap.get(ERRROR_SUBCODE));
callback.onVerificationFailure("OAuthException:" + errorMap.get(MESSAGE));
}
} else {
if (errorMap != null) {
LOG.trace("Unable to verify token: {}, errcode: [{}]", errorMap.get(MESSAGE),
errorMap.get(ERRCODE));
callback.onVerificationFailure("Unable to verify token: " + errorMap.get(MESSAGE)
+ ", errorcode: " + errorMap.get(ERRCODE));
} else {
LOG.trace("Unable to verify token. HTTP response 400");
callback.onVerificationFailure("Unable to verify token. HTTP response 400");
}
}
}
private Map<String, Object> getResponseMap(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
ObjectMapper responseMapper = new ObjectMapper();
return responseMapper.readValue(reader.readLine(), Map.class);
}
}
protected CloseableHttpResponse establishConnection(String userAccessToken, String accessToken) throws IOException {
URI uri = null;
try {
String facebookUriQuery = "input_token=" + userAccessToken + "&access_token=" + accessToken;
uri = new URI(FACEBOOK_URI_SCHEME, FACEBOOK_URI_AUTHORITY, FACEBOOK_URI_PATH, facebookUriQuery, null);
} catch (URISyntaxException ex) {
LOG.debug("Malformed URI", ex);
}
return httpClient.execute(new HttpGet(uri));
}
@Override
public void start() {
LOG.info("facebook user verifier started");
tokenVerifiersPool = new ThreadPoolExecutor(0, configuration.getMaxParallelConnections(),
MAX_SEC_FACEBOOK_REQUEST_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
// Increase max total connection
connectionManager.setMaxTotal(configuration.getMaxParallelConnections());
}
@Override
public void stop() {
LOG.info("stopping facebook verifier");
tokenVerifiersPool.shutdown();
try {
httpClient.close();
} catch (IOException ex) {
LOG.debug("Unable to close HttpClient ", ex);
}
LOG.info("facebook user verifier stopped");
}
@Override
public Class<FacebookAvroConfig> getConfigurationClass() {
return FacebookAvroConfig.class;
}
private class TokenVerifier implements Runnable {
private final String userExternalId;
private final String userAccessToken;
private final UserVerifierCallback callback;
private final FacebookAvroConfig config;
public TokenVerifier(String userExternalId, String userAccessToken,
UserVerifierCallback callback, FacebookAvroConfig config) {
this.userExternalId = userExternalId;
this.userAccessToken = userAccessToken;
this.callback = callback;
this.config = config;
}
@Override
public void run() {
String accessToken = config.getAppId() + "|" + config.getAppSecret();
LOG.trace("Started token verification with accessToken [{}]", accessToken);
CloseableHttpResponse closeableHttpResponse = null;
try {
closeableHttpResponse = establishConnection(userAccessToken, accessToken);
LOG.trace("Connection established [{}]", accessToken);
int responseCode = closeableHttpResponse.getStatusLine().getStatusCode();
if (responseCode == HTTP_BAD_REQUEST) {
handleBadResponse(closeableHttpResponse, callback);
} else if (responseCode == HTTP_OK) {
handleResponse(closeableHttpResponse, userExternalId, callback, userAccessToken);
} else { // other response codes
LOG.warn("Server response code: {}, no data can be retrieved", responseCode);
callback.onVerificationFailure("Server response code:" + responseCode
+ ", no data can be retrieved");
}
} catch (IOException ex) {
LOG.debug("Connection error", ex);
callback.onConnectionError(ex.getMessage());
} catch (Exception ex) {
LOG.debug("Unexpected error", ex);
callback.onInternalError(ex.getMessage());
} finally {
if (closeableHttpResponse != null) {
try {
closeableHttpResponse.close();
} catch (IOException ex) {
LOG.debug("Connection error: can't close CloseableHttpResponse ", ex);
}
}
}
}
}
}