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 @@
+
+
+
+
+
+
Too many unsuccessful attempts. Please try again in a few minutes.
Sorry, this user has been blocked on Commons
- The app doesn\'t currently support 2 Factor Authentication.
+ You must provide your two factor authentication code.
Login failed
Upload
Name this set
@@ -173,6 +173,7 @@ Tap this message (or hit back) to skip this step.
mapbox://styles/mapbox/traffic-day-v2
mapbox://styles/mapbox/traffic-night-v2
pk.eyJ1IjoibWFza2FyYXZpdmVrIiwiYSI6ImNqMmxvdzFjMTAwMHYzM283ZWM3eW5tcDAifQ.ib5SZ9EVjwJe6GSKve0bcg
+ 2FA Code
My Recent Upload Limit
Maximum Limit
Maximum limit should be 500