Merge pull request #771 from nextcloud/web-login

Add support for web login
This commit is contained in:
Andy Scherzinger 2017-03-27 21:56:41 +02:00 committed by GitHub
commit 89409ce049
3 changed files with 344 additions and 183 deletions

View file

@ -4,8 +4,10 @@
* @author Bartek Przybylski
* @author David A. Velasco
* @author masensio
* @author Mario Danic
* Copyright (C) 2012 Bartek Przybylski
* Copyright (C) 2015 ownCloud Inc.
* Copyright (C) 2017 Mario Danic
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
@ -18,6 +20,22 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* All changes by Mario Danic are distributed under the following terms:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.owncloud.android.authentication;
@ -30,6 +48,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@ -38,12 +57,14 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MotionEvent;
@ -54,6 +75,7 @@ import android.view.inputmethod.EditorInfo;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
@ -191,6 +213,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
private View mOkButton;
private TextView mAuthStatusView;
private WebView mLoginWebView;
private int mAuthStatusText = 0, mAuthStatusIcon = 0;
private String mAuthToken = "";
@ -205,6 +229,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
private final String OAUTH_TOKEN_TYPE = AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType());
private final String SAML_TOKEN_TYPE = AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType());
private boolean webViewLoginMethod;
private String webViewUser;
private String webViewPassword;
/**
* {@inheritDoc}
*
@ -242,7 +270,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mIsFirstAuthAttempt = savedInstanceState.getBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG);
}
webViewLoginMethod = !TextUtils.isEmpty(getResources().getString(R.string.webview_login_url));
if (webViewLoginMethod) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
/// load user interface
if (!webViewLoginMethod) {
setContentView(R.layout.account_setup);
/// initialize general UI elements
@ -274,10 +309,47 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
});
/// initialize block to be moved to single Fragment to check server and get info about it
initServerPreFragment(savedInstanceState);
/// initialize block to be moved to single Fragment to retrieve and validate credentials
initAuthorizationPreFragment(savedInstanceState);
} else {
setContentView(R.layout.account_setup_webview);
mLoginWebView = (WebView) findViewById(R.id.login_webview);
initWebViewLogin();
}
initServerPreFragment(savedInstanceState);
}
private void initWebViewLogin() {
mLoginWebView.getSettings().setAllowFileAccess(false);
mLoginWebView.getSettings().setJavaScriptEnabled(true);
mLoginWebView.getSettings().setUserAgentString(MainApp.getUserAgent());
mLoginWebView.loadUrl(getResources().getString(R.string.webview_login_url));
mLoginWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/")) {
parseAndLoginFromWebView(url);
return true;
}
return false;
}
});
}
private void parseAndLoginFromWebView(String dataString) {
String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/";
LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString);
if (loginUrlInfo != null) {
mServerInfo.mBaseUrl = normalizeUrlSuffix(loginUrlInfo.serverAddress);
webViewUser = loginUrlInfo.username;
webViewPassword = loginUrlInfo.password;
checkOcServer();
}
}
private void populateLoginFields(String dataString) throws IllegalArgumentException {
@ -420,7 +492,11 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL);
mServerInfo.mVersion = AccountUtils.getServerVersion(mAccount);
} else {
if (!webViewLoginMethod) {
mServerInfo.mBaseUrl = getString(R.string.server_url).trim();
} else {
mServerInfo.mBaseUrl = getString(R.string.webview_login_url).trim();
}
mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL);
}
} else {
@ -442,6 +518,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
if (!webViewLoginMethod) {
/// step 2 - set properties of UI elements (text, visibility, enabled...)
mHostUrlInput = (CustomEditText) findViewById(R.id.hostUrlInput);
// Convert IDN to Unicode
@ -451,21 +528,12 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mHostUrlInput.setEnabled(false);
mHostUrlInput.setFocusable(false);
}
String serverInputType = getResources().getString(R.string.server_input_type);
if (isUrlInputAllowed) {
mRefreshButton = findViewById(R.id.embeddedRefreshButton);
if (mAction == ACTION_CREATE &&
(serverInputType.equals(DIRECTORY_SERVER_INPUT_TYPE) ||
serverInputType.equals(SUBDOMAIN_SERVER_INPUT_TYPE))) {
mHostUrlInput.setText("");
}
} else {
findViewById(R.id.hostUrlFrame).setVisibility(View.GONE);
mRefreshButton = findViewById(R.id.centeredRefreshButton);
}
showRefreshButton(mServerIsChecked && !mServerIsValid &&
mWaitingForOpId > Integer.MAX_VALUE);
mServerStatusView = (TextView) findViewById(R.id.server_status_text);
@ -482,7 +550,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
public void afterTextChanged(Editable s) {
if (mOkButton.isEnabled() &&
!mServerInfo.mBaseUrl.equals(
normalizeUrl(mHostUrlInput.getFullServerUrl(), mServerInfo.mIsSslConn))) {
normalizeUrl(s.toString(), mServerInfo.mIsSslConn))) {
mOkButton.setEnabled(false);
}
}
@ -518,6 +586,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
});
}
}
/**
@ -645,11 +714,20 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
outState.putString(KEY_AUTH_TOKEN_TYPE, mAuthTokenType);
outState.putLong(KEY_WAITING_FOR_OP_ID, mWaitingForOpId);
if (!webViewLoginMethod) {
/// Server PRE-fragment state
outState.putInt(KEY_SERVER_STATUS_TEXT, mServerStatusText);
outState.putInt(KEY_SERVER_STATUS_ICON, mServerStatusIcon);
outState.putBoolean(KEY_SERVER_CHECKED, mServerIsChecked);
outState.putBoolean(KEY_SERVER_VALID, mServerIsValid);
/// Authentication PRE-fragment state
outState.putBoolean(KEY_PASSWORD_EXPOSED, isPasswordVisible());
outState.putInt(KEY_AUTH_STATUS_ICON, mAuthStatusIcon);
outState.putInt(KEY_AUTH_STATUS_TEXT, mAuthStatusText);
outState.putString(KEY_AUTH_TOKEN, mAuthToken);
}
outState.putBoolean(KEY_IS_SSL_CONN, mServerInfo.mIsSslConn);
outState.putString(KEY_HOST_URL_TEXT, mServerInfo.mBaseUrl);
if (mServerInfo.mVersion != null) {
@ -657,18 +735,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
outState.putString(KEY_SERVER_AUTH_METHOD, mServerInfo.mAuthMethod.name());
/// Authentication PRE-fragment state
outState.putBoolean(KEY_PASSWORD_EXPOSED, isPasswordVisible());
outState.putInt(KEY_AUTH_STATUS_ICON, mAuthStatusIcon);
outState.putInt(KEY_AUTH_STATUS_TEXT, mAuthStatusText);
outState.putString(KEY_AUTH_TOKEN, mAuthToken);
/// authentication
outState.putBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG, mIsFirstAuthAttempt);
/// AsyncTask (User and password)
if (!webViewLoginMethod) {
outState.putString(KEY_USERNAME, mUsernameInput.getText().toString().trim());
outState.putString(KEY_PASSWORD, mPasswordInput.getText().toString());
}
if (mAsyncTask != null) {
mAsyncTask.cancel(true);
@ -730,6 +804,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
protected void onResume() {
super.onResume();
if (!webViewLoginMethod) {
// bound here to avoid spurious changes triggered by Android on device rotations
mHostUrlInput.setOnFocusChangeListener(this);
mHostUrlInput.addTextChangedListener(mHostUrlInputWatcher);
@ -747,6 +822,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
Log_OC.e(TAG, "Illegal login data URL used, no Login pre-fill!", e);
}
}
}
// bind to Operations Service
mOperationsServiceConnection = new OperationsServiceConnection();
@ -772,8 +848,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mOperationsServiceBinder.removeOperationListener(this);
}
if (!webViewLoginMethod) {
mHostUrlInput.removeTextChangedListener(mHostUrlInputWatcher);
mHostUrlInput.setOnFocusChangeListener(null);
}
super.onPause();
}
@ -787,9 +865,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
unbindService(mOperationsServiceConnection);
mOperationsServiceBinder = null;
}
super.onDestroy();
if (webViewLoginMethod) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
}
super.onDestroy();
}
/**
* Parses the redirection with the response to the GET AUTHORIZATION request to the
@ -863,15 +945,23 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
private void checkOcServer() {
String uri = mHostUrlInput.getFullServerUrl().trim();
String uri;
if (mHostUrlInput != null) {
uri = mHostUrlInput.getText().toString().trim();
mOkButton.setEnabled(false);
showRefreshButton(false);
} else {
uri = mServerInfo.mBaseUrl;
}
mServerIsValid = false;
mServerIsChecked = false;
mOkButton.setEnabled(false);
mServerInfo = new GetServerInfoOperation.ServerInfo();
showRefreshButton(false);
if (uri.length() != 0) {
if (mHostUrlInput != null) {
uri = stripIndexPhpOrAppsFiles(uri, mHostUrlInput);
}
// Handle internationalized domain names
try {
@ -880,9 +970,11 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
// Let Owncloud library check the error of the malformed URI
}
if (mHostUrlInput != null) {
mServerStatusText = R.string.auth_testing_connection;
mServerStatusIcon = R.drawable.progress_small;
showServerStatus();
}
Intent getServerInfoIntent = new Intent();
getServerInfoIntent.setAction(OperationsService.ACTION_GET_SERVER_INFO);
@ -890,6 +982,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
OperationsService.EXTRA_SERVER_URL,
normalizeUrlSuffix(uri)
);
if (mOperationsServiceBinder != null) {
mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent);
} else {
@ -899,9 +992,11 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
} else {
mServerStatusText = 0;
mServerStatusIcon = 0;
if (!webViewLoginMethod) {
showServerStatus();
}
}
}
/**
@ -913,6 +1008,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
*
* @param hasFocus 'True' if focus is received, 'false' if is lost
*/
private void onPasswordFocusChanged(boolean hasFocus) {
if (hasFocus) {
showViewPasswordButton();
@ -992,7 +1088,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
startSamlBasedFederatedSingleSignOnAuthorization();
} else {
checkBasicAuthorization();
checkBasicAuthorization(null, null);
}
}
@ -1002,10 +1098,17 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
* Tests the credentials entered by the user performing a check of existence on
* the root folder of the ownCloud server.
*/
private void checkBasicAuthorization() {
private void checkBasicAuthorization(@Nullable String webViewUsername, @Nullable String webViewPassword) {
/// get basic credentials entered by user
String username = mUsernameInput.getText().toString().trim();
String password = mPasswordInput.getText().toString();
String username;
String password;
if (!webViewLoginMethod) {
username = mUsernameInput.getText().toString().trim();
password = mPasswordInput.getText().toString();
} else {
username = webViewUsername;
password = webViewPassword;
}
/// be gentle with the user
IndeterminateProgressDialog dialog =
@ -1109,11 +1212,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
if (mAction == ACTION_CREATE) {
if (!webViewLoginMethod) {
mUsernameInput.setText(username);
}
success = createAccount(result);
} else {
if (!mUsernameInput.getText().toString().trim().equals(username)) {
if (!webViewLoginMethod && !mUsernameInput.getText().toString().trim().equals(username)) {
// fail - not a new account, but an existing one; disallow
result = new RemoteOperationResult(ResultCode.ACCOUNT_NOT_THE_SAME);
mAuthToken = "";
@ -1138,8 +1243,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
finish();
}
} else {
if (!webViewLoginMethod) {
updateStatusIconFailUserName();
showAuthStatus();
}
Log_OC.e(TAG, "Access to user name failed: " + result.getLogMessage());
}
@ -1157,7 +1264,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mWaitingForOpId = Long.MAX_VALUE;
// update server status, but don't show it yet
if (!webViewLoginMethod) {
updateServerStatusIconAndText(result);
}
if (result.isSuccess()) {
/// SUCCESS means:
@ -1167,9 +1276,16 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
// 4. we got the authentication method required by the server
mServerInfo = (GetServerInfoOperation.ServerInfo) (result.getData().get(0));
if (webViewLoginMethod) {
checkBasicAuthorization(webViewUser, webViewPassword);
}
if (!authSupported(mServerInfo.mAuthMethod)) {
updateServerStatusIconNoRegularAuth(); // overrides updateServerStatusIconAndText()
if (!webViewLoginMethod) {
// overrides updateServerStatusIconAndText()
updateServerStatusIconNoRegularAuth();
}
mServerIsValid = false;
} else {
@ -1181,9 +1297,11 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
// refresh UI
if (!webViewLoginMethod) {
showRefreshButton(!mServerIsValid);
showServerStatus();
mOkButton.setEnabled(mServerIsValid);
}
/// very special case (TODO: move to a common place for all the remote operations)
if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) {
@ -1511,11 +1629,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
showServerStatus();
mAuthStatusIcon = 0;
mAuthStatusText = 0;
if (!webViewLoginMethod) {
showAuthStatus();
// update input controls state
showRefreshButton(true);
mOkButton.setEnabled(false);
}
// very special case (TODO: move to a common place for all the remote operations)
if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) {
@ -1523,8 +1643,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
} else { // authorization fail due to client side - probably wrong credentials
if (!webViewLoginMethod) {
updateAuthStatusIconAndText(result);
showAuthStatus();
}
Log_OC.d(TAG, "Access failed: " + result.getLogMessage());
}
}
@ -1562,8 +1684,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken);
} else {
if (!webViewLoginMethod) {
response.putString(AccountManager.KEY_AUTHTOKEN, mPasswordInput.getText().toString());
mAccountMgr.setPassword(mAccount, mPasswordInput.getText().toString());
} else {
response.putString(AccountManager.KEY_AUTHTOKEN, webViewPassword);
mAccountMgr.setPassword(mAccount, webViewPassword);
}
}
// remove managed clients for this account to enforce creation with fresh credentials
@ -1599,7 +1726,12 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
}
Uri uri = Uri.parse(mServerInfo.mBaseUrl);
String username = mUsernameInput.getText().toString().trim();
String username;
if (!webViewLoginMethod) {
username = mUsernameInput.getText().toString().trim();
} else {
username = webViewUser;
}
if (isOAuth) {
username = "OAuth_user" + (new java.util.Random(System.currentTimeMillis())).nextLong();
}
@ -1609,8 +1741,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
if (AccountUtils.exists(newAccount, getApplicationContext())) {
// fail - not a new account, but an existing one; disallow
RemoteOperationResult result = new RemoteOperationResult(ResultCode.ACCOUNT_NOT_NEW);
if (!webViewLoginMethod) {
updateAuthStatusIconAndText(result);
showAuthStatus();
}
Log_OC.d(TAG, result.getLogMessage());
return false;
@ -1621,9 +1755,15 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
// with external authorizations, the password is never input in the app
mAccountMgr.addAccountExplicitly(mAccount, "", null);
} else {
if (!webViewLoginMethod) {
mAccountMgr.addAccountExplicitly(
mAccount, mPasswordInput.getText().toString(), null
);
} else {
mAccountMgr.addAccountExplicitly(
mAccount, webViewPassword, null
);
}
}
// include account version with the new account
@ -1706,6 +1846,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
* to the last check on the ownCloud server.
*/
private void showServerStatus() {
if (!webViewLoginMethod) {
if (mServerStatusIcon == 0 && mServerStatusText == 0) {
mServerStatusView.setVisibility(View.INVISIBLE);
@ -1714,7 +1855,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mServerStatusView.setCompoundDrawablesWithIntrinsicBounds(mServerStatusIcon, 0, 0, 0);
mServerStatusView.setVisibility(View.VISIBLE);
}
}
}
@ -1723,6 +1864,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
* to the interactions with the OAuth authorization server.
*/
private void showAuthStatus() {
if (!webViewLoginMethod) {
if (mAuthStatusIcon == 0 && mAuthStatusText == 0) {
mAuthStatusView.setVisibility(View.INVISIBLE);
@ -1732,15 +1874,18 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mAuthStatusView.setVisibility(View.VISIBLE);
}
}
}
private void showRefreshButton(boolean show) {
if (webViewLoginMethod) {
if (show) {
mRefreshButton.setVisibility(View.VISIBLE);
} else {
mRefreshButton.setVisibility(View.GONE);
}
}
}
/**
* Called when the eye icon in the password field is clicked.
@ -1951,7 +2096,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
mOperationsServiceBinder.dispatchResultIfFinished((int) mWaitingForOpId, this);
}
if (mHostUrlInput.getText() != null && mHostUrlInput.getText().length() > 0 && !mServerIsChecked) {
if (!webViewLoginMethod && mHostUrlInput.getText() != null && mHostUrlInput.getText().length() > 0
&& !mServerIsChecked) {
checkOcServer();
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/login_webview">
</WebView>
</LinearLayout>

View file

@ -102,6 +102,9 @@
<!-- login data links -->
<string name="login_data_own_scheme" translatable="false">cloud</string>
<!-- url for webview login, with the protocol prefix
If set, will replace all other login methods available -->
<string name="webview_login_url" translatable="false"></string>
<!-- analytics enabled -->
<bool name="analytics_enabled">false</bool>