jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/322366 )

Change subject: Add basic 2FA support
......................................................................


Add basic 2FA support

Can be modified to be a "one-step" rather than "two-step" process for the
user if desired.

Very basic but this gets us from zero to something for those users who
need it.

Bug: T150900
Change-Id: I89f339ec8d58813815ea6791bf8930b83ff7f400
---
M app/src/androidTest/java/org/wikipedia/login/LoginClientTest.java
M app/src/main/java/org/wikipedia/descriptions/DescriptionEditFragment.java
M app/src/main/java/org/wikipedia/edit/EditSectionActivity.java
M app/src/main/java/org/wikipedia/login/LoginActivity.java
M app/src/main/java/org/wikipedia/login/LoginClient.java
A app/src/main/java/org/wikipedia/login/LoginOAuthResult.java
M app/src/main/java/org/wikipedia/login/LoginResult.java
M app/src/main/res/layout/activity_wiki_login.xml
M app/src/main/res/values-qq/strings.xml
M app/src/main/res/values/strings.xml
10 files changed, 137 insertions(+), 11 deletions(-)

Approvals:
  Niedzielski: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/app/src/androidTest/java/org/wikipedia/login/LoginClientTest.java 
b/app/src/androidTest/java/org/wikipedia/login/LoginClientTest.java
index 13cf624..74cc099 100644
--- a/app/src/androidTest/java/org/wikipedia/login/LoginClientTest.java
+++ b/app/src/androidTest/java/org/wikipedia/login/LoginClientTest.java
@@ -1,6 +1,7 @@
 package org.wikipedia.login;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
 import android.support.test.filters.SmallTest;
 
@@ -15,6 +16,7 @@
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
 import static org.wikipedia.test.TestUtil.runOnMainSync;
 
 @SmallTest public class LoginClientTest {
@@ -43,6 +45,11 @@
                             }
 
                             @Override
+                            public void twoFactorPrompt(@NonNull Throwable 
throwble, @Nullable String token) {
+                                fail("Two-factor prompt not expected here");
+                            }
+
+                            @Override
                             public void error(@NonNull Throwable caught) {
                                 assertThat("login failed!", false);
                             }
diff --git 
a/app/src/main/java/org/wikipedia/descriptions/DescriptionEditFragment.java 
b/app/src/main/java/org/wikipedia/descriptions/DescriptionEditFragment.java
index cc3586a..d67c2d3 100644
--- a/app/src/main/java/org/wikipedia/descriptions/DescriptionEditFragment.java
+++ b/app/src/main/java/org/wikipedia/descriptions/DescriptionEditFragment.java
@@ -213,6 +213,12 @@
                         }
 
                         @Override
+                        public void twoFactorPrompt(@NonNull Throwable caught, 
@Nullable String token) {
+                            editFailed(new LoginFailedException(getResources()
+                                            
.getString(R.string.login_2fa_other_workflow_error_msg)));
+                        }
+
+                        @Override
                         public void error(@NonNull Throwable caught) {
                             editFailed(caught);
                         }
diff --git a/app/src/main/java/org/wikipedia/edit/EditSectionActivity.java 
b/app/src/main/java/org/wikipedia/edit/EditSectionActivity.java
index cf644a5..c18d37a 100644
--- a/app/src/main/java/org/wikipedia/edit/EditSectionActivity.java
+++ b/app/src/main/java/org/wikipedia/edit/EditSectionActivity.java
@@ -471,6 +471,14 @@
                     }
 
                     @Override
+                    public void twoFactorPrompt(@NonNull Throwable caught, 
@Nullable String token) {
+                        FeedbackUtil.showError(EditSectionActivity.this,
+                                new 
LoginClient.LoginFailedException(getResources()
+                                        
.getString(R.string.login_2fa_other_workflow_error_msg)));
+                        onLoginError();
+                    }
+
+                    @Override
                     public void error(@NonNull Throwable caught) {
                         onLoginError();
                     }
diff --git a/app/src/main/java/org/wikipedia/login/LoginActivity.java 
b/app/src/main/java/org/wikipedia/login/LoginActivity.java
index 48e689f..4a4c26f 100644
--- a/app/src/main/java/org/wikipedia/login/LoginActivity.java
+++ b/app/src/main/java/org/wikipedia/login/LoginActivity.java
@@ -47,8 +47,10 @@
     @Pattern(regex = "[^#<>\\[\\]|{}\\/@]*", messageResId = 
R.string.create_account_username_error)
     private EditText usernameText;
     private EditText passwordText;
