Эх сурвалжийг харах

Merge pull request #12922 from nextcloud/feature/login_flow_v2

Login Flow v2
Tobias Kaminsky 11 сар өмнө
parent
commit
3755df0e5b

+ 200 - 28
app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java

@@ -17,6 +17,7 @@ import android.Manifest;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.annotation.SuppressLint;
+import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -33,29 +34,42 @@ import android.os.IBinder;
 import android.preference.PreferenceManager;
 import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.inputmethod.EditorInfo;
 import android.webkit.CookieManager;
 import android.webkit.CookieSyncManager;
 import android.webkit.WebResourceRequest;
 import android.webkit.WebResourceResponse;
 import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 import android.widget.Toast;
 
 import com.blikoon.qrcodescanner.QrCodeActivity;
+import com.google.android.material.button.MaterialButton;
 import com.google.android.material.snackbar.Snackbar;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
 import com.nextcloud.android.common.ui.color.ColorUtil;
 import com.nextcloud.android.common.ui.theme.utils.ColorRole;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.device.DeviceInfo;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.onboarding.FirstRunActivity;
 import com.nextcloud.client.onboarding.OnboardingService;
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.common.PlainClient;
+import com.nextcloud.operations.PostMethod;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -100,6 +114,8 @@ import com.owncloud.android.utils.WebViewUtil;
 import com.owncloud.android.utils.theme.CapabilityUtils;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
 
+import org.json.JSONObject;
+
 import java.io.InputStream;
 import java.net.URLDecoder;
 import java.util.HashMap;
@@ -107,9 +123,13 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -125,6 +145,8 @@ import de.cotech.hw.fido.ui.FidoDialogOptions;
 import de.cotech.hw.fido2.WebViewWebauthnBridge;
 import de.cotech.hw.fido2.ui.WebauthnDialogOptions;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import okhttp3.FormBody;
+import okhttp3.RequestBody;
 
 import static com.owncloud.android.utils.PermissionUtil.PERMISSIONS_CAMERA;
 
@@ -162,7 +184,17 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     private static final String KEY_USERNAME = "USERNAME";
     private static final String KEY_PASSWORD = "PASSWORD";
     private static final String KEY_ASYNC_TASK_IN_PROGRESS = "AUTH_IN_PROGRESS";
-    public static final String WEB_LOGIN = "/index.php/login/flow";
+
+    /**
+     * Login Flow v1
+     */
+    // public static final String WEB_LOGIN = "/index.php/login/flow";
+
+    /**
+     * Login Flow v2
+     */
+    public static final String WEB_LOGIN = "/index.php/login/v2";
+
     public static final String PROTOCOL_SUFFIX = "://";
     public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";
     public static final String HTTPS_PROTOCOL = "https://";
@@ -171,7 +203,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     public static final int NO_ICON = 0;
     public static final String EMPTY_STRING = "";
 
-    private static final int REQUEST_CODE_QR_SCAN = 101;
     public static final int REQUEST_CODE_FIRST_RUN = 102;
 
     /// parameters from EXTRAs in starter Intent
@@ -218,6 +249,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     @Inject PassCodeManager passCodeManager;
     @Inject ViewThemeUtils.Factory viewThemeUtilsFactory;
     @Inject ColorUtil colorUtil;
+    @Inject ClientFactory clientFactory;
+
+    private String token;
 
     private boolean onlyAdd = false;
     @SuppressLint("ResourceAsColor") @ColorInt
@@ -242,7 +276,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground();
         viewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY);
 
-        WebViewUtil webViewUtil = new WebViewUtil(this);
+        // WebViewUtil webViewUtil = new WebViewUtil(this);
 
         Uri data = getIntent().getData();
         boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme));
@@ -298,7 +332,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         if (webViewLoginMethod) {
             accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater());
             setContentView(accountSetupWebviewBinding.getRoot());
