mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-30 22:34:02 +01:00 
			
		
		
		
	Make 2FA login work with commons
This commit is contained in:
		
							parent
							
								
									022ac948a8
								
							
						
					
					
						commit
						1ea0a1ebeb
					
				
					 9 changed files with 271 additions and 209 deletions
				
			
		|  | @ -19,6 +19,8 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD | |||
| public class AccountUtil { | ||||
| 
 | ||||
|     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; | ||||
| 
 | ||||
|     public AccountUtil(Context context) { | ||||
|  |  | |||
|  | @ -1,72 +1,35 @@ | |||
| package fr.free.nrw.commons.auth; | ||||
| 
 | ||||
| import android.accounts.Account; | ||||
| import android.accounts.AccountManager; | ||||
| import android.accounts.AccountManagerFuture; | ||||
| import android.os.Bundle; | ||||
| 
 | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| import fr.free.nrw.commons.mwapi.MediaWikiApi; | ||||
| import fr.free.nrw.commons.theme.NavigationBaseActivity; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| 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.ACCOUNT_TYPE; | ||||
| import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE; | ||||
| 
 | ||||
| public abstract class AuthenticatedActivity extends NavigationBaseActivity { | ||||
| 
 | ||||
|     @Inject SessionManager sessionManager; | ||||
| 
 | ||||
|     @Inject | ||||
|     MediaWikiApi mediaWikiApi; | ||||
|     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() { | ||||
|         if (authCookie != null) { | ||||
|             onAuthCookieAcquired(authCookie); | ||||
|             return; | ||||
|         } | ||||
|         AccountManager accountManager = AccountManager.get(this); | ||||
|         Account curAccount = sessionManager.getCurrentAccount(); | ||||
|         if (curAccount == null) { | ||||
|             addAccount(accountManager); | ||||
|         } else { | ||||
|             getAuthCookie(curAccount, accountManager); | ||||
|         } | ||||
|         sessionManager.getAndSetAuthCookie() | ||||
|                 .subscribeOn(Schedulers.newThread()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(cookie -> { | ||||
|                     authCookie = cookie; | ||||
|                     onAuthCookieAcquired(authCookie); | ||||
|                 }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -74,14 +37,14 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { | |||
|         super.onCreate(savedInstanceState); | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             authCookie = savedInstanceState.getString("authCookie"); | ||||
|             authCookie = savedInstanceState.getString(AUTH_COOKIE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         outState.putString("authCookie", authCookie); | ||||
|         outState.putString(AUTH_COOKIE, authCookie); | ||||
|     } | ||||
| 
 | ||||
|     protected abstract void onAuthCookieAcquired(String authCookie); | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| package fr.free.nrw.commons.auth; | ||||
| 
 | ||||
| import android.accounts.Account; | ||||
| import android.accounts.AccountAuthenticatorActivity; | ||||
| import android.accounts.AccountAuthenticatorResponse; | ||||
| import android.accounts.AccountManager; | ||||
| import android.app.ProgressDialog; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
|  | @ -8,6 +11,7 @@ import android.os.Bundle; | |||
| import android.support.annotation.ColorRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.StringRes; | ||||
| import android.support.design.widget.TextInputLayout; | ||||
| import android.support.v4.app.NavUtils; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.support.v7.app.AppCompatDelegate; | ||||
|  | @ -21,6 +25,8 @@ import android.widget.Button; | |||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| import javax.inject.Inject; | ||||
| 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.mwapi.MediaWikiApi; | ||||
| 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 static android.view.KeyEvent.KEYCODE_ENTER; | ||||
| import static android.view.View.VISIBLE; | ||||
| 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 { | ||||
| 
 | ||||
|  | @ -58,6 +70,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|     @BindView(R.id.loginTwoFactor) EditText twoFactorEdit; | ||||
|     @BindView(R.id.error_message_container) ViewGroup errorMessageContainer; | ||||
|     @BindView(R.id.error_message) TextView errorMessage; | ||||
|     @BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer; | ||||
|     ProgressDialog progressDialog; | ||||
|     private AppCompatDelegate delegate; | ||||
|     private LoginTextWatcher textWatcher = new LoginTextWatcher(); | ||||
|  | @ -122,14 +135,109 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         super.onDestroy(); | ||||
|     } | ||||
| 
 | ||||
|     private LoginTask getLoginTask() { | ||||
|         return new LoginTask( | ||||
|                 this, | ||||
|                 canonicializeUsername(usernameEdit.getText().toString()), | ||||
|                 passwordEdit.getText().toString(), | ||||
|                 twoFactorEdit.getText().toString(), | ||||
|                 accountUtil, mwApi, defaultPrefs | ||||
|         ); | ||||
|     private void performLogin() { | ||||
|         Timber.d("Login to start!"); | ||||
|         final String username = canonicializeUsername(usernameEdit.getText().toString()); | ||||
|         final String password = passwordEdit.getText().toString(); | ||||
|         String twoFactorCode = twoFactorEdit.getText().toString(); | ||||
| 
 | ||||
|         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() { | ||||
|         if (BuildConfig.DEBUG) { | ||||
|             twoFactorEdit.setVisibility(View.VISIBLE); | ||||
|             twoFactorContainer.setVisibility(VISIBLE); | ||||
|             twoFactorEdit.setVisibility(VISIBLE); | ||||
|             showMessageAndCancelDialog(R.string.login_failed_2fa_needed); | ||||
|         } else { | ||||
|             showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported); | ||||
|  | @ -209,12 +318,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
|     private void performLogin() { | ||||
|         Timber.d("Login to start!"); | ||||
|         LoginTask task = getLoginTask(); | ||||
|         task.execute(); | ||||
|     } | ||||
| 
 | ||||
|     private void signUp() { | ||||
|         Intent intent = new Intent(this, SignupActivity.class); | ||||
|         startActivity(intent); | ||||
|  | @ -238,7 +341,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|     private void showMessage(@StringRes int resId, @ColorRes int colorResId) { | ||||
|         errorMessage.setText(getString(resId)); | ||||
|         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||
|         errorMessageContainer.setVisibility(View.VISIBLE); | ||||
|         errorMessageContainer.setVisibility(VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     private AppCompatDelegate getDelegate() { | ||||
|  | @ -260,7 +363,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         @Override | ||||
|         public void afterTextChanged(Editable editable) { | ||||
|             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); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -2,17 +2,16 @@ package fr.free.nrw.commons.auth; | |||
| 
 | ||||
| import android.accounts.Account; | ||||
| import android.accounts.AccountManager; | ||||
| import android.accounts.AuthenticatorException; | ||||
| import android.accounts.OperationCanceledException; | ||||
| import android.content.Context; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| import fr.free.nrw.commons.mwapi.MediaWikiApi; | ||||
| import io.reactivex.Completable; | ||||
| 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.AUTH_TOKEN_TYPE; | ||||
| 
 | ||||
| /** | ||||
|  * Manage the current logged in user session. | ||||
|  | @ -51,14 +50,31 @@ public class SessionManager { | |||
|         } | ||||
| 
 | ||||
|         accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); | ||||
|         try { | ||||
|             String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); | ||||
|             mediaWikiApi.setAuthCookie(authCookie); | ||||
|             return true; | ||||
|         } catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { | ||||
|             e.printStackTrace(); | ||||
|             return false; | ||||
|         } | ||||
|         getAndSetAuthCookie().subscribeOn(Schedulers.io()) | ||||
|                 .subscribe(authCookie -> { | ||||
|                     mediaWikiApi.setAuthCookie(authCookie); | ||||
|                 }); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     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() { | ||||
|  |  | |||
|  | @ -5,53 +5,37 @@ import android.accounts.Account; | |||
| import android.accounts.AccountAuthenticatorResponse; | ||||
| import android.accounts.AccountManager; | ||||
| import android.accounts.NetworkErrorException; | ||||
| import android.content.ContentResolver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| 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.LoginActivity.PARAM_USERNAME; | ||||
| import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; | ||||
| 
 | ||||
| public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { | ||||
|     private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.AUTHORITY, ModificationsContentProvider.AUTHORITY}; | ||||
| 
 | ||||
|     @NonNull | ||||
|     private final Context context; | ||||
|     private MediaWikiApi mediaWikiApi; | ||||
| 
 | ||||
|     WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) { | ||||
|     public WikiAccountAuthenticator(@NonNull Context context) { | ||||
|         super(context); | ||||
|         this.context = context; | ||||
|         this.mediaWikiApi = mwApi; | ||||
|     } | ||||
| 
 | ||||
|     private Bundle unsupportedOperation() { | ||||
|     @Override | ||||
|     public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { | ||||
|         Bundle bundle = new Bundle(); | ||||
|         bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION); | ||||
| 
 | ||||
|         // HACK: the docs indicate that this is a required key bit it's not displayed to the user. | ||||
|         bundle.putString(KEY_ERROR_MESSAGE, ""); | ||||
| 
 | ||||
|         bundle.putString("test", "editProperties"); | ||||
|         return bundle; | ||||
|     } | ||||
| 
 | ||||
|     private boolean supportedAccountType(@Nullable String type) { | ||||
|         return ACCOUNT_TYPE.equals(type); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, | ||||
|                              @NonNull String accountType, @Nullable String authTokenType, | ||||
|  | @ -59,86 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { | |||
|             throws NetworkErrorException { | ||||
| 
 | ||||
|         if (!supportedAccountType(accountType)) { | ||||
|             return unsupportedOperation(); | ||||
|             Bundle bundle = new Bundle(); | ||||
|             bundle.putString("test", "addAccount"); | ||||
|             return bundle; | ||||
|         } | ||||
| 
 | ||||
|         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 | ||||
|     public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, | ||||
|                                      @NonNull Account account, @Nullable Bundle options) | ||||
|             throws NetworkErrorException { | ||||
|         return unsupportedOperation(); | ||||
|         Bundle bundle = new Bundle(); | ||||
|         bundle.putString("test", "confirmCredentials"); | ||||
|         return bundle; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { | ||||
|         return unsupportedOperation(); | ||||
|     } | ||||
| 
 | ||||
|     private String getAuthCookie(String username, String password) throws IOException { | ||||
|         //TODO add 2fa support here | ||||
|         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); | ||||
|     public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, | ||||
|                                @NonNull Account account, @NonNull String authTokenType, | ||||
|                                @Nullable Bundle options) | ||||
|             throws NetworkErrorException { | ||||
|         Bundle bundle = new Bundle(); | ||||
|         bundle.putString("test", "getAuthToken"); | ||||
|         return bundle; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public String getAuthTokenLabel(@NonNull String authTokenType) { | ||||
|         //Note: the wikipedia app actually returns a string here.... | ||||
|         //return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null; | ||||
|         return null; | ||||
|         return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : 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 | ||||
|  | @ -147,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { | |||
|                               @NonNull Account account, @NonNull String[] features) | ||||
|             throws NetworkErrorException { | ||||
|         Bundle bundle = new Bundle(); | ||||
|         bundle.putBoolean(KEY_BOOLEAN_RESULT, false); | ||||
|         bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); | ||||
|         return bundle; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, | ||||
|                                     @NonNull Account account, @Nullable String authTokenType, | ||||
|                                     @Nullable Bundle options) throws NetworkErrorException { | ||||
|         return unsupportedOperation(); | ||||
|     private boolean supportedAccountType(@Nullable String type) { | ||||
|         return ACCOUNT_TYPE.equals(type); | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,30 +1,26 @@ | |||
| package fr.free.nrw.commons.auth; | ||||
| 
 | ||||
| import android.accounts.AbstractAccountAuthenticator; | ||||
| import android.content.Intent; | ||||
| import android.os.IBinder; | ||||
| 
 | ||||
| import javax.inject.Inject; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| 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 { | ||||
| 
 | ||||
|     @Inject MediaWikiApi mwApi; | ||||
|     private WikiAccountAuthenticator wikiAccountAuthenticator = null; | ||||
|     @Nullable | ||||
|     private AbstractAccountAuthenticator authenticator; | ||||
| 
 | ||||
|     @Override | ||||
|     public IBinder onBind(Intent intent) { | ||||
|         if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (wikiAccountAuthenticator == null) { | ||||
|             wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi); | ||||
|         } | ||||
|         return wikiAccountAuthenticator.getIBinder(); | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
|         authenticator = new WikiAccountAuthenticator(this); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public IBinder onBind(Intent intent) { | ||||
|         return authenticator == null ? null : authenticator.getIBinder(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,6 @@ | |||
| package fr.free.nrw.commons.modifications; | ||||
| 
 | ||||
| import android.accounts.Account; | ||||
| import android.accounts.AccountManager; | ||||
| import android.accounts.AuthenticatorException; | ||||
| import android.accounts.OperationCanceledException; | ||||
| import android.content.AbstractThreadedSyncAdapter; | ||||
| import android.content.ContentProviderClient; | ||||
| import android.content.Context; | ||||
|  | @ -16,7 +13,7 @@ import java.io.IOException; | |||
| 
 | ||||
| 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.ContributionDao; | ||||
| import fr.free.nrw.commons.contributions.ContributionsContentProvider; | ||||
|  | @ -29,6 +26,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { | |||
|     @Inject MediaWikiApi mwApi; | ||||
|     @Inject ContributionDao contributionDao; | ||||
|     @Inject ModifierSequenceDao modifierSequenceDao; | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     public ModificationsSyncAdapter(Context context, boolean autoInitialize) { | ||||
|         super(context, autoInitialize); | ||||
|  | @ -56,16 +55,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         String authCookie; | ||||
|         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; | ||||
|         } | ||||
| 
 | ||||
|         String authCookie = sessionManager.getAndSetAuthCookie().blockingSingle(); | ||||
|         if (isNullOrWhiteSpace(authCookie)) { | ||||
|             Timber.d("Could not authenticate :("); | ||||
|             return; | ||||
|  |  | |||
|  | @ -69,11 +69,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { | |||
|         final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); | ||||
|         schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); | ||||
|         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); | ||||
|         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 | ||||
|     public void setWikiMediaToolforgeUrl(String wikiMediaToolforgeUrl) { | ||||
|         this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; | ||||
|  | @ -86,11 +92,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { | |||
|      * @throws IOException On api request IO issue | ||||
|      */ | ||||
|     public String login(String username, String password) throws IOException { | ||||
|         String loginToken = getLoginToken(); | ||||
|         Timber.d("Login token is %s", loginToken); | ||||
|         return getErrorCodeToReturn(api.action("clientlogin") | ||||
|                 .param("rememberMe", "1") | ||||
|                 .param("username", username) | ||||
|                 .param("password", password) | ||||
|                 .param("logintoken", getLoginToken()) | ||||
|                 .param("logintoken", loginToken) | ||||
|                 .param("loginreturnurl", "https://commons.wikimedia.org") | ||||
|                 .post()); | ||||
|     } | ||||
|  | @ -103,12 +111,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { | |||
|      * @throws IOException On api request IO issue | ||||
|      */ | ||||
|     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") | ||||
|                 .param("rememberMe", "1") | ||||
|                 .param("rememberMe", "true") | ||||
|                 .param("username", username) | ||||
|                 .param("password", password) | ||||
|                 .param("logintoken", getLoginToken()) | ||||
|                 .param("logincontinue", "1") | ||||
|                 .param("logintoken", loginToken) | ||||
|                 .param("logincontinue", "true") | ||||
|                 .param("OATHToken", twoFactorCode) | ||||
|                 .post()); | ||||
|     } | ||||
|  |  | |||
|  | @ -12,6 +12,8 @@ import io.reactivex.Observable; | |||
| import io.reactivex.Single; | ||||
| 
 | ||||
| public interface MediaWikiApi { | ||||
|     String getUserAgent(); | ||||
| 
 | ||||
|     String getAuthCookie(); | ||||
| 
 | ||||
|     void setAuthCookie(String authCookie); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 maskara
						maskara