Make 2FA login work with commons

This commit is contained in:
maskara 2018-01-02 23:21:08 +05:30
parent 022ac948a8
commit 1ea0a1ebeb
9 changed files with 271 additions and 209 deletions

View file

@ -19,6 +19,8 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD
public class AccountUtil { public class AccountUtil {
public static final String ACCOUNT_TYPE = "fr.free.nrw.commons"; public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
public static final String AUTH_COOKIE = "authCookie";
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
private final Context context; private final Context context;
public AccountUtil(Context context) { public AccountUtil(Context context) {

View file

@ -1,72 +1,35 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.os.Bundle; import android.os.Bundle;
import javax.inject.Inject; import javax.inject.Inject;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity { public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Inject SessionManager sessionManager; @Inject SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
private String authCookie; private String authCookie;
private void getAuthCookie(Account account, AccountManager accountManager) {
Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false))
.subscribeOn(Schedulers.io())
.doOnError(Timber::e)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
this:: onAuthCookieAcquired,
throwable -> onAuthFailure());
}
private void addAccount(AccountManager accountManager) {
Single.just(accountManager.addAccount(ACCOUNT_TYPE, null, null,
null, AuthenticatedActivity.this, null, null))
.subscribeOn(Schedulers.io())
.map(AccountManagerFuture::getResult)
.doOnEvent((bundle, throwable) -> {
if (!bundle.containsKey(KEY_ACCOUNT_NAME)) {
throw new RuntimeException("Bundle doesn't contain account-name key: "
+ KEY_ACCOUNT_NAME);
}
})
.map(bundle -> bundle.getString(KEY_ACCOUNT_NAME))
.doOnError(Timber::e)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
Account curAccount = allAccounts[0];
getAuthCookie(curAccount, accountManager);
},
throwable -> onAuthFailure());
}
protected void requestAuthToken() { protected void requestAuthToken() {
if (authCookie != null) { if (authCookie != null) {
onAuthCookieAcquired(authCookie); onAuthCookieAcquired(authCookie);
return; return;
} }
AccountManager accountManager = AccountManager.get(this); sessionManager.getAndSetAuthCookie()
Account curAccount = sessionManager.getCurrentAccount(); .subscribeOn(Schedulers.newThread())
if (curAccount == null) { .observeOn(AndroidSchedulers.mainThread())
addAccount(accountManager); .subscribe(cookie -> {
} else { authCookie = cookie;
getAuthCookie(curAccount, accountManager); onAuthCookieAcquired(authCookie);
} });
} }
@Override @Override
@ -74,14 +37,14 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
authCookie = savedInstanceState.getString("authCookie"); authCookie = savedInstanceState.getString(AUTH_COOKIE);
} }
} }
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString("authCookie", authCookie); outState.putString(AUTH_COOKIE, authCookie);
} }
protected abstract void onAuthCookieAcquired(String authCookie); protected abstract void onAuthCookieAcquired(String authCookie);

View file