-            initWebViewLogin(webloginUrl, false);
+            anonymouslyPostLoginRequest(webloginUrl);
+            // initWebViewLogin(webloginUrl, false);
         } else {
             accountSetupBinding = AccountSetupBinding.inflate(getLayoutInflater());
             setContentView(accountSetupBinding.getRoot());
@@ -314,7 +349,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
         initServerPreFragment(savedInstanceState);
 
-        webViewUtil.checkWebViewVersion();
+        // webViewUtil.checkWebViewVersion();
     }
 
     private void deleteCookies() {
@@ -326,11 +361,57 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         }
     }
 
+    private String baseUrl;
+
+    /**
+     * This function facilitates the login process by anonymously posting a login request to a specified URL.
+     * After posting the request, it retrieves the login URL for completing the login flow.
+     * The login flow version used is v2.
+     *
+     * @param url The URL where the login request is to be anonymously posted.
+     *            This URL should handle the login request and return the login URL.
+     *            It's typically the entry point for the login process.
+     *            Example: "https://example.com/index.php/login/v2"
+     */
+    private void anonymouslyPostLoginRequest(String url) {
+        baseUrl = url;
+
+        Thread thread = new Thread(() -> {
+            PostMethod post = new PostMethod(baseUrl, false, new FormBody.Builder().build());
+
+            PlainClient client = clientFactory.createPlainClient();
+            post.execute(client);
+            String response = post.getResponseBodyAsString();
+            JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject();
+            String login = jsonObject.get("login").getAsString();
+            if (login == null) {
+                login = getResources().getString(R.string.webview_login_url);
+            }
+
+            String loginUrl = login;
+            runOnUiThread(() -> {
+                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(loginUrl));
+                loginFlowResultLauncher.launch(intent);
+            });
+
+            token = jsonObject.getAsJsonObject("poll").get("token").getAsString();
+        });
+
+        thread.start();
+    }
+
+    private final ActivityResultLauncher<Intent> loginFlowResultLauncher = registerForActivityResult(
+        new ActivityResultContracts.StartActivityForResult(), result -> poolLogin(clientFactory.createPlainClient()));
+
     private static String getWebLoginUserAgent() {
         return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
             Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " (Android)";
     }
 
+    /**
+     * @Deprecated This function is deprecated. Please use the {@link #anonymouslyPostLoginRequest(String)} method instead, which utilizes the improved login flow v2.
+     */
+    @Deprecated
     @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT")
     @SuppressLint("SetJavaScriptEnabled")
     private void initWebViewLogin(String baseURL, boolean useGenericUserAgent) {
@@ -680,7 +761,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) {
             accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater());
             setContentView(accountSetupWebviewBinding.getRoot());
-            initWebViewLogin(getString(R.string.provider_registration_server), true);
+            anonymouslyPostLoginRequest(getString(R.string.provider_registration_server));
+            // initWebViewLogin(getString(R.string.provider_registration_server), true);
         }
     }
 
@@ -915,7 +997,16 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
                 accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater());
                 setContentView(accountSetupWebviewBinding.getRoot());
-                initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false);
+
+                if (!isLoginProcessCompleted) {
+                    if (!isRedirectedToTheDefaultBrowser) {
+                        anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN);
+                        isRedirectedToTheDefaultBrowser = true;
+                    } else {
+                        initLoginInfoView();
+                    }
+                    // initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false);
+                }
             }
         } else {
             updateServerStatusIconAndText(result);
@@ -928,6 +1019,19 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         }
     }
 