+    private EditText twoFactorText;
     private View loginButton;
     private ProgressDialog progressDialog;
+    @Nullable private String firstStepToken;
 
     private LoginFunnel funnel;
     private String loginSource;
@@ -74,6 +76,7 @@
 
         usernameText = (EditText) findViewById(R.id.login_username_text);
         passwordText = ((PasswordTextInput) 
findViewById(R.id.login_password_input)).getEditText();
+        twoFactorText = (EditText) findViewById(R.id.login_2fa_text);
         View createAccountLink = findViewById(R.id.login_create_account_link);
 
         // We enable the login button as soon as the username and password 
fields are filled
@@ -206,13 +209,25 @@
     private void doLogin() {
         final String username = usernameText.getText().toString();
         final String password = passwordText.getText().toString();
+        final String twoFactorCode = twoFactorText.getText().toString();
 
         if (loginClient == null) {
             loginClient = new LoginClient();
         }
         progressDialog.show();
-        loginClient.request(WikipediaApp.getInstance().getWikiSite(), 
username, password,
-                new LoginClient.LoginCallback() {
+
+        if (!twoFactorCode.isEmpty()) {
+            loginClient.login(WikipediaApp.getInstance().getWikiSite(), 
username, password,
+                    twoFactorCode, firstStepToken, getCallback(username, 
password));
+        } else {
+            loginClient.request(WikipediaApp.getInstance().getWikiSite(), 
username, password,
+                    getCallback(username, password));
+        }
+    }
+
+    private LoginClient.LoginCallback getCallback(@NonNull final String 
username,
+                                                  @NonNull final String 
password) {
+        return new LoginClient.LoginCallback() {
             @Override
             public void success(@NonNull LoginResult result) {
                 if (!progressDialog.isShowing()) {
@@ -240,6 +255,18 @@
             }
 
             @Override
+            public void twoFactorPrompt(@NonNull Throwable caught, @NonNull 
String token) {
+                if (!progressDialog.isShowing()) {
+                    // no longer attached to activity!
+                    return;
+                }
+                progressDialog.dismiss();
+                firstStepToken = token;
+                twoFactorText.setVisibility(View.VISIBLE);
+                FeedbackUtil.showError(LoginActivity.this, caught);
+            }
+
+            @Override
             public void error(@NonNull Throwable caught) {
                 if (!progressDialog.isShowing()) {
                     // no longer attached to activity!
@@ -248,7 +275,7 @@
                 progressDialog.dismiss();
                 FeedbackUtil.showError(LoginActivity.this, caught);
             }
-        });
+        };
     }
 
     @Override
diff --git a/app/src/main/java/org/wikipedia/login/LoginClient.java 
b/app/src/main/java/org/wikipedia/login/LoginClient.java
index 07c1d4f..c9d7e26 100644
--- a/app/src/main/java/org/wikipedia/login/LoginClient.java
+++ b/app/src/main/java/org/wikipedia/login/LoginClient.java
@@ -5,6 +5,7 @@
 
 import com.google.gson.annotations.SerializedName;
 
+import org.apache.commons.lang3.StringUtils;
 import org.wikipedia.Constants;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.dataclient.WikiSite;
@@ -14,6 +15,8 @@
 import org.wikipedia.util.log.L;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import retrofit2.Call;
@@ -35,6 +38,7 @@
 
     public interface LoginCallback {
         void success(@NonNull LoginResult result);
+        void twoFactorPrompt(@NonNull Throwable caught, @Nullable String 
token);
         void error(@NonNull Throwable caught);
     }
 
@@ -54,7 +58,7 @@
                     MwQueryResponse<LoginToken> body = response.body();
                     LoginToken query = body.query();
                     if (query != null &&  query.getLoginToken() != null) {
-                        login(wiki, userName, password, query.getLoginToken(), 
cb);
+                        login(wiki, userName, password, null, 
query.getLoginToken(), cb);
                     } else if (body.getError() != null) {
                         cb.error(new IOException("Failed to retrieve login 
token. "
                                 + body.getError().toString()));
@@ -74,10 +78,12 @@
         });
     }
 
