Browse Source

Merge pull request #3027 from nextcloud/sso

add own sso token manager with custom dialog
Tobias Kaminsky 6 years ago
parent
commit
82ea0bcd68

+ 1 - 0
build.gradle

@@ -213,6 +213,7 @@ dependencies {
     implementation 'com.android.support:multidex:1.0.3'
 //    implementation project('nextcloud-android-library')
     genericImplementation "com.github.nextcloud:android-library:master-SNAPSHOT"
+    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
     gplayImplementation "com.github.nextcloud:android-library:master-SNAPSHOT"
     versionDevImplementation 'com.github.nextcloud:android-library:master-SNAPSHOT' // use always latest master
     implementation "com.android.support:support-v4:${supportLibraryVersion}"

+ 4 - 0
src/main/AndroidManifest.xml

@@ -304,6 +304,10 @@
             android:exported="true" >
         </service>
 
+        <activity
+            android:name=".ui.activity.SsoGrantPermissionActivity"
+            android:exported="true"
+            android:theme="@style/Theme.ownCloud.Dialog.NoTitle" />
     </application>
 
 </manifest>

+ 14 - 8
src/main/java/com/nextcloud/android/sso/Constants.java

@@ -3,14 +3,20 @@ package com.nextcloud.android.sso;
 public class Constants {
 
     // Authenticator related constants
-    public final static String SSO_USERNAME = "username";
-    public final static String SSO_TOKEN = "token";
-    public final static String SSO_SERVER_URL = "server_url";
+    public static final String SSO_USERNAME = "username";
+    public static final String SSO_TOKEN = "token";
+    public static final String SSO_SERVER_URL = "server_url";
+    public static final String SSO_SHARED_PREFERENCE = "single-sign-on";
+    public static final String NEXTCLOUD_SSO_EXCEPTION = "NextcloudSsoException";
+    public static final String NEXTCLOUD_SSO = "NextcloudSSO";
+    public static final String NEXTCLOUD_FILES_ACCOUNT = "NextcloudFilesAccount";
+
 
     // Custom Exceptions
-    static final String EXCEPTION_INVALID_TOKEN = "CE_1";
-    static final String EXCEPTION_ACCOUNT_NOT_FOUND = "CE_2";
-    static final String EXCEPTION_UNSUPPORTED_METHOD = "CE_3";
-    static final String EXCEPTION_INVALID_REQUEST_URL = "CE_4";
-    static final String EXCEPTION_HTTP_REQUEST_FAILED = "CE_5";
+    public static final String EXCEPTION_INVALID_TOKEN = "CE_1";
+    public static final String EXCEPTION_ACCOUNT_NOT_FOUND = "CE_2";
+    public static final String EXCEPTION_UNSUPPORTED_METHOD = "CE_3";
+    public static final String EXCEPTION_INVALID_REQUEST_URL = "CE_4";
+    public static final String EXCEPTION_HTTP_REQUEST_FAILED = "CE_5";
+    public static final String EXCEPTION_ACCOUNT_ACCESS_DECLINED = "CE_6";
 }

+ 2 - 2
src/main/java/com/nextcloud/android/sso/InputStreamBinder.java

@@ -33,7 +33,6 @@ import android.util.Log;
 import com.nextcloud.android.sso.aidl.IInputStreamService;
 import com.nextcloud.android.sso.aidl.NextcloudRequest;
 import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil;
-import com.owncloud.android.authentication.AccountAuthenticator;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
@@ -64,6 +63,7 @@ import static com.nextcloud.android.sso.Constants.EXCEPTION_HTTP_REQUEST_FAILED;
 import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_REQUEST_URL;
 import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_TOKEN;
 import static com.nextcloud.android.sso.Constants.EXCEPTION_UNSUPPORTED_METHOD;
+import static com.nextcloud.android.sso.Constants.SSO_SHARED_PREFERENCE;
 
 
 /**
@@ -220,7 +220,7 @@ public class InputStreamBinder extends IInputStreamService.Stub {
     private boolean isValid(NextcloudRequest request) {
         String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());
 
-        SharedPreferences sharedPreferences = context.getSharedPreferences(AccountAuthenticator.SSO_SHARED_PREFERENCE,
+        SharedPreferences sharedPreferences = context.getSharedPreferences(SSO_SHARED_PREFERENCE,
                 Context.MODE_PRIVATE);
         String hash = sharedPreferences.getString(callingPackageName, "");
         return validateToken(hash, request.getToken());

+ 1 - 56
src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java

@@ -28,21 +28,14 @@ import android.accounts.AccountManager;
 import android.accounts.NetworkErrorException;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
 import android.widget.Toast;
 
-import com.nextcloud.android.sso.Constants;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
-import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.accounts.AccountTypeUtils;
-import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.utils.EncryptionUtils;
-
-import java.util.UUID;
 
 
 /**
@@ -64,10 +57,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
     public static final String KEY_REQUIRED_FEATURES = "requiredFeatures";
     public static final String KEY_LOGIN_OPTIONS = "loginOptions";
     public static final String KEY_ACCOUNT = "account";
-    public static final String SSO_SHARED_PREFERENCE = "sso";
-    
-    private static final String NEXTCLOUD_SSO = "NextcloudSSO";
-    
+
     private static final String TAG = AccountAuthenticator.class.getSimpleName();
     
     private Context mContext;
@@ -162,51 +152,6 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
     @Override
     public Bundle getAuthToken(AccountAuthenticatorResponse response,
                                Account account, String authTokenType, Bundle options) {
-
-        if (NEXTCLOUD_SSO.equals(authTokenType)) {
-            final Bundle result = new Bundle();
-
-            String packageName = options.getString("androidPackageName");
-
-            if (packageName == null) {
-                Log_OC.e(TAG, "No calling package, exit.");
-                return result;
-            }
-
-            // create token
-            SharedPreferences sharedPreferences = mContext.getSharedPreferences(SSO_SHARED_PREFERENCE,
-                    Context.MODE_PRIVATE);
-            String token = UUID.randomUUID().toString().replaceAll("-", "");
-
-            String hashedTokenWithSalt = EncryptionUtils.generateSHA512(token);
-
-            SharedPreferences.Editor editor = sharedPreferences.edit();
-            editor.putString(packageName, hashedTokenWithSalt);
-            editor.apply();
-                        
-            String serverUrl;
-            String userId;
-            try {
-                OwnCloudAccount ocAccount = new OwnCloudAccount(account, mContext);
-                serverUrl = ocAccount.getBaseUri().toString();
-                AccountManager accountManager = AccountManager.get(mContext);
-                userId = accountManager.getUserData(account,
-                        com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
-            } catch (AccountUtils.AccountNotFoundException e) {
-                Log_OC.e(TAG, "Account not found");
-                return new Bundle();
-            }
-
-            result.putString(AccountManager.KEY_ACCOUNT_NAME,  account.name);
-            result.putString(AccountManager.KEY_ACCOUNT_TYPE,  MainApp.getAccountType(mContext));
-            result.putString(AccountManager.KEY_AUTHTOKEN,     NEXTCLOUD_SSO);
-            result.putString(Constants.SSO_USERNAME,   userId);
-            result.putString(Constants.SSO_TOKEN,      token);
-            result.putString(Constants.SSO_SERVER_URL, serverUrl);
-
-            return result;
-        }
-
         // validate parameters
         try {
             validateAccountType(account.type);

+ 213 - 0
src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java

@@ -0,0 +1,213 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author David Luhmer
+ * @author Andy Scherzinger
+ * Copyright (C) 2018 David Luhmer
+ * Copyright (C) 2018 Andy Scherzinger
+ *
+ * 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.ui.activity;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.nextcloud.android.sso.Constants;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.util.UUID;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import butterknife.Unbinder;
+
+import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED;
+import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_NOT_FOUND;
+import static com.nextcloud.android.sso.Constants.NEXTCLOUD_FILES_ACCOUNT;
+import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO;
+import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO_EXCEPTION;
+import static com.nextcloud.android.sso.Constants.SSO_SHARED_PREFERENCE;
+
+/**
+ * Activity for granting access rights to a Nextcloud account, used for SSO.
+ */
+public class SsoGrantPermissionActivity extends BaseActivity {
+
+    private static final String TAG = SsoGrantPermissionActivity.class.getCanonicalName();
+
+    private String packageName;
+    private Account account;
+
+    private Unbinder unbinder;
+
+    @BindView(R.id.appIcon)
+    ImageView appIcon;
+
+    @BindView(R.id.permissionText)
+    TextView permissionText;
+
+    @BindView(R.id.btnGrant)
+    Button grantPermissionButton;
+
+    @BindView(R.id.btnDecline)
+    Button declinePermissionButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_sso_grant_permission);
+
+        unbinder = ButterKnife.bind(this);
+
+        ComponentName callingActivity = getCallingActivity();
+
+        if (callingActivity != null) {
+            packageName = callingActivity.getPackageName();
+            String appName = getAppNameForPackage(packageName);
+            account = getIntent().getParcelableExtra(NEXTCLOUD_FILES_ACCOUNT);
+            permissionText.setText(makeSpecialPartsBold(
+                    getString(R.string.single_sign_on_request_token, appName, account.name),
+                    appName,
+                    account.name)
+            );
+            Log.v(TAG, "TOKEN-REQUEST: Calling Package: " + packageName);
+            Log.v(TAG, "TOKEN-REQUEST: App Name: " + appName);
+        } else {
+            // Activity was not started using startActivityForResult!
+            Log.e(TAG, "Calling Package is null");
+            setResultAndExit("Request was not executed properly. Use startActivityForResult()");
+        }
+
+        try {
+            if (packageName != null) {
+                Drawable appIcon = getPackageManager().getApplicationIcon(packageName);
+                this.appIcon.setImageDrawable(appIcon);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, e.getMessage());
+        }
+
+        int primaryColor = ThemeUtils.primaryColor(this, true);
+        grantPermissionButton.setTextColor(primaryColor);
+        declinePermissionButton.setTextColor(primaryColor);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        unbinder.unbind();
+    }
+
+    private SpannableStringBuilder makeSpecialPartsBold(String text, String... toBeStyledText) {
+        SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+        for (String textBlock : toBeStyledText) {
+            int start = text.indexOf(textBlock);
+            int end = start + textBlock.length();
+            ssb.setSpan(new StyleSpan(Typeface.BOLD), start, end, 0);
+            ssb.setSpan(new ForegroundColorSpan(Color.BLACK), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+
+        return ssb;
+    }
+
+    private void setResultAndExit(String exception) {
+        Intent data = new Intent();
+        data.putExtra(NEXTCLOUD_SSO_EXCEPTION, exception);
+        setResult(RESULT_CANCELED, data);
+        finish();
+    }
+
+    private String getAppNameForPackage(String pkg) {
+        final PackageManager pm = getApplicationContext().getPackageManager();
+        ApplicationInfo ai = null;
+        try {
+            ai = pm.getApplicationInfo(pkg, 0);
+        } catch (final PackageManager.NameNotFoundException e) {
+            Log.e(TAG, e.getMessage());
+        }
+        return (String) (ai != null ? pm.getApplicationLabel(ai) : "(unknown)");
+    }
+
+    @OnClick(R.id.btnDecline)
+    void exitFailed() {
+        setResultAndExit(EXCEPTION_ACCOUNT_ACCESS_DECLINED);
+    }
+
+    @OnClick(R.id.btnGrant)
+    void grantPermission() {
+        // create token
+        SharedPreferences sharedPreferences = getSharedPreferences(SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE);
+        String token = UUID.randomUUID().toString().replaceAll("-", "");
+
+        String hashedTokenWithSalt = EncryptionUtils.generateSHA512(token);
+
+        SharedPreferences.Editor editor = sharedPreferences.edit();
+        editor.putString(packageName, hashedTokenWithSalt);
+        editor.apply();
+
+        String serverUrl;
+        String userId;
+        try {
+            OwnCloudAccount ocAccount = new OwnCloudAccount(account, this);
+            serverUrl = ocAccount.getBaseUri().toString();
+            AccountManager accountManager = AccountManager.get(this);
+            userId = accountManager.getUserData(account,
+                    com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
+        } catch (AccountUtils.AccountNotFoundException e) {
+            Log_OC.e(TAG, "Account not found");
+            setResultAndExit(EXCEPTION_ACCOUNT_NOT_FOUND);
+            return;
+        }
+
+        final Bundle result = new Bundle();
+        result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+        result.putString(AccountManager.KEY_ACCOUNT_TYPE, MainApp.getAccountType(this));
+        result.putString(AccountManager.KEY_AUTHTOKEN, NEXTCLOUD_SSO);
+        result.putString(Constants.SSO_USERNAME, userId);
+        result.putString(Constants.SSO_TOKEN, token);
+        result.putString(Constants.SSO_SERVER_URL, serverUrl);
+
+        Intent data = new Intent();
+        data.putExtra(NEXTCLOUD_SSO, result);
+        setResult(RESULT_OK, data);
+        finish();
+    }
+}

+ 70 - 0
src/main/res/layout/activity_sso_grant_permission.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Nextcloud Android client application
+
+  Copyright (C) 2018 Andy Scherzinger
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or 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/>.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_margin="@dimen/standard_margin"
+    tools:context="com.owncloud.android.ui.activity.SsoGrantPermissionActivity">
+
+    <ImageView
+        android:id="@+id/appIcon"
+        android:layout_width="@dimen/user_icon_size"
+        android:layout_height="@dimen/user_icon_size"
+        android:layout_gravity="top|start"
+        android:contentDescription="@null"
+        android:src="@drawable/background"
+        app:layout_constraintTop_toTopOf="@+id/permissionText" />
+
+    <TextView
+        android:id="@+id/permissionText"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingEnd="@dimen/zero"
+        android:paddingLeft="@dimen/permission_dialog_text_padding"
+        android:paddingRight="@dimen/zero"
+        android:paddingStart="@dimen/permission_dialog_text_padding"
+        android:paddingBottom="@dimen/standard_padding"
+        android:textSize="@dimen/permission_dialog_text_size"
+        app:layout_constraintBottom_toTopOf="@+id/btnGrant"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Grant Nextcloud News access to your Nextcloud account incrediblyLong_username_with_123456789_number@Nextcloud_dummy.com?" />
+
+    <android.support.v7.widget.AppCompatButton
+        android:id="@+id/btnGrant"
+        style="@style/Button.Borderless"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/permission_allow"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <android.support.v7.widget.AppCompatButton
+        android:id="@+id/btnDecline"
+        style="@style/Button.Borderless"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/standard_half_margin"
+        android:layout_marginRight="@dimen/standard_half_margin"
+        android:text="@string/permission_deny"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/btnGrant" />
+
+</android.support.constraint.ConstraintLayout>

+ 2 - 0
src/main/res/values/dims.xml

@@ -147,5 +147,7 @@
     <dimen name="synced_folders_recycler_view_layout_margin">-3dp</dimen>
     <dimen name="toolbar_user_information_layout_margin">12dp</dimen>
     <dimen name="bottom_sheet_text_size">16sp</dimen>
+    <dimen name="permission_dialog_text_size">18sp</dimen>
+    <dimen name="permission_dialog_text_padding">56dp</dimen>
     <integer name="media_grid_width">4</integer>
 </resources>

+ 5 - 0
src/main/res/values/strings.xml

@@ -817,4 +817,9 @@
     <string name="file_rename">Rename</string>
     <string name="fab_label">Add or upload</string>
     <string name="account_creation_failed">Account creation failed</string>
+
+    <string name="single_sign_on_request_token" formatted="true">Allow %1$s to access to your Nextcloud account %2$s?</string>
+    <string name="permission_deny">Deny</string>
+    <string name="permission_allow">Allow</string>
+
 </resources>