+    // region LoginInfoView
+    private void initLoginInfoView() {
+        LinearLayout loginFlowLayout = accountSetupWebviewBinding.loginFlowV2.getRoot();
+        MaterialButton cancelButton = accountSetupWebviewBinding.loginFlowV2.cancelButton;
+        loginFlowLayout.setVisibility(View.VISIBLE);
+
+        cancelButton.setOnClickListener(v -> {
+            loginFlowExecutorService.shutdown();
+            recreate();
+        });
+    }
+    // endregion
+
     /**
      * Chooses the right icon and text to show to the user for the received operation result.
      *
@@ -1169,7 +1273,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
         } else {    // authorization fail due to client side - probably wrong credentials
             if (accountSetupWebviewBinding != null) {
-                initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false);
+                anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN);
+                // initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false);
                 DisplayUtils.showSnackMessage(this,
                                               accountSetupWebviewBinding.loginWebview, R.string.auth_access_failed,
                                               result.getLogMessage());
@@ -1340,9 +1445,37 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
     private void startQRScanner() {
         Intent intent = new Intent(this, QrCodeActivity.class);
-        startActivityForResult(intent, REQUEST_CODE_QR_SCAN);
+        qrScanResultLauncher.launch(intent);
     }
 
+    private final ActivityResultLauncher<Intent> qrScanResultLauncher = registerForActivityResult(
+        new ActivityResultContracts.StartActivityForResult(),
+        result -> {
+            if (result.getResultCode() == Activity.RESULT_OK) {
+                Intent data = result.getData();
+
+                if (data == null) {
+                    return;
+                }
+
+                String resultData = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult");
+
+                if (resultData == null || !resultData.startsWith(getString(R.string.login_data_own_scheme))) {
+                    mServerStatusIcon = R.drawable.ic_alert;
+                    mServerStatusText = "QR Code could not be read!";
+                    showServerStatus();
+                    return;
+                }
+
+                if (!getResources().getBoolean(R.bool.multiaccount_support) &&
+                    accountManager.getAccounts().length == 1) {
+                    Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
+                } else {
+                    parseAndLoginFromWebView(resultData);
+                }
+            }
+        });
+
     @Override
     public void onRequestPermissionsResult(int requestCode,
                                            @NonNull String[] permissions,
@@ -1362,7 +1495,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
      * server.
      */
     private void showServerStatus() {
-        if (accountSetupBinding == null) return;
+        if (accountSetupBinding == null) {
+            return;
+        }
 
         if (mServerStatusIcon == NO_ICON && EMPTY_STRING.equals(mServerStatusText)) {
             accountSetupBinding.serverStatusText.setVisibility(View.INVISIBLE);
@@ -1486,30 +1621,67 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         }
     }
 
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-        if (requestCode == REQUEST_CODE_QR_SCAN) {
-            if (data == null) {
-                return;
+    private final ScheduledExecutorService loginFlowExecutorService = Executors.newSingleThreadScheduledExecutor();
+    private boolean isLoginProcessCompleted = false;
+    private boolean isRedirectedToTheDefaultBrowser = false;
+
+    private void poolLogin(PlainClient client) {
+        loginFlowExecutorService.scheduleAtFixedRate(() -> {
+            if (!isLoginProcessCompleted) {
+                performLoginFlowV2(client);
             }
+        }, 0, 30, TimeUnit.SECONDS);
+    }
 
-            String result = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult");
+    private void performLoginFlowV2(PlainClient client) {
+        String postRequestUrl = baseUrl + "/poll";
 
-            if (result == null || !result.startsWith(getString(R.string.login_data_own_scheme))) {
-                mServerStatusIcon = R.drawable.ic_alert;
-                mServerStatusText = "QR Code could not be read!";
-                showServerStatus();
-                return;
-            }
+        RequestBody requestBody = new FormBody.Builder()
+            .add("token", token)
+            .build();
 
-            if (!getResources().getBoolean(R.bool.multiaccount_support) &&
-                accountManager.getAccounts().length == 1) {
-                Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
-            } else {
-                parseAndLoginFromWebView(result);
+        PostMethod post = new PostMethod(postRequestUrl, false, requestBody);
+        int status = post.execute(client);
+        String response = post.getResponseBodyAsString();
+
+        Log_OC.d(TAG, "performLoginFlowV2 status: " + status);
+        Log_OC.d(TAG, "performLoginFlowV2 response: " + response);
+
+        if (!response.isEmpty()) {
+            runOnUiThread(() -> completeLoginFlow(response, status));
+        }
+    }
+
+    private void completeLoginFlow(String response, int status) {
+        try {
+            JSONObject jsonObject = new JSONObject(response);
+
+            String server = jsonObject.getString("server");
+            String loginName = jsonObject.getString("loginName");
+            String appPassword = jsonObject.getString("appPassword");
+
+            LoginUrlInfo loginUrlInfo = new LoginUrlInfo();
+            loginUrlInfo.serverAddress = server;
+            loginUrlInfo.username = loginName;
+            loginUrlInfo.password = appPassword;
+
+            isLoginProcessCompleted = (status == 200 && !server.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty());
+
+            if (accountSetupBinding != null) {
+                accountSetupBinding.hostUrlInput.setText("");
             }
+            mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.serverAddress);
+            webViewUser = loginUrlInfo.username;
+            webViewPassword = loginUrlInfo.password;
+        } catch (Exception e) {
+            Log_OC.d(TAG, "Error caught at completeLoginFlow: " + e);
+            mServerStatusIcon = R.drawable.ic_alert;
+            mServerStatusText = getString(R.string.qr_could_not_be_read);
+            showServerStatus();
         }
+
+        checkOcServer();
+        loginFlowExecutorService.shutdown();
     }
 
     /**

+ 16 - 6
app/src/main/res/layout/account_setup_webview.xml

@@ -7,21 +7,31 @@
   ~ SPDX-License-Identifier: AGPL-3.0-or-later
 -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             android:orientation="vertical"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent">
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:orientation="vertical">
 
+    <!-- Login Flow V1 -->
     <WebView
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:visibility="gone"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
         android:id="@+id/login_webview">
     </WebView>
 
+    <!-- Login Flow V2 -->
+    <include
+        tools:visibility="visible"
+        android:visibility="gone"
+        android:id="@+id/login_flow_v2"
+        layout="@layout/login_flow_info_layout_v2" />
+
     <ProgressBar
         android:id="@+id/login_webview_progress_bar"
         style="?android:attr/progressBarStyle"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
-        android:indeterminate="true"/>
+        android:indeterminate="true" />
 </FrameLayout>

+ 41 - 0
app/src/main/res/layout/login_flow_info_layout_v2.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
+  ~ SPDX-License-Identifier: AGPL-3.0-or-later
+  -->
+
+<LinearLayout
+    android:id="@+id/login_flow_info_v2"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    android:orientation="vertical"
+    android:gravity="center"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <TextView
+        android:text="@string/authenticator_activity_please_complete_login_process"
+        android:layout_width="match_parent"
+        android:gravity="center"
+        android:textColor="@color/white"
+        android:textSize="@dimen/splash_text_size"
+        android:layout_marginBottom="@dimen/standard_double_margin"
+        android:layout_height="wrap_content"/>
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/cancel_button"
+        app:backgroundTint="@color/white"
+        android:textColor="@color/black"
+        android:theme="@style/Widget.Material3.Button.OutlinedButton"
+        android:layout_width="300dp"
+        android:layout_gravity="center"
+        android:gravity="center"
+        app:strokeColor="@color/white"
+        android:layout_marginBottom="@dimen/standard_double_margin"
+        android:layout_height="wrap_content"
+        app:cornerRadius="@dimen/button_corner_radius"
+        android:text="@string/authenticator_activity_cancel_login" />
+
+</LinearLayout>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -334,6 +334,9 @@
     <string name="auth_account_does_not_exist">The account is not added on this device yet</string>
     <string name="auth_access_failed">Access failed: %1$s</string>
 
+    <string name="authenticator_activity_cancel_login">Cancel Login</string>
+    <string name="authenticator_activity_please_complete_login_process">Please complete login process in your browser</string>
+
     <string name="favorite">Add to favorites</string>
     <string name="unset_favorite">Remove from favourites</string>
     <string name="encrypted">Set as encrypted</string>