Skip to content

Commit d6ba3f9

Browse files
committed
Implement two-factor authentication support
Closes pockethub#432
1 parent 1487338 commit d6ba3f9

10 files changed

+626
-27
lines changed

app/AndroidManifest.xml

+10
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,16 @@
238238
someone who explicitly knows the class name
239239
-->
240240
</activity>
241+
<activity
242+
android:name=".accounts.TwoFactorAuthActivity"
243+
android:configChanges="orientation|keyboardHidden|screenSize"
244+
android:excludeFromRecents="true" >
245+
246+
<!--
247+
No intent-filter here! This activity is only ever launched by
248+
someone who explicitly knows the class name
249+
-->
250+
</activity>
241251
<activity
242252
android:name=".ui.user.UriLauncherActivity"
243253
android:configChanges="orientation|keyboardHidden|screenSize"
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
Copyright 2013 GitHub Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
17+
android:layout_width="match_parent"
18+
android:layout_height="match_parent"
19+
android:orientation="vertical" >
20+
21+
<TextView
22+
android:id="@+id/tv_signup"
23+
style="@style/SubtitleText"
24+
android:layout_width="match_parent"
25+
android:background="@drawable/sign_up_background"
26+
android:gravity="center"
27+
android:padding="5dp"
28+
android:textColor="@color/sign_up_text"
29+
android:textColorLink="@color/sign_up_text_link" />
30+
31+
<LinearLayout
32+
android:layout_width="match_parent"
33+
android:layout_height="wrap_content"
34+
android:orientation="vertical"
35+
android:paddingBottom="10dp"
36+
android:paddingLeft="15dp"
37+
android:paddingRight="15dp"
38+
android:paddingTop="5dp" >
39+
40+
<TextView
41+
style="@style/HeaderTitleText"
42+
android:paddingTop="10dp"
43+
android:text="@string/enter_otp_code_title" />
44+
45+
<EditText
46+
android:id="@+id/et_otp_code"
47+
style="@style/LoginEditText"
48+
android:layout_marginTop="5dp"
49+
android:gravity="center"
50+
android:imeOptions="actionDone"
51+
android:inputType="number"
52+
android:maxLength="6" />
53+
54+
<TextView
55+
style="@style/SubtitleText"
56+
android:paddingTop="10dp"
57+
android:text="@string/enter_otp_code_message"
58+
android:textColor="@color/text" />
59+
60+
</LinearLayout>
61+
62+
</LinearLayout>

app/res/values/strings.xml

+3
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@
152152
<string name="select_milestone">Select Milestone</string>
153153
<string name="select_labels">Select Labels</string>
154154
<string name="select_ref">Select Branch or Tag</string>
155+
<string name="enter_otp_code_title">Authentication Code</string>
156+
<string name="enter_otp_code_message">Two-factor authentication is enabled for your account. Enter your authentication code to verify your identity.</string>
155157
<string name="no_milestone">No milestone</string>
156158
<string name="unassigned">No one is assigned</string>
157159
<string name="assigned">is assigned</string>
@@ -179,6 +181,7 @@
179181
<string name="section_issue_labels">Labels:</string>
180182
<string name="log_in">Log in</string>
181183
<string name="signup_link">New to GitHub? &lt;a href=\"https://github.com/plans\">Click here&lt;/a> to sign up</string>
184+
<string name="signup_link_two_factor_auth">Not sure what to do? &lt;a href=\"https://help.github.com/articles/about-two-factor-authentication\">Get some help.&lt;/a></string>
182185
<string name="connection_failed">Unable to connect to GitHub</string>
183186
<string name="invalid_login_or_password">Please enter a valid login &amp; password</string>
184187
<string name="invalid_password">Please enter a valid password.</string>

app/src/main/java/com/github/mobile/RequestCodes.java

+5
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,9 @@ public interface RequestCodes {
9494
* Request to view a repository
9595
*/
9696
int REPOSITORY_VIEW = 12;
97+
98+
/**
99+
* Request to enter two-factor authentication OTP code
100+
*/
101+
int OTP_CODE_ENTER = 13;
97102
}

app/src/main/java/com/github/mobile/accounts/AccountAuthenticator.java

