Add 2fa to login activity

Fixes #328
This commit is contained in:
addshore 2017-05-13 17:35:33 +02:00
parent 1d0dad9e1a
commit ea051c119a
5 changed files with 106 additions and 29 deletions

View file

@ -10,6 +10,9 @@ import org.mediawiki.api.ApiResult;
*/ */
public class MWApi extends org.mediawiki.api.MWApi { 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) { public MWApi(String apiURL, AbstractHttpClient client) {
super(apiURL, client); super(apiURL, client);
} }
@ -17,41 +20,69 @@ public class MWApi extends org.mediawiki.api.MWApi {
/** /**
* @param username String * @param username String
* @param password String * @param password String
* @return String On success: "PASS" * @return String as returned by this.getErrorCodeToReturn()
* continue: "2FA" (More information required for 2FA)
* failure: A failure message code (defined by mediawiki)
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
* @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 token = this.getLoginToken();
/** Request a login token to be used later to log in. */ ApiResult loginApiResult = this.action("clientlogin")
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")
.param("rememberMe", "1") .param("rememberMe", "1")
.param("username", username) .param("username", username)
.param("password", password) .param("password", password)
.param("logintoken", token) .param("logintoken", token)
.param("loginreturnurl", "http://example.com/")//TODO return to url? .param("loginreturnurl", LOGIN_RETURN_TO_URL)
.post(); .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")) { if (status.equals("PASS")) {
this.isLoggedIn = true; this.isLoggedIn = true;
return status; return status;
} else if (status.equals("FAIL")) { } else if (status.equals("FAIL")) {
return loginData.getString("/api/clientlogin/@messagecode"); return loginApiResult.getString("/api/clientlogin/@messagecode");
} else if ( } else if (
status.equals("UI") status.equals("UI")
&& loginData.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") && loginApiResult.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/@provider").equals("Two-factor authentication (OATH).")
) { ) {
return "2FA"; return "2FA";
} }
@ -60,5 +91,4 @@ public class MWApi extends org.mediawiki.api.MWApi {
return "genericerror-" + status; return "genericerror-" + status;
} }
} }

View file

