From 4b0e609a8985ee1a1687f25f60b3a444dc633992 Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 23 Jul 2016 11:52:52 +0800 Subject: [PATCH 1/3] Support two-factor authentication --- .../com/seafile/seadroid2/SeafConnection.java | 16 +++++++--- .../com/seafile/seadroid2/SeafException.java | 1 + .../account/ui/AccountDetailActivity.java | 31 ++++++++++++++++--- app/src/main/res/layout/account_detail.xml | 16 ++++++++++ app/src/main/res/values/strings.xml | 5 +++ 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java index 527795983..bd42e57df 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java @@ -5,6 +5,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.provider.Settings.Secure; +import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -151,12 +152,17 @@ private HttpRequest prepareApiDeleteRequest(String apiPath, Map param * @return true if login success, false otherwise * @throws SeafException */ - private boolean realLogin(String passwd) throws SeafException { + private boolean realLogin(String passwd, String authToken) throws SeafException { HttpRequest req = null; try { req = prepareApiPostRequest("api2/auth-token/", false, null); // Log.d(DEBUG_TAG, "Login to " + account.server + "api2/auth-token/"); + if (!TextUtils.isEmpty(authToken)) { + req.header("X-Seafile-OTP", authToken); + Log.d(DEBUG_TAG, "authToken " + authToken); + } + req.form("username", account.email); req.form("password", passwd); @@ -243,12 +249,12 @@ public String getServerInfo() throws SeafException { return result; } - public boolean doLogin(String passwd) throws SeafException { + public boolean doLogin(String passwd, String authToken) throws SeafException { try { - return realLogin(passwd); + return realLogin(passwd, authToken); } catch (Exception e) { // do again - return realLogin(passwd); + return realLogin(passwd, authToken); } } @@ -1506,6 +1512,8 @@ private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode) if (wiped != null) { throw SeafException.remoteWipedException; } + } else if (req.header("X-Seafile-OTP") != null && req.header("X-Seafile-OTP").equals("required")) { + throw SeafException.twoFactorAuthTokenMissing; } else { throw new SeafException(req.code(), req.message()); } diff --git a/app/src/main/java/com/seafile/seadroid2/SeafException.java b/app/src/main/java/com/seafile/seadroid2/SeafException.java index 3b664419a..6d96ca7c6 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafException.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafException.java @@ -21,6 +21,7 @@ public class SeafException extends Exception { public static final SeafException encryptException = new SeafException(10, "encryption key or iv is null"); public static final SeafException decryptException = new SeafException(11, "decryption key or iv is null"); public static final SeafException remoteWipedException = new SeafException(12, "Remote Wiped Error"); + public static final SeafException twoFactorAuthTokenMissing = new SeafException(13, "Two factor auth token is missing"); public SeafException(int code, String msg) { super(msg); diff --git a/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java b/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java index cd656a1b5..e6cbbe8f2 100644 --- a/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java @@ -7,6 +7,7 @@ import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Bundle; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.NavUtils; import android.support.v4.app.TaskStackBuilder; import android.support.v7.widget.Toolbar; @@ -62,6 +63,8 @@ public class AccountDetailActivity extends BaseActivity implements Toolbar.OnMen private TextView seahubUrlHintText; private ImageView clearEmail, clearPasswd, ivEyeClick; private RelativeLayout rlEye; + private TextInputLayout authTokenLayout; + private EditText authTokenText; private android.accounts.AccountManager mAccountManager; private boolean serverTextHasFocus; @@ -88,6 +91,10 @@ public void onCreate(Bundle savedInstanceState) { rlEye = (RelativeLayout) findViewById(R.id.rl_layout_eye); ivEyeClick = (ImageView) findViewById(R.id.iv_eye_click); + authTokenLayout = (TextInputLayout) findViewById(R.id.auth_token_hint); + authTokenText = (EditText) findViewById(R.id.auth_token); + authTokenLayout.setVisibility(View.GONE); + setupServerText(); Intent intent = getIntent(); @@ -375,6 +382,15 @@ public void login(View view) { return; } + String authToken = null; + if (authTokenLayout.getVisibility() == View.VISIBLE) { + authToken = authTokenText.getText().toString().trim(); + if (TextUtils.isEmpty(authToken)) { + authTokenText.setError(getResources().getString(R.string.two_factor_auth_token_empty)); + return; + } + } + try { serverURL = Utils.cleanServerURL(serverURL); } catch (MalformedURLException e) { @@ -394,7 +410,7 @@ public void login(View view) { progressDialog = new ProgressDialog(this); progressDialog.setMessage(getString(R.string.settings_cuc_loading)); progressDialog.setCancelable(false); - ConcurrentAsyncTask.execute(new LoginTask(tmpAccount, passwd)); + ConcurrentAsyncTask.execute(new LoginTask(tmpAccount, passwd, authToken)); } else { statusView.setText(R.string.network_down); } @@ -404,10 +420,12 @@ private class LoginTask extends AsyncTask { Account loginAccount; SeafException err = null; String passwd; + String authToken; - public LoginTask(Account loginAccount, String passwd) { + public LoginTask(Account loginAccount, String passwd, String authToken) { this.loginAccount = loginAccount; this.passwd = passwd; + this.authToken = authToken; } @Override @@ -425,7 +443,7 @@ protected String doInBackground(Void... params) { } private void resend() { - ConcurrentAsyncTask.execute(new LoginTask(loginAccount, passwd)); + ConcurrentAsyncTask.execute(new LoginTask(loginAccount, passwd, authToken)); } @Override @@ -448,6 +466,9 @@ public void onRejected() { }); dialog.show(getSupportFragmentManager(), SslConfirmDialog.FRAGMENT_TAG); return; + } else if (err == SeafException.twoFactorAuthTokenMissing) { + // show auth token input box + authTokenLayout.setVisibility(View.VISIBLE); } if (result != null && result.equals("Success")) { @@ -473,7 +494,7 @@ private String doLogin() { try { // if successful, this will place the auth token into "loginAccount" - if (!sc.doLogin(passwd)) + if (!sc.doLogin(passwd, authToken)) return getString(R.string.err_login_failed); // fetch email address from the server @@ -492,6 +513,8 @@ private String doLogin() { err = e; if (e == SeafException.sslException) { return getString(R.string.ssl_error); + } else if (e == SeafException.twoFactorAuthTokenMissing) { + return getString(R.string.two_factor_auth_error); } switch (e.getCode()) { case HttpURLConnection.HTTP_BAD_REQUEST: diff --git a/app/src/main/res/layout/account_detail.xml b/app/src/main/res/layout/account_detail.xml index 000cf8a66..c471d9e33 100644 --- a/app/src/main/res/layout/account_detail.xml +++ b/app/src/main/res/layout/account_detail.xml @@ -124,6 +124,22 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7ed5c56fb..ae1e2b85a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -497,4 +497,9 @@ Clear password failed Do you want to clear password? Auto clear passwords + + + Two-factor authentication token is required + Authentication token + Authentication token can\`t be empty From 9022740292b20c2f7df95a3abe2b6232c7a121f7 Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 23 Jul 2016 14:21:39 +0800 Subject: [PATCH 2/3] Show error message when auth token invalid --- .../com/seafile/seadroid2/SeafConnection.java | 30 +++++++++++++++++-- .../com/seafile/seadroid2/SeafException.java | 1 + .../account/ui/AccountDetailActivity.java | 10 +++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java index bd42e57df..09a2da418 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java @@ -153,6 +153,7 @@ private HttpRequest prepareApiDeleteRequest(String apiPath, Map param * @throws SeafException */ private boolean realLogin(String passwd, String authToken) throws SeafException { + boolean withAuthToken = false; HttpRequest req = null; try { req = prepareApiPostRequest("api2/auth-token/", false, null); @@ -160,7 +161,8 @@ private boolean realLogin(String passwd, String authToken) throws SeafException if (!TextUtils.isEmpty(authToken)) { req.header("X-Seafile-OTP", authToken); - Log.d(DEBUG_TAG, "authToken " + authToken); + withAuthToken = true; + // Log.d(DEBUG_TAG, "authToken " + authToken); } req.form("username", account.email); @@ -185,7 +187,7 @@ private boolean realLogin(String passwd, String authToken) throws SeafException req.form("client_version", appVersion); req.form("platform_version", Build.VERSION.RELEASE); - checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); + checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK, withAuthToken); String contentAsString = new String(req.bytes(), "UTF-8"); JSONObject obj = Utils.parseJsonObject(contentAsString); @@ -1502,6 +1504,25 @@ public Pair move(String srcRepoId, String srcPath, String dstRep } private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode) throws SeafException { + if (req.code() != expectedStatusCode) { + Log.d(DEBUG_TAG, "HTTP request failed : " + req.url() + ", " + req.code() + ", " + req.message()); + + if (req.message() == null) { + throw SeafException.networkException; + } else if (req.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + String wiped = req.header("X-Seafile-Wiped"); + if (wiped != null) { + throw SeafException.remoteWipedException; + } + } else { + throw new SeafException(req.code(), req.message()); + } + } else { + // Log.v(DEBUG_TAG, "HTTP request ok : " + req.url()); + } + } + + private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode, boolean withAuthToken) throws SeafException { if (req.code() != expectedStatusCode) { Log.d(DEBUG_TAG, "HTTP request failed : " + req.url() + ", " + req.code() + ", " + req.message()); @@ -1513,7 +1534,10 @@ private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode) throw SeafException.remoteWipedException; } } else if (req.header("X-Seafile-OTP") != null && req.header("X-Seafile-OTP").equals("required")) { - throw SeafException.twoFactorAuthTokenMissing; + if (withAuthToken) + throw SeafException.twoFactorAuthTokenInvalid; + else + throw SeafException.twoFactorAuthTokenMissing; } else { throw new SeafException(req.code(), req.message()); } diff --git a/app/src/main/java/com/seafile/seadroid2/SeafException.java b/app/src/main/java/com/seafile/seadroid2/SeafException.java index 6d96ca7c6..ace6e10c4 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafException.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafException.java @@ -22,6 +22,7 @@ public class SeafException extends Exception { public static final SeafException decryptException = new SeafException(11, "decryption key or iv is null"); public static final SeafException remoteWipedException = new SeafException(12, "Remote Wiped Error"); public static final SeafException twoFactorAuthTokenMissing = new SeafException(13, "Two factor auth token is missing"); + public static final SeafException twoFactorAuthTokenInvalid = new SeafException(14, "Two factor auth token is invalid"); public SeafException(int code, String msg) { super(msg); diff --git a/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java b/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java index e6cbbe8f2..9de893e09 100644 --- a/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/account/ui/AccountDetailActivity.java @@ -450,6 +450,7 @@ private void resend() { protected void onPostExecute(final String result) { progressDialog.dismiss(); if (err == SeafException.sslException) { + authTokenLayout.setVisibility(View.GONE); SslConfirmDialog dialog = new SslConfirmDialog(loginAccount, new SslConfirmDialog.Listener() { @Override @@ -469,6 +470,13 @@ public void onRejected() { } else if (err == SeafException.twoFactorAuthTokenMissing) { // show auth token input box authTokenLayout.setVisibility(View.VISIBLE); + authTokenText.setError(getString(R.string.two_factor_auth_error)); + } else if (err == SeafException.twoFactorAuthTokenInvalid) { + // show auth token input box + authTokenLayout.setVisibility(View.VISIBLE); + authTokenText.setError(getString(R.string.two_factor_auth_invalid)); + } else { + authTokenLayout.setVisibility(View.GONE); } if (result != null && result.equals("Success")) { @@ -515,6 +523,8 @@ private String doLogin() { return getString(R.string.ssl_error); } else if (e == SeafException.twoFactorAuthTokenMissing) { return getString(R.string.two_factor_auth_error); + } else if (e == SeafException.twoFactorAuthTokenInvalid) { + return getString(R.string.two_factor_auth_invalid); } switch (e.getCode()) { case HttpURLConnection.HTTP_BAD_REQUEST: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae1e2b85a..c3f7c7aca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -500,6 +500,7 @@ Two-factor authentication token is required + Two-factor authentication token is invalid Authentication token Authentication token can\`t be empty From c40621ab4dfcfd30d9be34f1e0fcf4076a8abf9e Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 23 Jul 2016 17:07:07 +0800 Subject: [PATCH 3/3] Code clean up --- app/src/main/java/com/seafile/seadroid2/SeafConnection.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java index 09a2da418..f055c17e6 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java @@ -1528,11 +1528,6 @@ private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode, if (req.message() == null) { throw SeafException.networkException; - } else if (req.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { - String wiped = req.header("X-Seafile-Wiped"); - if (wiped != null) { - throw SeafException.remoteWipedException; - } } else if (req.header("X-Seafile-OTP") != null && req.header("X-Seafile-OTP").equals("required")) { if (withAuthToken) throw SeafException.twoFactorAuthTokenInvalid;