+9-12
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ class AccountAuthenticator extends AbstractAccountAuthenticator {
5050

5151
private static final String TAG = "GitHubAccountAuthenticator";
5252

53+
private static final List<String> SCOPES = Arrays.asList("repo", "user", "gist");
54+
5355
private Context context;
5456

5557
public AccountAuthenticator(final Context context) {
@@ -90,7 +92,7 @@ public Bundle editProperties(final AccountAuthenticatorResponse response,
9092
return null;
9193
}
9294

93-
private boolean isValidAuthorization(final Authorization auth,
95+
private static boolean isValidAuthorization(final Authorization auth,
9496
final List<String> requiredScopes) {
9597
if (auth == null)
9698
return false;
@@ -116,14 +118,12 @@ private Intent createLoginIntent(final AccountAuthenticatorResponse response) {
116118
* Get existing authorization for this app
117119
*
118120
* @param service
119-
* @param scopes
120121
* @return token or null if none found
121122
* @throws IOException
122123
*/
123-
private String getAuthorization(final OAuthService service,
124-
final List<String> scopes) throws IOException {
124+
public static String getAuthorization(final OAuthService service) throws IOException {
125125
for (Authorization auth : service.getAuthorizations())
126-
if (isValidAuthorization(auth, scopes))
126+
if (isValidAuthorization(auth, SCOPES))
127127
return auth.getToken();
128128
return null;
129129
}
@@ -132,16 +132,14 @@ private String getAuthorization(final OAuthService service,
132132
* Create authorization for this app
133133
*
134134
* @param service
135-
* @param scopes
136135
* @return created token
137136
* @throws IOException
138137
*/
139-
private String createAuthorization(final OAuthService service,
140-
final List<String> scopes) throws IOException {
138+
public static String createAuthorization(final OAuthService service) throws IOException {
141139
Authorization auth = new Authorization();
142140
auth.setNote(APP_NOTE);
143141
auth.setNoteUrl(APP_NOTE_URL);
144-
auth.setScopes(scopes);
142+
auth.setScopes(SCOPES);
145143
auth = service.createAuthorization(auth);
146144
return auth != null ? auth.getToken() : null;
147145
}
@@ -167,13 +165,12 @@ public Bundle getAuthToken(final AccountAuthenticatorResponse response,
167165
DefaultClient client = new DefaultClient();
168166
client.setCredentials(account.name, password);
169167
OAuthService service = new OAuthService(client);
170-
List<String> scopes = Arrays.asList("repo", "user", "gist");
171168

172169
String authToken;
173170
try {
174-
authToken = getAuthorization(service, scopes);
171+
authToken = getAuthorization(service);
175172
if (TextUtils.isEmpty(authToken))
176-
authToken = createAuthorization(service, scopes);
173+
authToken = createAuthorization(service);
177174
} catch (IOException e) {
178175
Log.e(TAG, "Authorization retrieval failed", e);
179176
throw new NetworkErrorException(e);

app/src/main/java/com/github/mobile/accounts/LoginActivity.java

+63-15
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
import static android.view.KeyEvent.ACTION_DOWN;
2828
import static android.view.KeyEvent.KEYCODE_ENTER;
2929
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
30-
import static com.github.mobile.accounts.AccountConstants.ACCOUNT_TYPE;
31-
import static com.github.mobile.accounts.AccountConstants.PROVIDER_AUTHORITY;
30+
import static com.github.mobile.accounts.AccountConstants.*;
31+
import static com.github.mobile.RequestCodes.OTP_CODE_ENTER;
32+
import static com.github.mobile.accounts.TwoFactorAuthActivity.PARAM_EXCEPTION;
33+
import static com.github.mobile.accounts.TwoFactorAuthClient.TWO_FACTOR_AUTH_TYPE_SMS;
34+
3235
import android.accounts.Account;
3336
import android.accounts.AccountManager;
3437
import android.app.AlertDialog;
@@ -59,7 +62,6 @@
5962
import com.actionbarsherlock.view.Menu;
6063
import com.actionbarsherlock.view.MenuItem;
6164
import com.github.kevinsawicki.wishlist.ViewFinder;
62-
import com.github.mobile.DefaultClient;
6365
import com.github.mobile.R.id;
6466
import com.github.mobile.R.layout;
6567
import com.github.mobile.R.menu;
@@ -77,6 +79,7 @@
7779

7880
import org.eclipse.egit.github.core.User;
7981
import org.eclipse.egit.github.core.client.GitHubClient;
82+
import org.eclipse.egit.github.core.service.OAuthService;
8083
import org.eclipse.egit.github.core.service.UserService;
8184

8285
import roboguice.util.RoboAsyncTask;
@@ -105,7 +108,7 @@ public class LoginActivity extends RoboSherlockAccountAuthenticatorActivity {
105108
*/
106109
private static final long SYNC_PERIOD = 8L * 60L * 60L;
107110

108-
private static void configureSyncFor(Account account) {
111+
public static void configureSyncFor(Account account) {
109112
Log.d(TAG, "Configuring account sync");
110113

111114
ContentResolver.setIsSyncable(account, PROVIDER_AUTHORITY, 1);
@@ -114,7 +117,7 @@ private static void configureSyncFor(Account account) {
114117
new Bundle(), SYNC_PERIOD);
115118
}
116119

117-
private static class AccountLoader extends
120+
public static class AccountLoader extends
118121
AuthenticatedUserTask<List<User>> {
119122

120123
@Inject
@@ -198,6 +201,7 @@ public void afterTextChanged(Editable gitDirEditText) {
198201

199202
passwordText.setOnKeyListener(new OnKeyListener() {
200203

204+
@Override
201205
public boolean onKey(View v, int keyCode, KeyEvent event) {
202206
if (event != null && ACTION_DOWN == event.getAction()
203207
&& keyCode == KEYCODE_ENTER && loginEnabled()) {
@@ -210,6 +214,7 @@ public boolean onKey(View v, int keyCode, KeyEvent event) {
210214

211215
passwordText.setOnEditorActionListener(new OnEditorActionListener() {
212216

217+
@Override
213218
public boolean onEditorAction(TextView v, int actionId,
214219
KeyEvent event) {
215220
if (actionId == IME_ACTION_DONE && loginEnabled()) {
@@ -305,9 +310,19 @@ public void onCancel(DialogInterface dialog) {
305310

306311
@Override
307312
public User call() throws Exception {
308-
GitHubClient client = new DefaultClient();
313+
GitHubClient client = new TwoFactorAuthClient();
309314
client.setCredentials(username, password);
310-
User user = new UserService(client).getUser();
315+
316+
User user;
317+
try {
318+
user = new UserService(client).getUser();
319+
} catch (TwoFactorAuthException e) {
320+
if (e.twoFactorAuthType == TWO_FACTOR_AUTH_TYPE_SMS)
321+
sendSmsOtpCode(new OAuthService(client));
322+
openTwoFactorAuthActivity();
323+
324+
return null;
325+
}
311326

312327
Account account = new Account(user.getLogin(), ACCOUNT_TYPE);
313328
if (requestNewAccount) {
@@ -330,24 +345,37 @@ protected void onException(Exception e) throws RuntimeException {
330345
dialog.dismiss();
331346

332347
Log.d(TAG, "Exception requesting authenticated user", e);
333-
334-
if (AccountUtils.isUnauthorized(e))
335-
onAuthenticationResult(false);
336-
else
337-
ToastUtils.show(LoginActivity.this, e,
338-
string.connection_failed);
348+
handleLoginException(e);
339349
}
340350

341351
@Override
342352
public void onSuccess(User user) {
343353
dialog.dismiss();
344354

345-
onAuthenticationResult(true);
355+
if (user != null)
356+
onAuthenticationResult(true);
346357
}
347358
};
348359
authenticationTask.execute();
349360
}
350361

362+
@Override
363+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
364+
super.onActivityResult(requestCode, resultCode, data);
365+
366+
if (requestCode == OTP_CODE_ENTER) {
367+
switch (resultCode) {
368+
case RESULT_OK:
369+
onAuthenticationResult(true);
370+
break;
371+
case RESULT_CANCELED:
372+
Exception e = (Exception) data.getExtras().getSerializable(PARAM_EXCEPTION);
373+
handleLoginException(e);
374+
break;
375+
}
376+
}
377+
}
378+
351379
/**
352380
* Called when response is received from the server for confirm credentials
353381
* request. See onAuthenticationResult(). Sets the
@@ -406,6 +434,7 @@ public void onAuthenticationResult(boolean result) {
406434
}
407435
}
408436

437+
@Override
409438
public boolean onOptionsItemSelected(MenuItem item) {
410439
switch (item.getItemId()) {
411440
case id.m_login:
@@ -432,4 +461,23 @@ private List<String> getEmailAddresses() {
432461
addresses.add(account.name);
433462
return addresses;
434463
}
435-
}
464+
465+
private void sendSmsOtpCode(final OAuthService service) throws IOException {
466+
try {
467+
AccountAuthenticator.createAuthorization(service);
468+
} catch (TwoFactorAuthException ignored) {
469+
}
470+
}
471+
472+
private void openTwoFactorAuthActivity() {
473+
Intent intent = TwoFactorAuthActivity.createIntent(this, username, password);
474+
startActivityForResult(intent, OTP_CODE_ENTER);
475+
}
476+
477+
private void handleLoginException(final Exception e) {
478+
if (AccountUtils.isUnauthorized(e))
479+
onAuthenticationResult(false);
480+
else
481+
ToastUtils.show(LoginActivity.this, e, string.connection_failed);
482+
}
483+
}

0 commit comments

Comments
 (0)