-    private void login(@NonNull final WikiSite wiki, @NonNull final String 
userName,
-                       @NonNull final String password, @NonNull String 
loginToken,
-                       @NonNull final LoginCallback cb) {
-        loginCall = cachedService.service(wiki).logIn(userName, password, 
loginToken, Constants.WIKIPEDIA_URL);
+    void login(@NonNull final WikiSite wiki, @NonNull final String userName,
+                       @NonNull final String password, @Nullable final String 
twoFactorCode,
+                       @Nullable final String loginToken, @NonNull final 
LoginCallback cb) {
+        loginCall = StringUtils.defaultIfEmpty(twoFactorCode, "").isEmpty()
+                ? cachedService.service(wiki).logIn(userName, password, 
loginToken, Constants.WIKIPEDIA_URL)
+                : cachedService.service(wiki).logIn(userName, password, 
twoFactorCode, loginToken, true);
         loginCall.enqueue(new Callback<LoginResponse>() {
             @Override
             public void onResponse(Call<LoginResponse> call, 
Response<LoginResponse> response) {
@@ -90,6 +96,9 @@
                             // wikis is uppercases the first letter.
                             String actualUserName = 
loginResult.getUser().getUsername();
                             getGroupMemberships(wiki, actualUserName, 
loginResult, cb);
+                        } else if ("UI".equals(loginResult.getStatus())) {
+                            //TODO: Don't just assume this is a 2FA UI result
+                            cb.twoFactorPrompt(new 
LoginFailedException(loginResult.getMessage()), loginToken);
                         } else {
                             cb.error(new 
LoginFailedException(loginResult.getMessage()));
                         }
@@ -163,8 +172,15 @@
         @FormUrlEncoded
         @POST("w/api.php?action=clientlogin&format=json&rememberMe=true")
         Call<LoginResponse> logIn(@Field("username") String user, 
@Field("password") String pass,
-                                  @Field("logintoken") String token,
-                                  @Field("loginreturnurl") String url);
+                                  @Field("logintoken") String token, 
@Field("loginreturnurl") String url);
+
+        /** Actually log in. Has to be x-www-form-urlencoded */
+        @NonNull
+        @FormUrlEncoded
+        @POST("w/api.php?action=clientlogin&format=json&rememberMe=true")
+        Call<LoginResponse> logIn(@Field("username") String user, 
@Field("password") String pass,
+                                  @Field("OATHToken") String twoFactorCode, 
@Field("logintoken") String token,
+                                  @Field("logincontinue") boolean 
loginContinue);
     }
 
     private static final class LoginToken {
@@ -198,6 +214,7 @@
 
         private static class ClientLogin {
             @SerializedName("status") private String status;
+            @Nullable private List<Request> requests;
             @SerializedName("message") @Nullable private String message;
             @SerializedName("username") @Nullable private String userName;
 
@@ -208,10 +225,41 @@
                     user = new User(userName, password, 0);
                 } else if ("FAIL".equals(status)) {
                     userMessage = message;
+                } else if ("UI".equals(status)) {
+                    if (requests != null) {
+                        for (Request req : requests) {
+                            if ("TOTPAuthenticationRequest".equals(req.id())) {
+                                return new LoginOAuthResult(status, message);
+                            }
+                        }
+                    }
+                    userMessage = message;
+                } else {
+                    //TODO: String resource -- Looks like needed for others in 
this class too
+                    userMessage = "An unknown error occurred.";
                 }
                 return new LoginResult(status, user, userMessage);
             }
         }
+
+        private static class Request {
+            @SuppressWarnings("unused") @Nullable private String id;
+            //@SuppressWarnings("unused") @Nullable private JsonObject 
metadata;
+            @SuppressWarnings("unused") @Nullable private String required;
+            @SuppressWarnings("unused") @Nullable private String provider;
+            @SuppressWarnings("unused") @Nullable private String account;
+            @SuppressWarnings("unused") @Nullable private Map<String, 
RequestField> fields;
+
+            @Nullable String id() {
+                return id;
+            }
+        }
+
+        private static class RequestField {
+            @SuppressWarnings("unused") @Nullable private String type;
+            @SuppressWarnings("unused") @Nullable private String label;
+            @SuppressWarnings("unused") @Nullable private String help;
+        }
     }
 
     public static class LoginFailedException extends Throwable {
diff --git a/app/src/main/java/org/wikipedia/login/LoginOAuthResult.java 
b/app/src/main/java/org/wikipedia/login/LoginOAuthResult.java
new file mode 100644
index 0000000..5895748
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/login/LoginOAuthResult.java
@@ -0,0 +1,11 @@
+package org.wikipedia.login;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+class LoginOAuthResult extends LoginResult {
+
+    LoginOAuthResult(@NonNull String status, @Nullable String message) {
+        super(status, null, message);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/login/LoginResult.java 
b/app/src/main/java/org/wikipedia/login/LoginResult.java
index 990b79e..3814556 100644
--- a/app/src/main/java/org/wikipedia/login/LoginResult.java
+++ b/app/src/main/java/org/wikipedia/login/LoginResult.java
@@ -8,7 +8,7 @@
     @Nullable private final User user;
     @Nullable private final String message;
 
-    public LoginResult(@NonNull String status, @Nullable User user, @Nullable 
String message) {
+    LoginResult(@NonNull String status, @Nullable User user, @Nullable String 
message) {
         this.status = status;
         this.user = user;
         this.message = message;
diff --git a/app/src/main/res/layout/activity_wiki_login.xml 
b/app/src/main/res/layout/activity_wiki_login.xml
index 4c1ba82..4c26cbe 100644
--- a/app/src/main/res/layout/activity_wiki_login.xml
+++ b/app/src/main/res/layout/activity_wiki_login.xml
@@ -35,6 +35,21 @@
             app:passwordToggleEnabled="true"
             android:hint="@string/login_password_hint" />
 
+        <android.support.design.widget.TextInputLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/login_2fa_hint" >
+            <org.wikipedia.views.PlainPasteEditText
+                android:id="@+id/login_2fa_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                style="?android:textAppearanceMedium"
+                android:inputType="textVisiblePassword|textNoSuggestions"
+                android:imeOptions="flagNoExtractUi"
+                android:maxLines="1"
+                android:visibility="gone" />
+        </android.support.design.widget.TextInputLayout>
+
         <TextView
                 android:id="@+id/login_button"
                 style="@style/ButtonProgressive"
diff --git a/app/src/main/res/values-qq/strings.xml 
b/app/src/main/res/values-qq/strings.xml
index 301aee2..39c6280 100644
--- a/app/src/main/res/values-qq/strings.xml
+++ b/app/src/main/res/values-qq/strings.xml
@@ -105,6 +105,8 @@
   <string name="nav_item_login">{{Identical|Log in}}</string>
   <string name="login_username_hint">{{Identical|Username}}</string>
   <string name="login_password_hint">{{Identical|Password}}</string>
+  <string name="login_2fa_hint">Hint for a field where the user can enter a 
two-factor authentication code, if required.</string>
+  <string name="login_2fa_other_workflow_error_msg">Message for when login 
fails during an editing workflow because two-factor authentication is required. 
 Directs the user to return to the main activity, log in, and retry.</string>
   <string name="menu_login">{{Identical|Log in}}</string>
   <string name="login_activity_title">Used as window title.</string>
   <string name="login_in_progress_dialog_message">A bubble that describes the 
in-progress action.</string>
diff --git a/app/src/main/res/values/strings.xml 
b/app/src/main/res/values/strings.xml
index d1d5131..c79f351 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -81,6 +81,8 @@
     <string name="nav_item_login">Log in to Wikipedia</string>
     <string name="login_username_hint">Username</string>
     <string name="login_password_hint">Password</string>
+    <string name="login_2fa_hint">Two-Factor Authentication Code</string>
+    <string name="login_2fa_other_workflow_error_msg">Two-factor 
authentication required!  Please return to the main activity and log in with 
your 2FA token, then retry.</string>
     <string name="menu_login">Log in</string>
     <string name="login_activity_title">Log in to Wikipedia</string>
     <string name="login_in_progress_dialog_message">Logging you in…</string>

-- 
To view, visit https://gerrit.wikimedia.org/r/322366
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I89f339ec8d58813815ea6791bf8930b83ff7f400
Gerrit-PatchSet: 12
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Mholloway <mhollo...@wikimedia.org>
Gerrit-Reviewer: BearND <bsitzm...@wikimedia.org>
Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org>
Gerrit-Reviewer: Dbrant <dbr...@wikimedia.org>
Gerrit-Reviewer: Gergő Tisza <gti...@wikimedia.org>
Gerrit-Reviewer: Matanya <mata...@foss.co.il>
Gerrit-Reviewer: Mholloway <mhollo...@wikimedia.org>
Gerrit-Reviewer: Niedzielski <sniedziel...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to