From 74bedfb56d1fe1f91c4db4501b4dbacc87612ddd Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 2 Apr 2024 18:16:58 +0600 Subject: [PATCH 1/3] feat: enable OIDC auth support if account has authState string in it's userData, try to authenticate using Baerer token instead. This auth flow requires oidc related header to be passed to be succeed. --- library/build.gradle | 4 +- .../com/nextcloud/common/NextcloudClient.kt | 10 +- .../com/nextcloud/common/OkHttpMethodBase.kt | 5 + .../common/OkhttpPersistenceCookieJar.kt | 77 ++++++++++++ .../android/lib/common/BearerCredentials.java | 52 ++++++++ .../lib/common/OwnCloudBearerCredentials.java | 111 ++++++++++++++++++ .../android/lib/common/OwnCloudClient.java | 11 ++ .../lib/common/OwnCloudClientFactory.java | 71 ++++++----- .../lib/common/OwnCloudClientManager.java | 36 ++++-- .../common/OwnCloudCredentialsFactory.java | 4 + .../lib/common/accounts/AccountUtils.java | 108 +++++++++++++++-- .../common/operations/RemoteOperation.java | 1 + 12 files changed, 429 insertions(+), 61 deletions(-) create mode 100644 library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt create mode 100644 library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java create mode 100644 library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java diff --git a/library/build.gradle b/library/build.gradle index b6e9667b6..4c43e418f 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -47,7 +47,7 @@ configurations { def versionMajor = 1 def versionMinor = 0 -def versionPatch = 6 +def versionPatch = 7 def releasePatch = "release" def libName = "Nextcloud-Android-Library" def groupName = "foundation.e" @@ -63,6 +63,8 @@ dependencies { implementation "androidx.core:core-ktx:1.10.1" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'net.openid:appauth:0.11.1' + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0' spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.2' diff --git a/library/src/main/java/com/nextcloud/common/NextcloudClient.kt b/library/src/main/java/com/nextcloud/common/NextcloudClient.kt index 9473604f5..6029fabd0 100644 --- a/library/src/main/java/com/nextcloud/common/NextcloudClient.kt +++ b/library/src/main/java/com/nextcloud/common/NextcloudClient.kt @@ -55,6 +55,7 @@ class NextcloudClient private constructor( val client: OkHttpClient ) : NextcloudUriProvider by delegate { var followRedirects = true + var oidcLoginWithToken = false constructor( baseUri: Uri, @@ -73,7 +74,7 @@ class NextcloudClient private constructor( @JvmStatic val TAG = NextcloudClient::class.java.simpleName - private fun createDefaultClient(context: Context): OkHttpClient { + private fun createDefaultClient(context: Context, cookieJar: CookieJar = CookieJar.NO_COOKIES): OkHttpClient { val trustManager = AdvancedX509TrustManager(NetworkUtils.getKnownServersStore(context)) val sslContext = NetworkUtils.getSSLContext() @@ -82,7 +83,7 @@ class NextcloudClient private constructor( val sslSocketFactory = sslContext.socketFactory return OkHttpClient.Builder() - .cookieJar(CookieJar.NO_COOKIES) + .cookieJar(cookieJar) .connectTimeout(DEFAULT_CONNECTION_TIMEOUT_LONG, TimeUnit.MILLISECONDS) .readTimeout(DEFAULT_DATA_TIMEOUT_LONG, TimeUnit.MILLISECONDS) .callTimeout(DEFAULT_CONNECTION_TIMEOUT_LONG + DEFAULT_DATA_TIMEOUT_LONG, TimeUnit.MILLISECONDS) @@ -97,8 +98,9 @@ class NextcloudClient private constructor( baseUri: Uri, userId: String, credentials: String, - context: Context - ) : this(baseUri, userId, credentials, createDefaultClient(context)) + context: Context, + cookieJar: CookieJar = CookieJar.NO_COOKIES + ) : this(baseUri, userId, credentials, createDefaultClient(context, cookieJar)) @Suppress("TooGenericExceptionCaught") fun execute(remoteOperation: RemoteOperation): RemoteOperationResult { diff --git a/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt b/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt index e3c80f155..adb520135 100644 --- a/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt +++ b/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt @@ -147,6 +147,11 @@ abstract class OkHttpMethodBase( temp.header(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE) } + if (nextcloudClient.oidcLoginWithToken) { + temp.header(RemoteOperation.OIDC_LOGIN_WITH_TOKEN, "true") + temp.header(RemoteOperation.OCS_API_HEADER, "true") + } + applyType(temp) val request = temp.build() diff --git a/library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt b/library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt new file mode 100644 index 000000000..7c49a482b --- /dev/null +++ b/library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt @@ -0,0 +1,77 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.common + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +class OkhttpPersistenceCookieJar(context: Context, private val account: Account) : CookieJar { + + private val accountManager = AccountManager.get(context.applicationContext) + + override fun loadForRequest(url: HttpUrl): List { + return getCookieMap(url).values.filter { + it.matches(url) + } + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val cookieList = cookies.filter { + it.expiresAt > System.currentTimeMillis() + } + + if (cookieList.isEmpty()) { + return + } + + val cookieMap = getCookieMap(url) + + // replace old cookie with new one + cookieList.forEach { + cookieMap[it.name] = it + } + + val cookieString = + cookieMap.values.joinToString(separator = Constants.OKHTTP_COOKIE_SEPARATOR) + accountManager.setUserData(account, Constants.KEY_OKHTTP_COOKIES, cookieString) + } + + private fun getCookieMap(url: HttpUrl): HashMap { + val result = HashMap() + val cookiesString = + accountManager.getUserData(account, Constants.KEY_OKHTTP_COOKIES) ?: return HashMap() + + val cookies = cookiesString.split(Constants.OKHTTP_COOKIE_SEPARATOR.toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + + cookies.forEach { + val cookie = Cookie.parse(url, it) ?: return@forEach + + if (cookie.expiresAt > System.currentTimeMillis()) { + result[cookie.name] = cookie + } + } + + return result + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java b/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java new file mode 100644 index 000000000..7fa431528 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java @@ -0,0 +1,52 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.common; + +import androidx.annotation.NonNull; + +import org.apache.commons.httpclient.Credentials; + +import java.util.Objects; + +public class BearerCredentials implements Credentials { + + private final String accessToken; + + public BearerCredentials(String accessToken) { + super(); + this.accessToken = accessToken; + } + + @NonNull + @Override + public String toString() { + return accessToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BearerCredentials that = (BearerCredentials) o; + return Objects.equals(accessToken, that.accessToken); + } + + @Override + public int hashCode() { + return Objects.hash(accessToken); + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java new file mode 100644 index 000000000..a31f45a20 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java @@ -0,0 +1,111 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import org.apache.commons.httpclient.auth.AuthPolicy; +import org.apache.commons.httpclient.auth.AuthScope; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class OwnCloudBearerCredentials implements OwnCloudCredentials { + + private final String userName; + private final String accessToken; + + public OwnCloudBearerCredentials(String userName, String accessToken) { + this.accessToken = accessToken != null ? accessToken : ""; + this.userName = userName != null ? userName : ""; + } + + @Override + public void applyTo(OwnCloudClient client) { + List authPrefs = new ArrayList<>(1); + authPrefs.add("Bearer"); + + client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + client.getParams().setAuthenticationPreemptive(false); + client.getParams().setCredentialCharset(OwnCloudCredentialsFactory.CREDENTIAL_CHARSET); + client.getState().setCredentials(AuthScope.ANY, new BearerCredentials(accessToken)); + } + + @Override + public String getUsername() { + return userName; + } + + @Override + public String getAuthToken() { + return accessToken; + } + + @Override + public boolean authTokenExpires() { + return false; + } + + @Override + public String toOkHttpCredentials() { + return "Bearer " + accessToken; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(userName); + dest.writeString(accessToken); + } + + protected OwnCloudBearerCredentials(Parcel in) { + userName = in.readString(); + accessToken = in.readString(); + } + + public static final Creator CREATOR = new Creator<>() { + @Override + public OwnCloudBearerCredentials createFromParcel(Parcel source) { + return new OwnCloudBearerCredentials(source); + } + + @Override + public OwnCloudBearerCredentials[] newArray(int size) { + return new OwnCloudBearerCredentials[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OwnCloudBearerCredentials that = (OwnCloudBearerCredentials) o; + return Objects.equals(userName, that.userName) && Objects.equals(accessToken, that.accessToken); + } + + @Override + public int hashCode() { + return Objects.hash(userName, accessToken); + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java index 9ff9891bc..9fea5ed96 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java @@ -31,6 +31,7 @@ import com.nextcloud.common.DNSCache; import com.nextcloud.common.NextcloudUriDelegate; import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.common.network.RedirectionPath; +import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.utils.Log_OC; import org.apache.commons.httpclient.Cookie; @@ -182,6 +183,8 @@ public class OwnCloudClient extends HttpClient { */ @Override public int executeMethod(HttpMethod method) throws IOException { + addBearerCredentialIfRequired(method); + final String hostname = method.getURI().getHost(); try { @@ -232,6 +235,14 @@ public class OwnCloudClient extends HttpClient { } } + private void addBearerCredentialIfRequired(HttpMethod method) { + if (credentials instanceof OwnCloudBearerCredentials) { + method.setRequestHeader("Authorization", "Bearer " + credentials.getAuthToken()); + method.setRequestHeader(RemoteOperation.OIDC_LOGIN_WITH_TOKEN, "true"); + method.setRequestHeader(RemoteOperation.OCS_API_HEADER, "true"); + } + } + /* * if 401 received & cookie is passed on request, there is a chance the invalidated session-cookie is passed. * Retry without passing the cookie. diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java index 760b4f047..a1792a9da 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java @@ -26,18 +26,16 @@ package com.owncloud.android.lib.common; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Context; import android.net.Uri; -import android.os.Bundle; import com.nextcloud.common.NextcloudClient; import com.nextcloud.common.OkHttpCredentialsUtil; +import com.nextcloud.common.OkhttpPersistenceCookieJar; import com.nextcloud.common.User; -import com.owncloud.android.lib.common.accounts.AccountTypeUtils; import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; import com.owncloud.android.lib.common.network.NetworkUtils; @@ -46,6 +44,8 @@ import com.owncloud.android.lib.common.utils.Log_OC; import java.io.IOException; import java.security.GeneralSecurityException; +import okhttp3.CookieJar; + public class OwnCloudClientFactory { final private static String TAG = OwnCloudClientFactory.class.getSimpleName(); @@ -87,10 +87,7 @@ public class OwnCloudClientFactory { OwnCloudClient client = createOwnCloudClient(baseUri, appContext, true); client.setUserId(userId); - String username = AccountUtils.getUsernameForAccount(account); - String password = AccountUtils.getPassword(am, account); - OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBasicCredentials(username, password); - + OwnCloudCredentials credentials = AccountUtils.getCredentialsForAccount(appContext, account); client.setCredentials(credentials); // Restore cookies @@ -111,22 +108,7 @@ public class OwnCloudClientFactory { OwnCloudClient client = createOwnCloudClient(baseUri, appContext, true); client.setUserId(userId); - String username = AccountUtils.getUsernameForAccount(account); - //String password = am.getPassword(account); - //String password = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypePass(), - // false); - AccountManagerFuture future = am.getAuthToken(account, - AccountTypeUtils.getAuthTokenTypePass(account.type), null, - currentActivity, null, null); - - Bundle result = future.getResult(); - String password = result.getString(AccountManager.KEY_AUTHTOKEN); - - if (password == null) { - password = am.getPassword(account); - } - - OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBasicCredentials(username, password); + OwnCloudCredentials credentials = AccountUtils.getCredentialForAccount(appContext, account, currentActivity); client.setCredentials(credentials); @@ -174,7 +156,9 @@ public class OwnCloudClientFactory { String userId, String credentials, Context context, - boolean followRedirects) { + boolean followRedirects, + boolean loginWithToken, + CookieJar cookieJar) { try { NetworkUtils.registerAdvancedSslContext(true, context); } catch (GeneralSecurityException e) { @@ -186,8 +170,9 @@ public class OwnCloudClientFactory { " in the system will be used for HTTPS connections", e); } - NextcloudClient client = new NextcloudClient(uri, userId, credentials, context); + NextcloudClient client = new NextcloudClient(uri, userId, credentials, context, cookieJar); client.setFollowRedirects(followRedirects); + client.setOidcLoginWithToken(loginWithToken); return client; } @@ -214,6 +199,28 @@ public class OwnCloudClientFactory { // TODO avoid calling to getUserData here String userId = AccountUtils.getUpdatedUserId(am, account); String username = AccountUtils.getUsernameForAccount(account); + + CookieJar cookieJar = new OkhttpPersistenceCookieJar(appContext, account); + + if (username == null || username.isEmpty()) { + throw new AccountNotFoundException( + account, + "Username could not be retrieved", + null); + } + + String accessToken = AccountUtils.getAccessToken(am, account); + if (accessToken != null) { + String credential = "Bearer " + accessToken; + return createNextcloudClient(baseUri, + userId, + credential, + appContext, + true, + true, + cookieJar); + } + String password; try { password = AccountUtils.getPassword(am, account); @@ -226,10 +233,10 @@ public class OwnCloudClientFactory { throw new AccountNotFoundException(account, "Error receiving password token", e); } - if (username == null || username.isEmpty() || password.isEmpty()) { + if (password.isEmpty()) { throw new AccountNotFoundException( account, - "Username or password could not be retrieved", + "password could not be retrieved", null); } @@ -238,9 +245,11 @@ public class OwnCloudClientFactory { // AccountUtils.restoreCookies(account, client, appContext); return createNextcloudClient(baseUri, - userId, - OkHttpCredentialsUtil.basic(username, password), - appContext, - true); + userId, + OkHttpCredentialsUtil.basic(username, password), + appContext, + true, + false, + cookieJar); } } diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java index 99a5c79c6..6f6ce82c1 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java @@ -47,7 +47,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** - * * @author David A. Velasco * @author masensio * @author Tobias Kaminsky @@ -198,13 +197,6 @@ public class OwnCloudClientManager { if (client == null) { - - // TODO v2 - //client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); - // enable cookie tracking - - //AccountUtils.restoreCookies(accountName, client, context); - account.loadCredentials(context); String credentials = account.getCredentials().toOkHttpCredentials(); @@ -224,7 +216,9 @@ public class OwnCloudClientManager { userId, credentials, context.getApplicationContext(), - true); + true, + isOIDCLogin(account), + AccountUtils.getOkhttpCookieJar(context, accountName)); if (accountName != null) { clientsNewWithKnownUsername.put(accountName, client); @@ -239,12 +233,13 @@ public class OwnCloudClientManager { } } } else { + if (!reusingKnown && Log.isLoggable(TAG, Log.VERBOSE)) { Log_OC.v(TAG, "reusing client for session " + sessionName); } - // TODO v2 - // keepCredentialsUpdated(account, client); - // keepUriUpdated(account, client); + + keepCredentialsUpdated(context, account, client); + keepUriUpdated(account, client); } if (Log.isLoggable(TAG, Log.DEBUG)) { @@ -254,6 +249,23 @@ public class OwnCloudClientManager { return client; } + private void keepCredentialsUpdated(Context context, OwnCloudAccount account, NextcloudClient client) throws OperationCanceledException, AuthenticatorException, IOException { + account.loadCredentials(context); + client.setCredentials(account.getCredentials().toOkHttpCredentials()); + client.setOidcLoginWithToken(isOIDCLogin(account)); + } + + private void keepUriUpdated(OwnCloudAccount account, NextcloudClient client) { + Uri recentUri = account.getBaseUri(); + if (!recentUri.equals(client.getBaseUri())) { + client.setBaseUri(recentUri); + } + } + + private boolean isOIDCLogin(OwnCloudAccount account) { + return (account.getCredentials() instanceof OwnCloudBearerCredentials); + } + @Nullable public OwnCloudClient removeClientFor(@Nullable OwnCloudAccount account) { diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudCredentialsFactory.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudCredentialsFactory.java index e3904440d..d053d0a4d 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudCredentialsFactory.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudCredentialsFactory.java @@ -33,6 +33,10 @@ public class OwnCloudCredentialsFactory { return new OwnCloudBasicCredentials(username, password); } + public static OwnCloudCredentials newBearerCredentials(String username, String accessToken) { + return new OwnCloudBearerCredentials(username, accessToken); + } + public static final OwnCloudCredentials getAnonymousCredentials() { if (sAnonymousCredentials == null) { sAnonymousCredentials = new OwnCloudAnonymousCredentials(); diff --git a/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java b/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java index cd95db68a..e6952a592 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java +++ b/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java @@ -27,26 +27,35 @@ package com.owncloud.android.lib.common.accounts; import android.accounts.Account; import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; import android.accounts.AccountsException; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; +import android.app.Activity; import android.content.Context; import android.net.Uri; +import android.os.Bundle; import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.nextcloud.common.OkhttpPersistenceCookieJar; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudCredentials; import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.status.OwnCloudVersion; +import net.openid.appauth.AuthState; + import org.apache.commons.httpclient.Cookie; +import org.json.JSONException; import java.io.IOException; +import okhttp3.CookieJar; + public class AccountUtils { private static final String TAG = AccountUtils.class.getSimpleName(); @@ -142,12 +151,62 @@ public class AccountUtils { throws OperationCanceledException, AuthenticatorException, IOException { AccountManager am = AccountManager.get(context); - String username = AccountUtils.getUsernameForAccount(account); + String username = getUsernameForAccount(account); + String accessToken = getAccessToken(am, account); + + if (accessToken != null) { + return OwnCloudCredentialsFactory.newBearerCredentials(username, accessToken); + } + String password = getPassword(am, account); return OwnCloudCredentialsFactory.newBasicCredentials(username, password); } + @NonNull + public static OwnCloudCredentials getCredentialForAccount(@NonNull Context context, @NonNull Account account, @NonNull Activity activity) throws OperationCanceledException, AuthenticatorException, IOException { + AccountManager accountManager = AccountManager.get(context); + + String username = getUsernameForAccount(account); + + String accessToken = getAccessToken(accountManager, account); + if (accessToken != null) { + return OwnCloudCredentialsFactory.newBearerCredentials(username, accessToken); + } + + AccountManagerFuture future = accountManager.getAuthToken(account, + AccountTypeUtils.getAuthTokenTypePass(account.type), null, + activity, null, null); + + Bundle result = future.getResult(); + String password = result.getString(AccountManager.KEY_AUTHTOKEN); + + if (password == null) { + password = accountManager.getPassword(account); + } + + return OwnCloudCredentialsFactory.newBasicCredentials(username, password); + } + + + @Nullable + public static String getAccessToken(@NonNull AccountManager accountManager, @NonNull Account account) { + String authStateString = accountManager.getUserData(account, Constants.KEY_AUTH_STATE); + + if (authStateString == null || authStateString.trim().isBlank()) { + return null; + } + + try { + AuthState authState = AuthState.jsonDeserialize(authStateString); + return authState.getAccessToken(); + } catch (JSONException e) { + Log_OC.e(TAG, e.getMessage()); + } + + return null; + } + @Nullable public static String getPassword(AccountManager accountManager, Account account) { String password = accountManager.getPassword(account); @@ -275,18 +334,7 @@ public class AccountUtils { public static void restoreCookies(String accountName, OwnCloudClient client, Context context) { Log_OC.d(TAG, "Restoring cookies for " + accountName); - // Account Manager - AccountManager am = AccountManager.get(context.getApplicationContext()); - - // Get account - Account account = null; - Account accounts[] = am.getAccounts(); - for (Account a : accounts) { - if (a.name.equals(accountName)) { - account = a; - break; - } - } + Account account = getAccount(context, accountName); // Restoring cookies if (account != null) { @@ -294,6 +342,34 @@ public class AccountUtils { } } + @Nullable + private static Account getAccount(@NonNull Context context, @Nullable String accountName) { + if (accountName == null) { + return null; + } + + AccountManager accountManager = AccountManager.get(context.getApplicationContext()); + + Account[] accounts = accountManager.getAccounts(); + for (Account account : accounts) { + if (account.name.equals(accountName)) { + return account; + } + } + + return null; + } + + @NonNull + public static CookieJar getOkhttpCookieJar(@NonNull Context context, @Nullable String accountName) { + Account account = getAccount(context, accountName); + if (account == null) { + return CookieJar.NO_COOKIES; + } + + return new OkhttpPersistenceCookieJar(context, account); + } + public static class AccountNotFoundException extends AccountsException { /** @@ -402,6 +478,12 @@ public class AccountUtils { * User ID, internally and never changing id of user, used by e.g. dav/trashbin/$userId/trash */ public static final String KEY_USER_ID = "oc_id"; + + public static final String KEY_AUTH_STATE = "auth_state"; + + public static final String KEY_OKHTTP_COOKIES = "cookie_key"; + + public static final String OKHTTP_COOKIE_SEPARATOR = ""; } } diff --git a/library/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java index 1a04b80be..b940244aa 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java @@ -61,6 +61,7 @@ public abstract class RemoteOperation implements Runnable { * OCS API header name */ public static final String OCS_API_HEADER = "OCS-APIREQUEST"; + public static final String OIDC_LOGIN_WITH_TOKEN = "OIDC-LOGIN-WITH-TOKEN"; public static final String OCS_ETAG_HEADER = "If-None-Match"; /** -- GitLab From 803f507147684edc501ab00d78ca9f3badbb30b3 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 2 Apr 2024 22:04:02 +0600 Subject: [PATCH 2/3] chore: update according to review --- .../lib/common/OwnCloudClientFactory.java | 17 +++++------------ .../lib/common/OwnCloudClientManager.java | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java index a1792a9da..9a0af309c 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java @@ -200,7 +200,7 @@ public class OwnCloudClientFactory { String userId = AccountUtils.getUpdatedUserId(am, account); String username = AccountUtils.getUsernameForAccount(account); - CookieJar cookieJar = new OkhttpPersistenceCookieJar(appContext, account); + final CookieJar cookieJar = new OkhttpPersistenceCookieJar(appContext, account); if (username == null || username.isEmpty()) { throw new AccountNotFoundException( @@ -209,7 +209,7 @@ public class OwnCloudClientFactory { null); } - String accessToken = AccountUtils.getAccessToken(am, account); + final String accessToken = AccountUtils.getAccessToken(am, account); if (accessToken != null) { String credential = "Bearer " + accessToken; return createNextcloudClient(baseUri, @@ -224,22 +224,15 @@ public class OwnCloudClientFactory { String password; try { password = AccountUtils.getPassword(am, account); - if (password == null) { - Log_OC.e(TAG, "Error receiving password token (password==null)"); - throw new AccountNotFoundException(account, "Error receiving password token (password==null)", null); + if (password == null || password.isEmpty()) { + Log_OC.e(TAG, "Error receiving password token (password==null || empty)"); + throw new AccountNotFoundException(account, "Error receiving password token (password==null || empty)", null); } } catch (Exception e) { Log_OC.e(TAG, "Error receiving password token", e); throw new AccountNotFoundException(account, "Error receiving password token", e); } - if (password.isEmpty()) { - throw new AccountNotFoundException( - account, - "password could not be retrieved", - null); - } - // Restore cookies // TODO v2 cookie handling // AccountUtils.restoreCookies(account, client, appContext); diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java index 6f6ce82c1..1a5f77372 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java @@ -256,7 +256,7 @@ public class OwnCloudClientManager { } private void keepUriUpdated(OwnCloudAccount account, NextcloudClient client) { - Uri recentUri = account.getBaseUri(); + final Uri recentUri = account.getBaseUri(); if (!recentUri.equals(client.getBaseUri())) { client.setBaseUri(recentUri); } -- GitLab From 3fb0eea3c95845e09725d3f6098d5da5ab2b0584 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Wed, 3 Apr 2024 13:24:09 +0600 Subject: [PATCH 3/3] chore: update according to review --- ...rsistenceCookieJar.kt => OkHttpPersistentCookieJar.kt} | 5 +++-- .../owncloud/android/lib/common/BearerCredentials.java | 8 ++++---- .../android/lib/common/OwnCloudBearerCredentials.java | 8 ++++---- .../android/lib/common/OwnCloudClientFactory.java | 6 +++--- .../android/lib/common/OwnCloudClientManager.java | 7 ++++--- .../android/lib/common/accounts/AccountUtils.java | 4 ++-- 6 files changed, 20 insertions(+), 18 deletions(-) rename library/src/main/java/com/nextcloud/common/{OkhttpPersistenceCookieJar.kt => OkHttpPersistentCookieJar.kt} (93%) diff --git a/library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt b/library/src/main/java/com/nextcloud/common/OkHttpPersistentCookieJar.kt similarity index 93% rename from library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt rename to library/src/main/java/com/nextcloud/common/OkHttpPersistentCookieJar.kt index 7c49a482b..dfb25ce4a 100644 --- a/library/src/main/java/com/nextcloud/common/OkhttpPersistenceCookieJar.kt +++ b/library/src/main/java/com/nextcloud/common/OkHttpPersistentCookieJar.kt @@ -24,9 +24,9 @@ import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl -class OkhttpPersistenceCookieJar(context: Context, private val account: Account) : CookieJar { +class OkHttpPersistentCookieJar(context: Context, private val account: Account) : CookieJar { - private val accountManager = AccountManager.get(context.applicationContext) + private val accountManager = AccountManager.get(context) override fun loadForRequest(url: HttpUrl): List { return getCookieMap(url).values.filter { @@ -52,6 +52,7 @@ class OkhttpPersistenceCookieJar(context: Context, private val account: Account) val cookieString = cookieMap.values.joinToString(separator = Constants.OKHTTP_COOKIE_SEPARATOR) + accountManager.setUserData(account, Constants.KEY_OKHTTP_COOKIES, cookieString) } diff --git a/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java b/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java index 7fa431528..d1e1a071e 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java +++ b/library/src/main/java/com/owncloud/android/lib/common/BearerCredentials.java @@ -38,10 +38,10 @@ public class BearerCredentials implements Credentials { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BearerCredentials that = (BearerCredentials) o; + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + BearerCredentials that = (BearerCredentials) object; return Objects.equals(accessToken, that.accessToken); } diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java index a31f45a20..0a55b650f 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudBearerCredentials.java @@ -97,10 +97,10 @@ public class OwnCloudBearerCredentials implements OwnCloudCredentials { }; @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OwnCloudBearerCredentials that = (OwnCloudBearerCredentials) o; + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + OwnCloudBearerCredentials that = (OwnCloudBearerCredentials) object; return Objects.equals(userName, that.userName) && Objects.equals(accessToken, that.accessToken); } diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java index 9a0af309c..bb4cecdb0 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java @@ -34,7 +34,7 @@ import android.net.Uri; import com.nextcloud.common.NextcloudClient; import com.nextcloud.common.OkHttpCredentialsUtil; -import com.nextcloud.common.OkhttpPersistenceCookieJar; +import com.nextcloud.common.OkHttpPersistentCookieJar; import com.nextcloud.common.User; import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; @@ -200,8 +200,6 @@ public class OwnCloudClientFactory { String userId = AccountUtils.getUpdatedUserId(am, account); String username = AccountUtils.getUsernameForAccount(account); - final CookieJar cookieJar = new OkhttpPersistenceCookieJar(appContext, account); - if (username == null || username.isEmpty()) { throw new AccountNotFoundException( account, @@ -209,6 +207,8 @@ public class OwnCloudClientFactory { null); } + final CookieJar cookieJar = new OkHttpPersistentCookieJar(appContext, account); + final String accessToken = AccountUtils.getAccessToken(am, account); if (accessToken != null) { String credential = "Bearer " + accessToken; diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java index 1a5f77372..3344e14e5 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManager.java @@ -32,6 +32,7 @@ import android.content.Context; import android.net.Uri; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.nextcloud.common.NextcloudClient; @@ -249,20 +250,20 @@ public class OwnCloudClientManager { return client; } - private void keepCredentialsUpdated(Context context, OwnCloudAccount account, NextcloudClient client) throws OperationCanceledException, AuthenticatorException, IOException { + private void keepCredentialsUpdated(@NonNull Context context, @NonNull OwnCloudAccount account, @NonNull NextcloudClient client) throws OperationCanceledException, AuthenticatorException, IOException { account.loadCredentials(context); client.setCredentials(account.getCredentials().toOkHttpCredentials()); client.setOidcLoginWithToken(isOIDCLogin(account)); } - private void keepUriUpdated(OwnCloudAccount account, NextcloudClient client) { + private void keepUriUpdated(@NonNull OwnCloudAccount account, @NonNull NextcloudClient client) { final Uri recentUri = account.getBaseUri(); if (!recentUri.equals(client.getBaseUri())) { client.setBaseUri(recentUri); } } - private boolean isOIDCLogin(OwnCloudAccount account) { + private boolean isOIDCLogin(@NonNull OwnCloudAccount account) { return (account.getCredentials() instanceof OwnCloudBearerCredentials); } diff --git a/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java b/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java index e6952a592..4a4f64c0e 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java +++ b/library/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java @@ -40,7 +40,7 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.nextcloud.common.OkhttpPersistenceCookieJar; +import com.nextcloud.common.OkHttpPersistentCookieJar; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudCredentials; import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; @@ -367,7 +367,7 @@ public class AccountUtils { return CookieJar.NO_COOKIES; } - return new OkhttpPersistenceCookieJar(context, account); + return new OkHttpPersistentCookieJar(context.getApplicationContext(), account); } public static class AccountNotFoundException extends AccountsException { -- GitLab