/**
* Copyright 2010-present Facebook.
*
* 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 com.facebook;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.webkit.CookieSyncManager;
import com.facebook.android.R;
import com.facebook.internal.AnalyticsEvents;
import com.facebook.internal.NativeProtocol;
import com.facebook.internal.ServerProtocol;
import com.facebook.internal.Utility;
import com.facebook.model.GraphUser;
import com.facebook.widget.WebDialog;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class AuthorizationClient implements Serializable {
private static final long serialVersionUID = 1L;
private static final String TAG = "Facebook-AuthorizationClient";
private static final String WEB_VIEW_AUTH_HANDLER_STORE =
"com.facebook.AuthorizationClient.WebViewAuthHandler.TOKEN_STORE_KEY";
private static final String WEB_VIEW_AUTH_HANDLER_TOKEN_KEY = "TOKEN";
// Constants for logging login-related data. Some of these are only used by Session, but grouped here for
// maintainability.
private static final String EVENT_NAME_LOGIN_METHOD_START = "fb_mobile_login_method_start";
private static final String EVENT_NAME_LOGIN_METHOD_COMPLETE = "fb_mobile_login_method_complete";
private static final String EVENT_PARAM_METHOD_RESULT_SKIPPED = "skipped";
static final String EVENT_NAME_LOGIN_START = "fb_mobile_login_start";
static final String EVENT_NAME_LOGIN_COMPLETE = "fb_mobile_login_complete";
// Note: to ensure stability of column mappings across the four different event types, we prepend a column
// index to each name, and we log all columns with all events, even if they are empty.
static final String EVENT_PARAM_AUTH_LOGGER_ID = "0_auth_logger_id";
static final String EVENT_PARAM_TIMESTAMP = "1_timestamp_ms";
static final String EVENT_PARAM_LOGIN_RESULT = "2_result";
static final String EVENT_PARAM_METHOD = "3_method";
static final String EVENT_PARAM_ERROR_CODE = "4_error_code";
static final String EVENT_PARAM_ERROR_MESSAGE = "5_error_message";
static final String EVENT_PARAM_EXTRAS = "6_extras";
static final String EVENT_EXTRAS_TRY_LOGIN_ACTIVITY = "try_login_activity";
static final String EVENT_EXTRAS_TRY_LEGACY = "try_legacy";
static final String EVENT_EXTRAS_LOGIN_BEHAVIOR = "login_behavior";
static final String EVENT_EXTRAS_REQUEST_CODE = "request_code";
static final String EVENT_EXTRAS_IS_LEGACY = "is_legacy";
static final String EVENT_EXTRAS_PERMISSIONS = "permissions";
static final String EVENT_EXTRAS_DEFAULT_AUDIENCE = "default_audience";
static final String EVENT_EXTRAS_MISSING_INTERNET_PERMISSION = "no_internet_permission";
static final String EVENT_EXTRAS_NOT_TRIED = "not_tried";
static final String EVENT_EXTRAS_NEW_PERMISSIONS = "new_permissions";
List<AuthHandler> handlersToTry;
AuthHandler currentHandler;
transient Context context;
transient StartActivityDelegate startActivityDelegate;
transient OnCompletedListener onCompletedListener;
transient BackgroundProcessingListener backgroundProcessingListener;
transient boolean checkedInternetPermission;
AuthorizationRequest pendingRequest;
Map<String, String> loggingExtras;
private transient AppEventsLogger appEventsLogger;
interface OnCompletedListener {
void onCompleted(Result result);
}
interface BackgroundProcessingListener {
void onBackgroundProcessingStarted();
void onBackgroundProcessingStopped();
}
interface StartActivityDelegate {
public void startActivityForResult(Intent intent, int requestCode);
public Activity getActivityContext();
}
void setContext(final Context context) {
this.context = context;
// We rely on individual requests to tell us how to start an activity.
startActivityDelegate = null;
}
void setContext(final Activity activity) {
this.context = activity;
// If we are used in the context of an activity, we will always use that activity to
// call startActivityForResult.
startActivityDelegate = new StartActivityDelegate() {
@Override
public void startActivityForResult(Intent intent, int requestCode) {
activity.startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return activity;
}
};
}
void startOrContinueAuth(AuthorizationRequest request) {
if (getInProgress()) {
continueAuth();
} else {
authorize(request);
}
}
void authorize(AuthorizationRequest request) {
if (request == null) {
return;
}
if (pendingRequest != null) {
throw new FacebookException("Attempted to authorize while a request is pending.");
}
if (request.needsNewTokenValidation() && !checkInternetPermission()) {
// We're going to need INTERNET permission later and don't have it, so fail early.
return;
}
pendingRequest = request;
handlersToTry = getHandlerTypes(request);
tryNextHandler();
}
void continueAuth() {
if (pendingRequest == null || currentHandler == null) {
throw new FacebookException("Attempted to continue authorization without a pending request.");
}
if (currentHandler.needsRestart()) {
currentHandler.cancel();
tryCurrentHandler();
}
}
boolean getInProgress() {
return pendingRequest != null && currentHandler != null;
}
void cancelCurrentHandler() {
if (currentHandler != null) {
currentHandler.cancel();
}
}
boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (pendingRequest != null && requestCode == pendingRequest.getRequestCode()) {
return currentHandler.onActivityResult(requestCode, resultCode, data);
}
return false;
}
private List<AuthHandler> getHandlerTypes(AuthorizationRequest request) {
ArrayList<AuthHandler> handlers = new ArrayList<AuthHandler>();
final SessionLoginBehavior behavior = request.getLoginBehavior();
if (behavior.allowsKatanaAuth()) {
if (!request.isLegacy()) {
handlers.add(new GetTokenAuthHandler());
}
handlers.add(new KatanaProxyAuthHandler());
}
if (behavior.allowsWebViewAuth()) {
handlers.add(new WebViewAuthHandler());
}
return handlers;
}
boolean checkInternetPermission() {
if (checkedInternetPermission) {
return true;
}
int permissionCheck = checkPermission(Manifest.permission.INTERNET);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
String errorType = context.getString(R.string.com_facebook_internet_permission_error_title);
String errorDescription = context.getString(R.string.com_facebook_internet_permission_error_message);
complete(Result.createErrorResult(pendingRequest, errorType, errorDescription));
return false;
}
checkedInternetPermission = true;
return true;
}
void tryNextHandler() {
if (currentHandler != null) {
logAuthorizationMethodComplete(currentHandler.getNameForLogging(), EVENT_PARAM_METHOD_RESULT_SKIPPED,
null, null, currentHandler.methodLoggingExtras);
}
while (handlersToTry != null && !handlersToTry.isEmpty()) {
currentHandler = handlersToTry.remove(0);
boolean started = tryCurrentHandler();
if (started) {
return;
}
}
if (pendingRequest != null) {
// We went through all handlers without successfully attempting an auth.
completeWithFailure();
}
}
private void completeWithFailure() {
complete(Result.createErrorResult(pendingRequest, "Login attempt failed.", null));
}
private void addLoggingExtra(String key, String value, boolean accumulate) {
if (loggingExtras == null) {
loggingExtras = new HashMap<String, String>();
}
if (loggingExtras.containsKey(key) && accumulate) {
value = loggingExtras.get(key) + "," + value;
}
loggingExtras.put(key, value);
}
boolean tryCurrentHandler() {
if (currentHandler.needsInternetPermission() && !checkInternetPermission()) {
addLoggingExtra(EVENT_EXTRAS_MISSING_INTERNET_PERMISSION, AppEventsConstants.EVENT_PARAM_VALUE_YES,
false);
return false;
}
boolean tried = currentHandler.tryAuthorize(pendingRequest);
if (tried) {
logAuthorizationMethodStart(currentHandler.getNameForLogging());
} else {
// We didn't try it, so we don't get any other completion notification -- log that we skipped it.
addLoggingExtra(EVENT_EXTRAS_NOT_TRIED, currentHandler.getNameForLogging(), true);
}
return tried;
}
void completeAndValidate(Result outcome) {
// Do we need to validate a successful result (as in the case of a reauth)?
if (outcome.token != null && pendingRequest.needsNewTokenValidation()) {
validateSameFbidAndFinish(outcome);
} else {
// We're done, just notify the listener.
complete(outcome);
}
}
void complete(Result outcome) {
// This might be null if, for some reason, none of the handlers were successfully tried (in which case
// we already logged that).
if (currentHandler != null) {
logAuthorizationMethodComplete(currentHandler.getNameForLogging(), outcome,
currentHandler.methodLoggingExtras);
}
if (loggingExtras != null) {
// Pass this back to the caller for logging at the aggregate level.
outcome.loggingExtras = loggingExtras;
}
handlersToTry = null;
currentHandler = null;
pendingRequest = null;
loggingExtras = null;
notifyOnCompleteListener(outcome);
}
OnCompletedListener getOnCompletedListener() {
return onCompletedListener;
}
void setOnCompletedListener(OnCompletedListener onCompletedListener) {
this.onCompletedListener = onCompletedListener;
}
BackgroundProcessingListener getBackgroundProcessingListener() {
return backgroundProcessingListener;
}
void setBackgroundProcessingListener(BackgroundProcessingListener backgroundProcessingListener) {
this.backgroundProcessingListener = backgroundProcessingListener;
}
StartActivityDelegate getStartActivityDelegate() {
if (startActivityDelegate != null) {
return startActivityDelegate;
} else if (pendingRequest != null) {
// Wrap the request's delegate in our own.
return new StartActivityDelegate() {
@Override
public void startActivityForResult(Intent intent, int requestCode) {
pendingRequest.getStartActivityDelegate().startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return pendingRequest.getStartActivityDelegate().getActivityContext();
}
};
}
return null;
}
int checkPermission(String permission) {
return context.checkCallingOrSelfPermission(permission);
}
void validateSameFbidAndFinish(Result pendingResult) {
if (pendingResult.token == null) {
throw new FacebookException("Can't validate without a token");
}
RequestBatch batch = createReauthValidationBatch(pendingResult);
notifyBackgroundProcessingStart();
batch.executeAsync();
}
RequestBatch createReauthValidationBatch(final Result pendingResult) {
// We need to ensure that the token we got represents the same fbid as the old one. We issue
// a "me" request using the current token, a "me" request using the new token, and a "me/permissions"
// request using the current token to get the permissions of the user.
final ArrayList<String> fbids = new ArrayList<String>();
final ArrayList<String> grantedPermissions = new ArrayList<String>();
final ArrayList<String> declinedPermissions = new ArrayList<String>();
final String newToken = pendingResult.token.getToken();
Request.Callback meCallback = new Request.Callback() {
@Override
public void onCompleted(Response response) {
try {
GraphUser user = response.getGraphObjectAs(GraphUser.class);
if (user != null) {
fbids.add(user.getId());
}
} catch (Exception ex) {
}
}
};
String validateSameFbidAsToken = pendingRequest.getPreviousAccessToken();
Request requestCurrentTokenMe = createGetProfileIdRequest(validateSameFbidAsToken);
requestCurrentTokenMe.setCallback(meCallback);
Request requestNewTokenMe = createGetProfileIdRequest(newToken);
requestNewTokenMe.setCallback(meCallback);
Request requestCurrentTokenPermissions = createGetPermissionsRequest(validateSameFbidAsToken);
requestCurrentTokenPermissions.setCallback(new Request.Callback() {
@Override
public void onCompleted(Response response) {
try {
Session.PermissionsPair permissionsPair = Session.handlePermissionResponse(response);
if (permissionsPair != null) {
grantedPermissions.addAll(permissionsPair.getGrantedPermissions());
declinedPermissions.addAll(permissionsPair.getDeclinedPermissions());
}
} catch (Exception ex) {
}
}
});
RequestBatch batch = new RequestBatch(requestCurrentTokenMe, requestNewTokenMe,
requestCurrentTokenPermissions);
batch.setBatchApplicationId(pendingRequest.getApplicationId());
batch.addCallback(new RequestBatch.Callback() {
@Override
public void onBatchCompleted(RequestBatch batch) {
try {
Result result = null;
if (fbids.size() == 2 && fbids.get(0) != null && fbids.get(1) != null &&
fbids.get(0).equals(fbids.get(1))) {
// Modify the token to have the right permission set.
AccessToken tokenWithPermissions = AccessToken
.createFromTokenWithRefreshedPermissions(pendingResult.token,
grantedPermissions, declinedPermissions);
result = Result.createTokenResult(pendingRequest, tokenWithPermissions);
} else {
result = Result
.createErrorResult(pendingRequest, "User logged in as different Facebook user.", null);
}
complete(result);
} catch (Exception ex) {
complete(Result.createErrorResult(pendingRequest, "Caught exception", ex.getMessage()));
} finally {
notifyBackgroundProcessingStop();
}
}
});
return batch;
}
Request createGetPermissionsRequest(String accessToken) {
Bundle parameters = new Bundle();
parameters.putString("access_token", accessToken);
return new Request(null, "me/permissions", parameters, HttpMethod.GET, null);
}
Request createGetProfileIdRequest(String accessToken) {
Bundle parameters = new Bundle();
parameters.putString("fields", "id");
parameters.putString("access_token", accessToken);
return new Request(null, "me", parameters, HttpMethod.GET, null);
}
private AppEventsLogger getAppEventsLogger() {
if (appEventsLogger == null || !appEventsLogger.getApplicationId().equals(pendingRequest.getApplicationId())) {
appEventsLogger = AppEventsLogger.newLogger(context, pendingRequest.getApplicationId());
}
return appEventsLogger;
}
private void notifyOnCompleteListener(Result outcome) {
if (onCompletedListener != null) {
onCompletedListener.onCompleted(outcome);
}
}
private void notifyBackgroundProcessingStart() {
if (backgroundProcessingListener != null) {
backgroundProcessingListener.onBackgroundProcessingStarted();
}
}
private void notifyBackgroundProcessingStop() {
if (backgroundProcessingListener != null) {
backgroundProcessingListener.onBackgroundProcessingStopped();
}
}
private void logAuthorizationMethodStart(String method) {
Bundle bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId());
bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
bundle.putString(EVENT_PARAM_METHOD, method);
getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_START, null, bundle);
}
private void logAuthorizationMethodComplete(String method, Result result, Map<String, String> loggingExtras) {
logAuthorizationMethodComplete(method, result.code.getLoggingValue(), result.errorMessage, result.errorCode,
loggingExtras);
}
private void logAuthorizationMethodComplete(String method, String result, String errorMessage, String errorCode,
Map<String, String> loggingExtras) {
Bundle bundle = null;
if (pendingRequest == null) {
// We don't expect this to happen, but if it does, log an event for diagnostic purposes.
bundle = newAuthorizationLoggingBundle("");
bundle.putString(EVENT_PARAM_LOGIN_RESULT, Result.Code.ERROR.getLoggingValue());
bundle.putString(EVENT_PARAM_ERROR_MESSAGE,
"Unexpected call to logAuthorizationMethodComplete with null pendingRequest.");
} else {
bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId());
if (result != null) {
bundle.putString(EVENT_PARAM_LOGIN_RESULT, result);
}
if (errorMessage != null) {
bundle.putString(EVENT_PARAM_ERROR_MESSAGE, errorMessage);
}
if (errorCode != null) {
bundle.putString(EVENT_PARAM_ERROR_CODE, errorCode);
}
if (loggingExtras != null && !loggingExtras.isEmpty()) {
JSONObject jsonObject = new JSONObject(loggingExtras);
bundle.putString(EVENT_PARAM_EXTRAS, jsonObject.toString());
}
}
bundle.putString(EVENT_PARAM_METHOD, method);
bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_COMPLETE, null, bundle);
}
static Bundle newAuthorizationLoggingBundle(String authLoggerId) {
// We want to log all parameters for all events, to ensure stability of columns across different event types.
Bundle bundle = new Bundle();
bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
bundle.putString(EVENT_PARAM_AUTH_LOGGER_ID, authLoggerId);
bundle.putString(EVENT_PARAM_METHOD, "");
bundle.putString(EVENT_PARAM_LOGIN_RESULT, "");
bundle.putString(EVENT_PARAM_ERROR_MESSAGE, "");
bundle.putString(EVENT_PARAM_ERROR_CODE, "");
bundle.putString(EVENT_PARAM_EXTRAS, "");
return bundle;
}
abstract class AuthHandler implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, String> methodLoggingExtras;
abstract boolean tryAuthorize(AuthorizationRequest request);
abstract String getNameForLogging();
boolean onActivityResult(int requestCode, int resultCode, Intent data) {
return false;
}
boolean needsRestart() {
return false;
}
boolean needsInternetPermission() {
return false;
}
void cancel() {
}
protected void addLoggingExtra(String key, Object value) {
if (methodLoggingExtras == null) {
methodLoggingExtras = new HashMap<String, String>();
}
methodLoggingExtras.put(key, value == null ? null : value.toString());
}
}
class WebViewAuthHandler extends AuthHandler {
private static final long serialVersionUID = 1L;
private transient WebDialog loginDialog;
private String applicationId;
private String e2e;
@Override
String getNameForLogging() {
return "web_view";
}
@Override
boolean needsRestart() {
// Because we are presenting WebView UI within the current context, we need to explicitly
// restart the process if the context goes away and is recreated.
return true;
}
@Override
boolean needsInternetPermission() {
return true;
}
@Override
void cancel() {
if (loginDialog != null) {
// Since we are calling dismiss explicitly, we need to remove the completion listener to prevent
// responding to the upcoming "Cancel" result.
loginDialog.setOnCompleteListener(null);
loginDialog.dismiss();
loginDialog = null;
}
}
@Override
boolean tryAuthorize(final AuthorizationRequest request) {
applicationId = request.getApplicationId();
Bundle parameters = new Bundle();
if (!Utility.isNullOrEmpty(request.getPermissions())) {
String scope = TextUtils.join(",", request.getPermissions());
parameters.putString(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
addLoggingExtra(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
}
SessionDefaultAudience audience = request.getDefaultAudience();
parameters.putString(ServerProtocol.DIALOG_PARAM_DEFAULT_AUDIENCE, audience.getNativeProtocolAudience());
String previousToken = request.getPreviousAccessToken();
if (!Utility.isNullOrEmpty(previousToken) && (previousToken.equals(loadCookieToken()))) {
parameters.putString(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, previousToken);
// Don't log the actual access token, just its presence or absence.
addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_YES);
} else {
// The call to clear cookies will create the first instance of CookieSyncManager if necessary
Utility.clearFacebookCookies(context);
addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_NO);
}
WebDialog.OnCompleteListener listener = new WebDialog.OnCompleteListener() {
@Override
public void onComplete(Bundle values, FacebookException error) {
onWebDialogComplete(request, values, error);
}
};
e2e = getE2E();
addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e);
WebDialog.Builder builder =
new AuthDialogBuilder(getStartActivityDelegate().getActivityContext(), applicationId, parameters)
.setE2E(e2e)
.setIsRerequest(request.isRerequest())
.setOnCompleteListener(listener);
loginDialog = builder.build();
loginDialog.show();
return true;
}
void onWebDialogComplete(AuthorizationRequest request, Bundle values,
FacebookException error) {
Result outcome;
if (values != null) {
// Actual e2e we got from the dialog should be used for logging.
if (values.containsKey(ServerProtocol.DIALOG_PARAM_E2E)) {
e2e = values.getString(ServerProtocol.DIALOG_PARAM_E2E);
}
AccessToken token = AccessToken
.createFromWebBundle(request.getPermissions(), values, AccessTokenSource.WEB_VIEW);
outcome = Result.createTokenResult(pendingRequest, token);
// Ensure any cookies set by the dialog are saved
// This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager
// has never been created.
CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
syncManager.sync();
saveCookieToken(token.getToken());
} else {
if (error instanceof FacebookOperationCanceledException) {
outcome = Result.createCancelResult(pendingRequest, "User canceled log in.");
} else {
// Something went wrong, don't log a completion event since it will skew timing results.
e2e = null;
String errorCode = null;
String errorMessage = error.getMessage();
if (error instanceof FacebookServiceException) {
FacebookRequestError requestError = ((FacebookServiceException)error).getRequestError();
errorCode = String.format("%d", requestError.getErrorCode());
errorMessage = requestError.toString();
}
outcome = Result.createErrorResult(pendingRequest, null, errorMessage, errorCode);
}
}
if (!Utility.isNullOrEmpty(e2e)) {
logWebLoginCompleted(applicationId, e2e);
}
completeAndValidate(outcome);
}
private void saveCookieToken(String token) {
Context context = getStartActivityDelegate().getActivityContext();
context.getSharedPreferences(
WEB_VIEW_AUTH_HANDLER_STORE,
Context.MODE_PRIVATE)
.edit()
.putString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, token)
.apply();
}
private String loadCookieToken() {
Context context = getStartActivityDelegate().getActivityContext();
SharedPreferences sharedPreferences = context.getSharedPreferences(
WEB_VIEW_AUTH_HANDLER_STORE,
Context.MODE_PRIVATE);
return sharedPreferences.getString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, "");
}
}
class GetTokenAuthHandler extends AuthHandler {
private static final long serialVersionUID = 1L;
private transient GetTokenClient getTokenClient;
@Override
String getNameForLogging() {
return "get_token";
}
@Override
void cancel() {
if (getTokenClient != null) {
getTokenClient.cancel();
getTokenClient = null;
}
}
@Override
boolean needsRestart() {
// if the getTokenClient is null, that means an orientation change has occurred, and we need
// to recreate the GetTokenClient, so return true to indicate we need a restart
return getTokenClient == null;
}
boolean tryAuthorize(final AuthorizationRequest request) {
getTokenClient = new GetTokenClient(context, request.getApplicationId());
if (!getTokenClient.start()) {
return false;
}
notifyBackgroundProcessingStart();
GetTokenClient.CompletedListener callback = new GetTokenClient.CompletedListener() {
@Override
public void completed(Bundle result) {
getTokenCompleted(request, result);
}
};
getTokenClient.setCompletedListener(callback);
return true;
}
void getTokenCompleted(AuthorizationRequest request, Bundle result) {
getTokenClient = null;
notifyBackgroundProcessingStop();
if (result != null) {
ArrayList<String> currentPermissions = result.getStringArrayList(NativeProtocol.EXTRA_PERMISSIONS);
List<String> permissions = request.getPermissions();
if ((currentPermissions != null) &&
((permissions == null) || currentPermissions.containsAll(permissions))) {
// We got all the permissions we needed, so we can complete the auth now.
AccessToken token = AccessToken
.createFromNativeLogin(result, AccessTokenSource.FACEBOOK_APPLICATION_SERVICE);
Result outcome = Result.createTokenResult(pendingRequest, token);
completeAndValidate(outcome);
return;
}
// We didn't get all the permissions we wanted, so update the request with just the permissions
// we still need.
List<String> newPermissions = new ArrayList<String>();
for (String permission : permissions) {
if (!currentPermissions.contains(permission)) {
newPermissions.add(permission);
}
}
if (!newPermissions.isEmpty()) {
addLoggingExtra(EVENT_EXTRAS_NEW_PERMISSIONS, TextUtils.join(",", newPermissions));
}
request.setPermissions(newPermissions);
}
tryNextHandler();
}
}
abstract class KatanaAuthHandler extends AuthHandler {
private static final long serialVersionUID = 1L;
protected boolean tryIntent(Intent intent, int requestCode) {
if (intent == null) {
return false;
}
try {
getStartActivityDelegate().startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
// We don't expect this to happen, since we've already validated the intent and bailed out before
// now if it couldn't be resolved.
return false;
}
return true;
}
}
class KatanaProxyAuthHandler extends KatanaAuthHandler {
private static final long serialVersionUID = 1L;
private String applicationId;
@Override
String getNameForLogging() {
return "katana_proxy_auth";
}
@Override
boolean tryAuthorize(AuthorizationRequest request) {
applicationId = request.getApplicationId();
String e2e = getE2E();
Intent intent = NativeProtocol.createProxyAuthIntent(context, request.getApplicationId(),
request.getPermissions(), e2e, request.isRerequest(), request.getDefaultAudience());
addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e);
return tryIntent(intent, request.getRequestCode());
}
@Override
boolean onActivityResult(int requestCode, int resultCode, Intent data) {
// Handle stuff
Result outcome;
if (data == null) {
// This happens if the user presses 'Back'.
outcome = Result.createCancelResult(pendingRequest, "Operation canceled");
} else if (resultCode == Activity.RESULT_CANCELED) {
outcome = Result.createCancelResult(pendingRequest, data.getStringExtra("error"));
} else if (resultCode != Activity.RESULT_OK) {
outcome = Result.createErrorResult(pendingRequest, "Unexpected resultCode from authorization.", null);
} else {
outcome = handleResultOk(data);
}
if (outcome != null) {
completeAndValidate(outcome);
} else {
tryNextHandler();
}
return true;
}
private Result handleResultOk(Intent data) {
Bundle extras = data.getExtras();
String error = extras.getString("error");
if (error == null) {
error = extras.getString("error_type");
}
String errorCode = extras.getString("error_code");
String errorMessage = extras.getString("error_message");
if (errorMessage == null) {
errorMessage = extras.getString("error_description");
}
String e2e = extras.getString(NativeProtocol.FACEBOOK_PROXY_AUTH_E2E_KEY);
if (!Utility.isNullOrEmpty(e2e)) {
logWebLoginCompleted(applicationId, e2e);
}
if (error == null && errorCode == null && errorMessage == null) {
AccessToken token = AccessToken.createFromWebBundle(pendingRequest.getPermissions(), extras,
AccessTokenSource.FACEBOOK_APPLICATION_WEB);
return Result.createTokenResult(pendingRequest, token);
} else if (ServerProtocol.errorsProxyAuthDisabled.contains(error)) {
return null;
} else if (ServerProtocol.errorsUserCanceled.contains(error)) {
return Result.createCancelResult(pendingRequest, null);
} else {
return Result.createErrorResult(pendingRequest, error, errorMessage, errorCode);
}
}
}
private static String getE2E() {
JSONObject e2e = new JSONObject();
try {
e2e.put("init", System.currentTimeMillis());
} catch (JSONException e) {
}
return e2e.toString();
}
private void logWebLoginCompleted(String applicationId, String e2e) {
AppEventsLogger appEventsLogger = AppEventsLogger.newLogger(context, applicationId);
Bundle parameters = new Bundle();
parameters.putString(AnalyticsEvents.PARAMETER_WEB_LOGIN_E2E, e2e);
parameters.putLong(AnalyticsEvents.PARAMETER_WEB_LOGIN_SWITCHBACK_TIME, System.currentTimeMillis());
parameters.putString(AnalyticsEvents.PARAMETER_APP_ID, applicationId);
appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_WEB_LOGIN_COMPLETE, null, parameters);
}
static class AuthDialogBuilder extends WebDialog.Builder {
private static final String OAUTH_DIALOG = "oauth";
static final String REDIRECT_URI = "fbconnect://success";
private String e2e;
private boolean isRerequest;
public AuthDialogBuilder(Context context, String applicationId, Bundle parameters) {
super(context, applicationId, OAUTH_DIALOG, parameters);
}
public AuthDialogBuilder setE2E(String e2e) {
this.e2e = e2e;
return this;
}
public AuthDialogBuilder setIsRerequest(boolean isRerequest) {
this.isRerequest = isRerequest;
return this;
}
@Override
public WebDialog build() {
Bundle parameters = getParameters();
parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, REDIRECT_URI);
parameters.putString(ServerProtocol.DIALOG_PARAM_CLIENT_ID, getApplicationId());
parameters.putString(ServerProtocol.DIALOG_PARAM_E2E, e2e);
parameters.putString(ServerProtocol.DIALOG_PARAM_RESPONSE_TYPE, ServerProtocol.DIALOG_RESPONSE_TYPE_TOKEN);
parameters.putString(ServerProtocol.DIALOG_PARAM_RETURN_SCOPES, ServerProtocol.DIALOG_RETURN_SCOPES_TRUE);
// Only set the rerequest auth type for non legacy requests
if (isRerequest && !Settings.getPlatformCompatibilityEnabled()) {
parameters.putString(ServerProtocol.DIALOG_PARAM_AUTH_TYPE, ServerProtocol.DIALOG_REREQUEST_AUTH_TYPE);
}
return new WebDialog(getContext(), OAUTH_DIALOG, parameters, getTheme(), getListener());
}
}
static class AuthorizationRequest implements Serializable {
private static final long serialVersionUID = 1L;
private transient final StartActivityDelegate startActivityDelegate;
private final SessionLoginBehavior loginBehavior;
private final int requestCode;
private boolean isLegacy = false;
private List<String> permissions;
private final SessionDefaultAudience defaultAudience;
private final String applicationId;
private final String previousAccessToken;
private final String authId;
private boolean isRerequest = false;
AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode, boolean isLegacy,
List<String> permissions, SessionDefaultAudience defaultAudience, String applicationId,
String validateSameFbidAsToken, StartActivityDelegate startActivityDelegate, String authId) {
this.loginBehavior = loginBehavior;
this.requestCode = requestCode;
this.isLegacy = isLegacy;
this.permissions = permissions;
this.defaultAudience = defaultAudience;
this.applicationId = applicationId;
this.previousAccessToken = validateSameFbidAsToken;
this.startActivityDelegate = startActivityDelegate;
this.authId = authId;
}
StartActivityDelegate getStartActivityDelegate() {
return startActivityDelegate;
}
List<String> getPermissions() {
return permissions;
}
void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
SessionLoginBehavior getLoginBehavior() {
return loginBehavior;
}
int getRequestCode() {
return requestCode;
}
SessionDefaultAudience getDefaultAudience() {
return defaultAudience;
}
String getApplicationId() {
return applicationId;
}
boolean isLegacy() {
return isLegacy;
}
void setIsLegacy(boolean isLegacy) {
this.isLegacy = isLegacy;
}
String getPreviousAccessToken() {
return previousAccessToken;
}
boolean needsNewTokenValidation() {
return previousAccessToken != null && !isLegacy;
}
String getAuthId() {
return authId;
}
boolean isRerequest() {
return isRerequest;
}
void setRerequest(boolean isRerequest) {
this.isRerequest = isRerequest;
}
}
static class Result implements Serializable {
private static final long serialVersionUID = 1L;
enum Code {
SUCCESS("success"),
CANCEL("cancel"),
ERROR("error");
private final String loggingValue;
Code(String loggingValue) {
this.loggingValue = loggingValue;
}
// For consistency across platforms, we want to use specific string values when logging these results.
String getLoggingValue() {
return loggingValue;
}
}
final Code code;
final AccessToken token;
final String errorMessage;
final String errorCode;
final AuthorizationRequest request;
Map<String, String> loggingExtras;
private Result(AuthorizationRequest request, Code code, AccessToken token, String errorMessage,
String errorCode) {
this.request = request;
this.token = token;
this.errorMessage = errorMessage;
this.code = code;
this.errorCode = errorCode;
}
static Result createTokenResult(AuthorizationRequest request, AccessToken token) {
return new Result(request, Code.SUCCESS, token, null, null);
}
static Result createCancelResult(AuthorizationRequest request, String message) {
return new Result(request, Code.CANCEL, null, message, null);
}
static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription) {
return createErrorResult(request, errorType, errorDescription, null);
}
static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription,
String errorCode) {
String message = TextUtils.join(": ", Utility.asListNoNulls(errorType, errorDescription));
return new Result(request, Code.ERROR, null, message, errorCode);
}
}
}