From ea051c119ab072057a3ec4edb2e98d6728f7643b Mon Sep 17 00:00:00 2001 From: addshore Date: Sat, 13 May 2017 17:35:33 +0200 Subject: [PATCH] Add 2fa to login activity Fixes #328 --- .../main/java/fr/free/nrw/commons/MWApi.java | 72 +++++++++++++------ .../free/nrw/commons/auth/LoginActivity.java | 41 +++++++++-- .../auth/WikiAccountAuthenticator.java | 1 + app/src/main/res/layout/activity_login.xml | 18 +++++ app/src/main/res/values/strings.xml | 3 +- 5 files changed, 106 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/MWApi.java b/app/src/main/java/fr/free/nrw/commons/MWApi.java index aef8b5cd0..785ea7886 100644 --- a/app/src/main/java/fr/free/nrw/commons/MWApi.java +++ b/app/src/main/java/fr/free/nrw/commons/MWApi.java @@ -10,6 +10,9 @@ import org.mediawiki.api.ApiResult; */ public class MWApi extends org.mediawiki.api.MWApi { + /** We don't actually use this but need to pass it in requests */ + private static String LOGIN_RETURN_TO_URL = "https://commons.wikimedia.org"; + public MWApi(String apiURL, AbstractHttpClient client) { super(apiURL, client); } @@ -17,41 +20,69 @@ public class MWApi extends org.mediawiki.api.MWApi { /** * @param username String * @param password String - * @return String On success: "PASS" - * continue: "2FA" (More information required for 2FA) - * failure: A failure message code (defined by mediawiki) - * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART + * @return String as returned by this.getErrorCodeToReturn() * @throws IOException On api request IO issue */ public String login(String username, String password) throws IOException { - - /** Request a login token to be used later to log in. */ - ApiResult tokenData = this.action("query") - .param("action", "query") - .param("meta", "tokens") - .param("type", "login") - .post(); - String token = tokenData.getString("/api/query/tokens/@logintoken"); - - /** Actually log in. */ - ApiResult loginData = this.action("clientlogin") + String token = this.getLoginToken(); + ApiResult loginApiResult = this.action("clientlogin") .param("rememberMe", "1") .param("username", username) .param("password", password) .param("logintoken", token) - .param("loginreturnurl", "http://example.com/")//TODO return to url? + .param("loginreturnurl", LOGIN_RETURN_TO_URL) .post(); - String status = loginData.getString("/api/clientlogin/@status"); + return this.getErrorCodeToReturn( loginApiResult ); + } + /** + * @param username String + * @param password String + * @param twoFactorCode String + * @return String as returned by this.getErrorCodeToReturn() + * @throws IOException On api request IO issue + */ + public String login(String username, String password, String twoFactorCode) throws IOException { + String token = this.getLoginToken();//TODO cache this instead of calling again when 2FAing + ApiResult loginApiResult = this.action("clientlogin") + .param("rememberMe", "1") + .param("username", username) + .param("password", password) + .param("logintoken", token) + .param("logincontinue", "1") + .param("OATHToken", twoFactorCode) + .post(); + + return this.getErrorCodeToReturn( loginApiResult ); + } + + private String getLoginToken() throws IOException { + ApiResult tokenResult = this.action("query") + .param("action", "query") + .param("meta", "tokens") + .param("type", "login") + .post(); + return tokenResult.getString("/api/query/tokens/@logintoken"); + } + + /** + * @param loginApiResult ApiResult Any clientlogin api result + * @return String On success: "PASS" + * continue: "2FA" (More information required for 2FA) + * failure: A failure message code (defined by mediawiki) + * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART + */ + private String getErrorCodeToReturn( ApiResult loginApiResult ) { + String status = loginApiResult.getString("/api/clientlogin/@status"); if (status.equals("PASS")) { this.isLoggedIn = true; return status; } else if (status.equals("FAIL")) { - return loginData.getString("/api/clientlogin/@messagecode"); + return loginApiResult.getString("/api/clientlogin/@messagecode"); } else if ( status.equals("UI") - && loginData.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") - && loginData.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") + && loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") + && loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") ) { return "2FA"; } @@ -60,5 +91,4 @@ public class MWApi extends org.mediawiki.api.MWApi { return "genericerror-" + status; } - } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 6c3f6a389..57a369bad 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -48,6 +48,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { Button signupButton; EditText usernameEdit; EditText passwordEdit; + EditText twoFactorEdit; ProgressDialog dialog; private class LoginTask extends AsyncTask { @@ -55,6 +56,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { Activity context; String username; String password; + String twoFactorCode = ""; @Override protected void onPostExecute(String result) { @@ -107,12 +109,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { // Matches nosuchuser, nosuchusershort, noname response = R.string.login_failed_username; - passwordEdit.setText(""); - + emptySensitiveEditFields(); } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { // Matches wrongpassword, wrongpasswordempty response = R.string.login_failed_password; - passwordEdit.setText(""); + emptySensitiveEditFields(); } else if (result.toLowerCase().contains("throttle".toLowerCase())) { // Matches unknown throttle error codes response = R.string.login_failed_throttled; @@ -120,7 +121,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { // Matches login-userblocked response = R.string.login_failed_blocked; } else if (result.equals("2FA")){ - response = R.string.login_failed_2fa_not_supported; + twoFactorEdit.setVisibility(View.VISIBLE); + response = R.string.login_failed_2fa_needed; } else { // Occurs with unhandled login failure codes Timber.d("Login failed with reason: %s", result); @@ -131,6 +133,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { } } + private void emptySensitiveEditFields() { + passwordEdit.setText(""); + twoFactorEdit.setText(""); + } + @Override protected void onPreExecute() { super.onPreExecute(); @@ -150,8 +157,16 @@ public class LoginActivity extends AccountAuthenticatorActivity { protected String doInBackground(String... params) { username = params[0]; password = params[1]; + if(params.length > 2) { + twoFactorCode = params[2]; + } + try { - return app.getApi().login(username, password); + if(twoFactorCode.isEmpty()) { + return app.getApi().login(username, password); + } else { + return app.getApi().login(username, password, twoFactorCode); + } } catch (IOException e) { // Do something better! return "NetworkFailure"; @@ -168,6 +183,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { signupButton = (Button) findViewById(R.id.signupButton); usernameEdit = (EditText) findViewById(R.id.loginUsername); passwordEdit = (EditText) findViewById(R.id.loginPassword); + twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor); final LoginActivity that = this; prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); @@ -181,7 +197,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { @Override public void afterTextChanged(Editable editable) { - if(usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0) { + if( + usernameEdit.getText().length() != 0 && + passwordEdit.getText().length() != 0 && + ( twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE ) + ) { loginButton.setEnabled(true); } else { loginButton.setEnabled(false); @@ -191,6 +211,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { usernameEdit.addTextChangedListener(loginEnabler); passwordEdit.addTextChangedListener(loginEnabler); + twoFactorEdit.addTextChangedListener(loginEnabler); passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { @@ -249,9 +270,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { String password = passwordEdit.getText().toString(); + String twoFactorCode = twoFactorEdit.getText().toString(); + Timber.d("Login to start!"); LoginTask task = new LoginTask(this); - task.execute(canonicalUsername, password); + if(twoFactorCode.isEmpty()) { + task.execute(canonicalUsername, password); + } else { + task.execute(canonicalUsername, password, twoFactorCode); + } } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index 42448ace0..c134af4da 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -44,6 +44,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { private String getAuthCookie(String username, String password) throws IOException { MWApi api = CommonsApplication.createMWApi(); + //TODO add 2fa support here String result = api.login(username, password); if(result.equals("PASS")) { return api.getAuthCookie(); diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index aeda37b39..c81f78a33 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -62,6 +62,24 @@ + + + + + +