@ -48,6 +48,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
Button signupButton; Button signupButton;
EditText usernameEdit; EditText usernameEdit;
EditText passwordEdit; EditText passwordEdit;
EditText twoFactorEdit;
ProgressDialog dialog; ProgressDialog dialog;
private class LoginTask extends AsyncTask<String, String, String> { private class LoginTask extends AsyncTask<String, String, String> {
@ -55,6 +56,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
Activity context; Activity context;
String username; String username;
String password; String password;
String twoFactorCode = "";
@Override @Override
protected void onPostExecute(String result) { 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())) { } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname // Matches nosuchuser, nosuchusershort, noname
response = R.string.login_failed_username; response = R.string.login_failed_username;
passwordEdit.setText(""); emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty // Matches wrongpassword, wrongpasswordempty
response = R.string.login_failed_password; response = R.string.login_failed_password;
passwordEdit.setText(""); emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) { } else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes // Matches unknown throttle error codes
response = R.string.login_failed_throttled; response = R.string.login_failed_throttled;
@ -120,7 +121,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
// Matches login-userblocked // Matches login-userblocked
response = R.string.login_failed_blocked; response = R.string.login_failed_blocked;
} else if (result.equals("2FA")){ } 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 { } else {
// Occurs with unhandled login failure codes // Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result); 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 @Override
protected void onPreExecute() { protected void onPreExecute() {
super.onPreExecute(); super.onPreExecute();
@ -150,8 +157,16 @@ public class LoginActivity extends AccountAuthenticatorActivity {
protected String doInBackground(String... params) { protected String doInBackground(String... params) {
username = params[0]; username = params[0];
password = params[1]; password = params[1];
if(params.length > 2) {
twoFactorCode = params[2];
}
try { 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) { } catch (IOException e) {
// Do something better! // Do something better!
return "NetworkFailure"; return "NetworkFailure";
@ -168,6 +183,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
signupButton = (Button) findViewById(R.id.signupButton); signupButton = (Button) findViewById(R.id.signupButton);
usernameEdit = (EditText) findViewById(R.id.loginUsername); usernameEdit = (EditText) findViewById(R.id.loginUsername);
passwordEdit = (EditText) findViewById(R.id.loginPassword); passwordEdit = (EditText) findViewById(R.id.loginPassword);
twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor);
final LoginActivity that = this; final LoginActivity that = this;
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
@ -181,7 +197,11 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Override @Override
public void afterTextChanged(Editable editable) { 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); loginButton.setEnabled(true);
} else { } else {
loginButton.setEnabled(false); loginButton.setEnabled(false);
@ -191,6 +211,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.addTextChangedListener(loginEnabler); usernameEdit.addTextChangedListener(loginEnabler);
passwordEdit.addTextChangedListener(loginEnabler); passwordEdit.addTextChangedListener(loginEnabler);
twoFactorEdit.addTextChangedListener(loginEnabler);
passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
@ -249,9 +270,15 @@ public class LoginActivity extends AccountAuthenticatorActivity {
String password = passwordEdit.getText().toString(); String password = passwordEdit.getText().toString();
String twoFactorCode = twoFactorEdit.getText().toString();
Timber.d("Login to start!"); Timber.d("Login to start!");
LoginTask task = new LoginTask(this); LoginTask task = new LoginTask(this);
task.execute(canonicalUsername, password); if(twoFactorCode.isEmpty()) {
task.execute(canonicalUsername, password);
} else {
task.execute(canonicalUsername, password, twoFactorCode);
}
} }
@Override @Override

View file

@ -44,6 +44,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
private String getAuthCookie(String username, String password) throws IOException { private String getAuthCookie(String username, String password) throws IOException {
MWApi api = CommonsApplication.createMWApi(); MWApi api = CommonsApplication.createMWApi();
//TODO add 2fa support here
String result = api.login(username, password); String result = api.login(username, password);
if(result.equals("PASS")) { if(result.equals("PASS")) {
return api.getAuthCookie(); return api.getAuthCookie();

View file

@ -62,6 +62,24 @@
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="false"
>
<android.support.design.widget.TextInputEditText
android:id="@+id/loginTwoFactor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:inputType="textNoSuggestions"
android:visibility="gone"
/>
</android.support.design.widget.TextInputLayout>
<Button <Button
android:id="@+id/loginButton" android:id="@+id/loginButton"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -42,7 +42,7 @@
<string name="login_failed_password">Unable to login - please check your password</string> <string name="login_failed_password">Unable to login - please check your password</string>
<string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string> <string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string>
<string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string> <string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string>
<string name="login_failed_2fa_not_supported">The app doesn\'t currently support 2 Factor Authentication.</string> <string name="login_failed_2fa_needed">You must provide your two factor authentication code.</string>
<string name="login_failed_generic">Login failed</string> <string name="login_failed_generic">Login failed</string>
<string name="share_upload_button">Upload</string> <string name="share_upload_button">Upload</string>
<string name="multiple_share_base_title">Name this set</string> <string name="multiple_share_base_title">Name this set</string>
@ -173,6 +173,7 @@ Tap this message (or hit back) to skip this step.</string>
<string name="map_theme_light">mapbox://styles/mapbox/traffic-day-v2</string> <string name="map_theme_light">mapbox://styles/mapbox/traffic-day-v2</string>
<string name="map_theme_dark">mapbox://styles/mapbox/traffic-night-v2</string> <string name="map_theme_dark">mapbox://styles/mapbox/traffic-night-v2</string>
<string name="mapbox_commons_app_token">pk.eyJ1IjoibWFza2FyYXZpdmVrIiwiYSI6ImNqMmxvdzFjMTAwMHYzM283ZWM3eW5tcDAifQ.ib5SZ9EVjwJe6GSKve0bcg</string> <string name="mapbox_commons_app_token">pk.eyJ1IjoibWFza2FyYXZpdmVrIiwiYSI6ImNqMmxvdzFjMTAwMHYzM283ZWM3eW5tcDAifQ.ib5SZ9EVjwJe6GSKve0bcg</string>
<string name="_2fa_code">2FA Code</string>
<string name="number_of_uploads">My Recent Upload Limit</string> <string name="number_of_uploads">My Recent Upload Limit</string>
<string name="maximum_limit">Maximum Limit</string> <string name="maximum_limit">Maximum Limit</string>
<string name="maximum_limit_alert">Maximum limit should be 500</string> <string name="maximum_limit_alert">Maximum limit should be 500</string>