@ -1,6 +1,9 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity; import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -8,6 +11,7 @@ import android.os.Bundle;
import android.support.annotation.ColorRes; import android.support.annotation.ColorRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;
@ -21,6 +25,8 @@ import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -36,10 +42,16 @@ import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
public class LoginActivity extends AccountAuthenticatorActivity { public class LoginActivity extends AccountAuthenticatorActivity {
@ -58,6 +70,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit; @BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer; @BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
@BindView(R.id.error_message) TextView errorMessage; @BindView(R.id.error_message) TextView errorMessage;
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
ProgressDialog progressDialog; ProgressDialog progressDialog;
private AppCompatDelegate delegate; private AppCompatDelegate delegate;
private LoginTextWatcher textWatcher = new LoginTextWatcher(); private LoginTextWatcher textWatcher = new LoginTextWatcher();
@ -122,14 +135,109 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onDestroy(); super.onDestroy();
} }
private LoginTask getLoginTask() { private void performLogin() {
return new LoginTask( Timber.d("Login to start!");
this, final String username = canonicializeUsername(usernameEdit.getText().toString());
canonicializeUsername(usernameEdit.getText().toString()), final String password = passwordEdit.getText().toString();
passwordEdit.getText().toString(), String twoFactorCode = twoFactorEdit.getText().toString();
twoFactorEdit.getText().toString(),
accountUtil, mwApi, defaultPrefs showLoggingProgressBar();
); Observable.fromCallable(() -> login(username, password, twoFactorCode))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> handleLogin(username, password, result));
}
private String login(String username, String password, String twoFactorCode) {
try {
if (twoFactorCode.isEmpty()) {
return mwApi.login(username, password);
} else {
return mwApi.login(username, password, twoFactorCode);
}
} catch (IOException e) {
// Do something better!
return "NetworkFailure";
}
}
private void handleLogin(String username, String password, String result) {
Timber.d("Login done!");
if (result.equals("PASS")) {
handlePassResult(username, password);
} else {
handleOtherResults(result);
}
}
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setTitle(getString(R.string.logging_in_title));
progressDialog.setMessage(getString(R.string.logging_in_message));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.show();
}
private void handlePassResult(String username, String password) {
showSuccessAndDismissDialog();
requestAuthToken();
AccountAuthenticatorResponse response = null;
Bundle extras = getIntent().getExtras();
if (extras != null) {
Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) {
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(authResult);
}
}
accountUtil.createAccount(response, username, password);
startMainActivity();
}
protected void requestAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = sessionManager.getCurrentAccount();
if (curAccount != null) {
accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
}
}
/**
* Match known failure message codes and provide messages.
*
* @param result String
*/
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
showMessageAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
showMessageAndCancelDialog(R.string.login_failed_username);
emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
showMessageAndCancelDialog(R.string.login_failed_password);
emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
showMessageAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
// Matches login-userblocked
showMessageAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
askUserForTwoFactorAuth();
} else {
// Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result);
showMessageAndCancelDialog(R.string.login_failed_generic);
}
} }
/** /**
@ -182,7 +290,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
public void askUserForTwoFactorAuth() { public void askUserForTwoFactorAuth() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
twoFactorEdit.setVisibility(View.VISIBLE); twoFactorContainer.setVisibility(VISIBLE);
twoFactorEdit.setVisibility(VISIBLE);
showMessageAndCancelDialog(R.string.login_failed_2fa_needed); showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
} else { } else {
showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported); showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported);
@ -209,12 +318,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
finish(); finish();
} }
private void performLogin() {
Timber.d("Login to start!");
LoginTask task = getLoginTask();
task.execute();
}
private void signUp() { private void signUp() {
Intent intent = new Intent(this, SignupActivity.class); Intent intent = new Intent(this, SignupActivity.class);
startActivity(intent); startActivity(intent);
@ -238,7 +341,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private void showMessage(@StringRes int resId, @ColorRes int colorResId) { private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
errorMessage.setText(getString(resId)); errorMessage.setText(getString(resId));
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
errorMessageContainer.setVisibility(View.VISIBLE); errorMessageContainer.setVisibility(VISIBLE);
} }
private AppCompatDelegate getDelegate() { private AppCompatDelegate getDelegate() {
@ -260,7 +363,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Override @Override
public void afterTextChanged(Editable editable) { public void afterTextChanged(Editable editable) {
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE); && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
loginButton.setEnabled(enabled); loginButton.setEnabled(enabled);
} }
} }

View file

@ -2,17 +2,16 @@ package fr.free.nrw.commons.auth;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context; import android.content.Context;
import java.io.IOException;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
/** /**
* Manage the current logged in user session. * Manage the current logged in user session.
@ -51,14 +50,31 @@ public class SessionManager {
} }
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
try { getAndSetAuthCookie().subscribeOn(Schedulers.io())
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); .subscribe(authCookie -> {
mediaWikiApi.setAuthCookie(authCookie); mediaWikiApi.setAuthCookie(authCookie);
return true; });
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { return true;
e.printStackTrace(); }
return false;
} public Observable<String> getAndSetAuthCookie() {
AccountManager accountManager = AccountManager.get(context);
Account curAccount = getCurrentAccount();
return Observable.fromCallable(() -> {
String authCookie = accountManager.blockingGetAuthToken(curAccount, AUTH_TOKEN_TYPE, false);
if (authCookie == null) {
Timber.d("Media wiki auth cookie is %s", mediaWikiApi.getAuthCookie());
authCookie = mediaWikiApi.getAuthCookie();
//authCookie = currentAccount.name + "|" + currentAccount.type + "|" + mediaWikiApi.getUserAgent();
//mediaWikiApi.setAuthCookie(authCookie);
}
Timber.d("Auth cookie is %s", authCookie);
return authCookie;
}).onErrorReturn(throwable-> {
Timber.e(throwable, "Auth cookie is still null :(");
return null;
});
} }
public Completable clearAllAccounts() { public Completable clearAllAccounts() {

View file

@ -5,53 +5,37 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.accounts.NetworkErrorException; import android.accounts.NetworkErrorException;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.io.IOException; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
import static android.accounts.AccountManager.KEY_ERROR_CODE;
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
import static android.accounts.AccountManager.KEY_INTENT;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.AUTHORITY, ModificationsContentProvider.AUTHORITY};
@NonNull
private final Context context; private final Context context;
private MediaWikiApi mediaWikiApi;
WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) { public WikiAccountAuthenticator(@NonNull Context context) {
super(context); super(context);
this.context = context; this.context = context;
this.mediaWikiApi = mwApi;
} }
private Bundle unsupportedOperation() { @Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION); bundle.putString("test", "editProperties");
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
bundle.putString(KEY_ERROR_MESSAGE, "");
return bundle; return bundle;
} }
private boolean supportedAccountType(@Nullable String type) {
return ACCOUNT_TYPE.equals(type);
}
@Override @Override
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
@NonNull String accountType, @Nullable String authTokenType, @NonNull String accountType, @Nullable String authTokenType,
@ -59,86 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
throws NetworkErrorException { throws NetworkErrorException {
if (!supportedAccountType(accountType)) { if (!supportedAccountType(accountType)) {
return unsupportedOperation(); Bundle bundle = new Bundle();
bundle.putString("test", "addAccount");
return bundle;
} }
return addAccount(response); return addAccount(response);
} }
private Bundle addAccount(AccountAuthenticatorResponse response) {
Intent Intent = new Intent(context, LoginActivity.class);
Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(KEY_INTENT, Intent);
return bundle;
}
@Override @Override
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable Bundle options) @NonNull Account account, @Nullable Bundle options)
throws NetworkErrorException { throws NetworkErrorException {
return unsupportedOperation(); Bundle bundle = new Bundle();
bundle.putString("test", "confirmCredentials");
return bundle;
} }
@Override @Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
return unsupportedOperation(); @NonNull Account account, @NonNull String authTokenType,
} @Nullable Bundle options)
throws NetworkErrorException {
private String getAuthCookie(String username, String password) throws IOException { Bundle bundle = new Bundle();
//TODO add 2fa support here bundle.putString("test", "getAuthToken");
String result = mediaWikiApi.login(username, password);
if (result.equals("PASS")) {
return mediaWikiApi.getAuthCookie();
} else {
return null;
}
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle options) throws NetworkErrorException {
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
final AccountManager am = AccountManager.get(context);
final String password = am.getPassword(account);
if (password != null) {
String authCookie;
try {
authCookie = getAuthCookie(account.name, password);
} catch (IOException e) {
// Network error!
e.printStackTrace();
throw new NetworkErrorException(e);
}
if (authCookie != null) {
final Bundle result = new Bundle();
result.putString(KEY_ACCOUNT_NAME, account.name);
result.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
result.putString(KEY_AUTHTOKEN, authCookie);
return result;
}
}
// If we get here, then we couldn't access the user's password - so we
// need to re-prompt them for their credentials. We do that by creating
// an intent to display our AuthenticatorActivity panel.
final Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(PARAM_USERNAME, account.name);
intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(KEY_INTENT, intent);
return bundle; return bundle;
} }
@Nullable @Nullable
@Override @Override
public String getAuthTokenLabel(@NonNull String authTokenType) { public String getAuthTokenLabel(@NonNull String authTokenType) {
//Note: the wikipedia app actually returns a string here.... return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null; }
return null;
@Nullable
@Override
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "updateCredentials");
return bundle;
} }
@Nullable @Nullable
@ -147,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
@NonNull Account account, @NonNull String[] features) @NonNull Account account, @NonNull String[] features)
throws NetworkErrorException { throws NetworkErrorException {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBoolean(KEY_BOOLEAN_RESULT, false); bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return bundle; return bundle;
} }
@Nullable private boolean supportedAccountType(@Nullable String type) {
@Override return ACCOUNT_TYPE.equals(type);
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable String authTokenType,
@Nullable Bundle options) throws NetworkErrorException {
return unsupportedOperation();
} }
private Bundle addAccount(AccountAuthenticatorResponse response) {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
private Bundle unsupportedOperation() {
Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "");
return bundle;
}
@Override
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
Account account) throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)) {
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (allowed) {
for (String auth : SYNC_AUTHORITIES) {
ContentResolver.cancelSync(account, auth);
}
}
}
return result;
}
} }

View file

@ -1,30 +1,26 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.AbstractAccountAuthenticator;
import android.content.Intent; import android.content.Intent;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.Nullable;
import javax.inject.Inject;
import fr.free.nrw.commons.di.CommonsDaggerService; import fr.free.nrw.commons.di.CommonsDaggerService;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT;
public class WikiAccountAuthenticatorService extends CommonsDaggerService { public class WikiAccountAuthenticatorService extends CommonsDaggerService {
@Inject MediaWikiApi mwApi; @Nullable
private WikiAccountAuthenticator wikiAccountAuthenticator = null; private AbstractAccountAuthenticator authenticator;
@Override @Override
public IBinder onBind(Intent intent) { public void onCreate() {
if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) { super.onCreate();
return null; authenticator = new WikiAccountAuthenticator(this);
}
if (wikiAccountAuthenticator == null) {
wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi);
}
return wikiAccountAuthenticator.getIBinder();
} }
@Nullable
@Override
public IBinder onBind(Intent intent) {
return authenticator == null ? null : authenticator.getIBinder();
}
} }

View file

@ -1,9 +1,6 @@
package fr.free.nrw.commons.modifications; package fr.free.nrw.commons.modifications;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter; import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.Context; import android.content.Context;
@ -16,7 +13,7 @@ import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
@ -29,6 +26,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Inject MediaWikiApi mwApi; @Inject MediaWikiApi mwApi;
@Inject ContributionDao contributionDao; @Inject ContributionDao contributionDao;
@Inject ModifierSequenceDao modifierSequenceDao; @Inject ModifierSequenceDao modifierSequenceDao;
@Inject
SessionManager sessionManager;
public ModificationsSyncAdapter(Context context, boolean autoInitialize) { public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize); super(context, autoInitialize);
@ -56,16 +55,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return; return;
} }
String authCookie; String authCookie = sessionManager.getAndSetAuthCookie().blockingSingle();
try {
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
} catch (OperationCanceledException | AuthenticatorException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Timber.d("Could not authenticate :(");
return;
}
if (isNullOrWhiteSpace(authCookie)) { if (isNullOrWhiteSpace(authCookie)) {
Timber.d("Could not authenticate :("); Timber.d("Could not authenticate :(");
return; return;

View file

@ -69,11 +69,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE); params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params); httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient); api = new MWApi(apiURL, httpClient);
} }
@Override
@NonNull
public String getUserAgent() {
return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
}
@VisibleForTesting @VisibleForTesting
public void setWikiMediaToolforgeUrl(String wikiMediaToolforgeUrl) { public void setWikiMediaToolforgeUrl(String wikiMediaToolforgeUrl) {
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
@ -86,11 +92,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
* @throws IOException On api request IO issue * @throws IOException On api request IO issue
*/ */
public String login(String username, String password) throws IOException { public String login(String username, String password) throws IOException {
String loginToken = getLoginToken();
Timber.d("Login token is %s", loginToken);
return getErrorCodeToReturn(api.action("clientlogin") return getErrorCodeToReturn(api.action("clientlogin")
.param("rememberMe", "1") .param("rememberMe", "1")
.param("username", username) .param("username", username)
.param("password", password) .param("password", password)
.param("logintoken", getLoginToken()) .param("logintoken", loginToken)
.param("loginreturnurl", "https://commons.wikimedia.org") .param("loginreturnurl", "https://commons.wikimedia.org")
.post()); .post());
} }
@ -103,12 +111,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
* @throws IOException On api request IO issue * @throws IOException On api request IO issue
*/ */
public String login(String username, String password, String twoFactorCode) throws IOException { public String login(String username, String password, String twoFactorCode) throws IOException {
String loginToken = getLoginToken();
Timber.d("Login token is %s", loginToken);
return getErrorCodeToReturn(api.action("clientlogin") return getErrorCodeToReturn(api.action("clientlogin")
.param("rememberMe", "1") .param("rememberMe", "true")
.param("username", username) .param("username", username)
.param("password", password) .param("password", password)
.param("logintoken", getLoginToken()) .param("logintoken", loginToken)
.param("logincontinue", "1") .param("logincontinue", "true")
.param("OATHToken", twoFactorCode) .param("OATHToken", twoFactorCode)
.post()); .post());
} }

View file

@ -12,6 +12,8 @@ import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
public interface MediaWikiApi { public interface MediaWikiApi {
String getUserAgent();
String getAuthCookie(); String getAuthCookie();
void setAuthCookie(String authCookie); void setAuthCookie(String authCookie);