Skip to content

Commit 6bb7de8

Browse files
committed
Add 2fa to login activity
Fixes #328
1 parent 81d23a3 commit 6bb7de8

File tree

5 files changed

+104
-27
lines changed

5 files changed

+104
-27
lines changed

app/src/main/java/fr/free/nrw/commons/MWApi.java

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,79 @@
1010
*/
1111
public class MWApi extends org.mediawiki.api.MWApi {
1212

13+
/** We don't actually use this but need to pass it in requests */
14+
private static String LOGIN_RETURN_TO_URL = "https://commons.wikimedia.org";
15+
1316
public MWApi(String apiURL, AbstractHttpClient client) {
1417
super(apiURL, client);
1518
}
1619

1720
/**
1821
* @param username String
1922
* @param password String
20-
* @return String On success: "PASS"
21-
* continue: "2FA" (More information required for 2FA)
22-
* failure: A failure message code (defined by mediawiki)
23-
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
23+
* @return String as returned by this.getErrorCodeToReturn()
2424
* @throws IOException On api request IO issue
2525
*/
2626
public String login(String username, String password) throws IOException {
27-
28-
/** Request a login token to be used later to log in. */
29-
ApiResult tokenData = this.action("query")
30-
.param("action", "query")
31-
.param("meta", "tokens")
32-
.param("type", "login")
27+
String token = this.getLoginToken();
28+
ApiResult loginApiResult = this.action("clientlogin")
29+
.param("rememberMe", "1")
30+
.param("username", username)
31+
.param("password", password)
32+
.param("logintoken", token)
33+
.param("loginreturnurl", LOGIN_RETURN_TO_URL)
3334
.post();
34-
String token = tokenData.getString("/api/query/tokens/@logintoken");
35+
return this.getErrorCodeToReturn( loginApiResult );
36+
}
3537

36-
/** Actually log in. */
37-
ApiResult loginData = this.action("clientlogin")
38+
/**
39+
* @param username String
40+
* @param password String
41+
* @param twoFactorCode String
42+
* @return String as returned by this.getErrorCodeToReturn()
43+
* @throws IOException On api request IO issue
44+
*/
45+
public String login(String username, String password, String twoFactorCode) throws IOException {
46+
String token = this.getLoginToken();//TODO cache this instead of calling again when 2FAing
47+
ApiResult loginApiResult = this.action("clientlogin")
3848
.param("rememberMe", "1")
3949
.param("username", username)
4050
.param("password", password)
4151
.param("logintoken", token)
42-
.param("loginreturnurl", "http://example.com/")//TODO return to url?
52+
.param("logincontinue", "1")
53+
.param("OATHToken", twoFactorCode)
4354
.post();
44-
String status = loginData.getString("/api/clientlogin/@status");
4555

56+
return this.getErrorCodeToReturn( loginApiResult );
57+
}
58+
59+
private String getLoginToken() throws IOException {
60+
ApiResult tokenResult = this.action("query")
61+
.param("action", "query")
62+
.param("meta", "tokens")
63+
.param("type", "login")
64+
.post();
65+
return tokenResult.getString("/api/query/tokens/@logintoken");
66+
}
67+
68+
/**
69+
* @param loginApiResult ApiResult Any clientlogin api result
70+
* @return String On success: "PASS"
71+
* continue: "2FA" (More information required for 2FA)
72+
* failure: A failure message code (defined by mediawiki)
73+
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
74+
*/
75+
private String getErrorCodeToReturn( ApiResult loginApiResult ) {
76+
String status = loginApiResult.getString("/api/clientlogin/@status");
4677
if (status.equals("PASS")) {
4778
this.isLoggedIn = true;
4879
return status;
4980
} else if (status.equals("FAIL")) {
50-
return loginData.getString("/api/clientlogin/@messagecode");
81+
return loginApiResult.getString("/api/clientlogin/@messagecode");
5182
} else if (
5283
status.equals("UI")
53-
&& loginData.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
54-
&& loginData.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
84+
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
85+
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
5586
) {
5687
return "2FA";
5788
}
@@ -60,5 +91,4 @@ public String login(String username, String password) throws IOException {
6091
return "genericerror-" + status;
6192
}
6293

63-
6494
}

app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ public class LoginActivity extends AccountAuthenticatorActivity {
4848
Button signupButton;
4949
EditText usernameEdit;
5050
EditText passwordEdit;
51+
EditText twoFactorEdit;
5152
ProgressDialog dialog;
5253

5354
private class LoginTask extends AsyncTask<String, String, String> {
5455

5556
Activity context;
5657
String username;
5758
String password;
59+
String twoFactorCode = "";
5860

5961
@Override
6062
protected void onPostExecute(String result) {
@@ -107,20 +109,20 @@ protected void onPostExecute(String result) {
107109
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
108110
// Matches nosuchuser, nosuchusershort, noname
109111
response = R.string.login_failed_username;
110-
passwordEdit.setText("");
111-
112+
emptySensitiveEditFields();
112113
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
113114
// Matches wrongpassword, wrongpasswordempty
114115
response = R.string.login_failed_password;
115-
passwordEdit.setText("");
116+
emptySensitiveEditFields();
116117
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
117118
// Matches unknown throttle error codes
118119
response = R.string.login_failed_throttled;
119120
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
120121
// Matches login-userblocked
121122
response = R.string.login_failed_blocked;
122123
} else if (result.equals("2FA")){
123-
response = R.string.login_failed_2fa_not_supported;
124+
twoFactorEdit.setVisibility(View.VISIBLE);
125+
response = R.string.login_failed_2fa_needed;
124126
} else {
125127
// Occurs with unhandled login failure codes
126128
Timber.d("Login failed with reason: %s", result);
@@ -131,6 +133,11 @@ protected void onPostExecute(String result) {
131133
}
132134
}
133135

136+
private void emptySensitiveEditFields() {
137+
passwordEdit.setText("");
138+
twoFactorEdit.setText("");
139+
}
140+
134141
@Override
135142
protected void onPreExecute() {
136143
super.onPreExecute();
@@ -150,8 +157,16 @@ protected void onPreExecute() {
150157
protected String doInBackground(String... params) {
151158
username = params[0];
152159
password = params[1];
160+
if(params.length > 2) {
161+
twoFactorCode = params[2];
162+
}
163+
153164
try {
154-
return app.getApi().login(username, password);
165+
if(twoFactorCode.isEmpty()) {
166+
return app.getApi().login(username, password);
167+
} else {
168+
return app.getApi().login(username, password, twoFactorCode);
169+
}
155170
} catch (IOException e) {
156171
// Do something better!
157172
return "NetworkFailure";
@@ -168,6 +183,7 @@ public void onCreate(Bundle savedInstanceState) {
168183
signupButton = (Button) findViewById(R.id.signupButton);
169184
usernameEdit = (EditText) findViewById(R.id.loginUsername);
170185
passwordEdit = (EditText) findViewById(R.id.loginPassword);
186+
twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor);
171187
final LoginActivity that = this;
172188

173189
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
@@ -181,7 +197,11 @@ public void onTextChanged(CharSequence charSequence, int start, int count, int a
181197

182198
@Override
183199
public void afterTextChanged(Editable editable) {
184-
if(usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0) {
200+
if(
201+
usernameEdit.getText().length() != 0 &&
202+
passwordEdit.getText().length() != 0 &&
203+
( twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE )
204+
) {
185205
loginButton.setEnabled(true);
186206
} else {
187207
loginButton.setEnabled(false);
@@ -191,6 +211,7 @@ public void afterTextChanged(Editable editable) {
191211

192212
usernameEdit.addTextChangedListener(loginEnabler);
193213
passwordEdit.addTextChangedListener(loginEnabler);
214+
twoFactorEdit.addTextChangedListener(loginEnabler);
194215
passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
195216
@Override
196217
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
@@ -249,9 +270,15 @@ private void performLogin() {
249270

250271
String password = passwordEdit.getText().toString();
251272

273+
String twoFactorCode = twoFactorEdit.getText().toString();
274+
252275
Timber.d("Login to start!");
253276
LoginTask task = new LoginTask(this);
254-
task.execute(canonicalUsername, password);
277+
if(twoFactorCode.isEmpty()) {
278+
task.execute(canonicalUsername, password);
279+
} else {
280+
task.execute(canonicalUsername, password, twoFactorCode);
281+
}
255282
}
256283

257284
@Override

app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public Bundle editProperties(AccountAuthenticatorResponse response, String accou
4444

4545
private String getAuthCookie(String username, String password) throws IOException {
4646
MWApi api = CommonsApplication.createMWApi();
47+
//TODO add 2fa support here
4748
String result = api.login(username, password);
4849
if(result.equals("PASS")) {
4950
return api.getAuthCookie();

app/src/main/res/layout/activity_login.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@
6262

6363
</android.support.design.widget.TextInputLayout>
6464

65+
<android.support.design.widget.TextInputLayout
66+
android:layout_width="match_parent"
67+
android:layout_height="wrap_content"
68+
app:passwordToggleEnabled="false"
69+
>
70+
71+
<android.support.design.widget.TextInputEditText
72+
android:id="@+id/loginTwoFactor"
73+
android:layout_width="match_parent"
74+
android:layout_height="wrap_content"
75+
android:hint="@string/_2fa_code"
76+
android:imeOptions="flagNoExtractUi"
77+
android:inputType="textNoSuggestions"
78+
android:visibility="gone"
79+
/>
80+
81+
</android.support.design.widget.TextInputLayout>
82+
6583
<Button
6684
android:id="@+id/loginButton"
6785
android:layout_width="match_parent"

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<string name="login_failed_password">Unable to login - please check your password</string>
4343
<string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string>
4444
<string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string>
45-
<string name="login_failed_2fa_not_supported">The app doesn\'t currently support 2 Factor Authentication.</string>
45+
<string name="login_failed_2fa_needed">You must provide your two factor authentication code.</string>
4646
<string name="login_failed_generic">Login failed</string>
4747
<string name="share_upload_button">Upload</string>
4848
<string name="multiple_share_base_title">Name this set</string>
@@ -171,4 +171,5 @@ Tap this message (or hit back) to skip this step.</string>
171171
<string name="use_wikidata">Use Wikidata</string>
172172
<string name="use_wikidata_summary">(Warning: disabling this may cause large mobile data consumption)</string>
173173
<string name="mapbox_commons_app_token">pk.eyJ1IjoibWFza2FyYXZpdmVrIiwiYSI6ImNqMmxvdzFjMTAwMHYzM283ZWM3eW5tcDAifQ.ib5SZ9EVjwJe6GSKve0bcg</string>
174+
<string name="_2fa_code">2FA Code</string>
174175
</resources>

0 commit comments

Comments
 (0)