diff --git a/.github/workflows/build-tester-apk.yml b/.github/workflows/build-tester-apk.yml index 2c40ee95a..0508eae3f 100644 --- a/.github/workflows/build-tester-apk.yml +++ b/.github/workflows/build-tester-apk.yml @@ -45,6 +45,7 @@ jobs: COMMIT_HASH=$(git rev-parse HEAD) echo "Commit hash: $COMMIT_HASH" echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT + echo "commit_hash=$COMMIT_HASH" >> "$GITHUB_ENV" - name: Setup JDK uses: actions/setup-java@v5 @@ -99,6 +100,9 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: build-tester-apk-${{ github.run_number }} + target_commitish: ${{ env.commit_hash }} name: build-tester-apk build ${{ github.run_number }} files: ${{ steps.set_artifact.outputs.artifact_name }} prerelease: true + generate_release_notes: false + body: Built from ${{ env.commit_hash }} diff --git a/opencloudApp/build.gradle b/opencloudApp/build.gradle index 28332662a..24fd7d582 100644 --- a/opencloudApp/build.gradle +++ b/opencloudApp/build.gradle @@ -100,7 +100,7 @@ android { testInstrumentationRunner "eu.opencloud.android.utils.OCTestAndroidJUnitRunner" - versionCode = 4 + versionCode = 5 versionName = "1.2.0" buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\"" diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java index 213ab40b1..8f408e6ea 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AccountAuthenticator.java @@ -55,6 +55,7 @@ import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_CLIENT_REGISTRATION_CLIENT_SECRET; import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_OAUTH2_REFRESH_TOKEN; import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_OAUTH2_SCOPE; +import static eu.opencloud.android.data.authentication.AuthenticationConstantsKt.KEY_OIDC_ISSUER; import static eu.opencloud.android.presentation.authentication.AuthenticatorConstants.KEY_AUTH_TOKEN_TYPE; import static org.koin.java.KoinJavaComponent.inject; @@ -333,9 +334,13 @@ private String refreshToken( String baseUrl = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL); - // OIDC Discovery + // OIDC Discovery: prefer the stored webfinger issuer (points to the real IDP), + // fall back to baseUrl for accounts created before webfinger support. + String oidcIssuer = accountManager.getUserData(account, KEY_OIDC_ISSUER); + String discoveryUrl = (oidcIssuer != null) ? oidcIssuer : baseUrl; + @NotNull Lazy oidcDiscoveryUseCase = inject(OIDCDiscoveryUseCase.class); - OIDCDiscoveryUseCase.Params oidcDiscoveryUseCaseParams = new OIDCDiscoveryUseCase.Params(baseUrl); + OIDCDiscoveryUseCase.Params oidcDiscoveryUseCaseParams = new OIDCDiscoveryUseCase.Params(discoveryUrl); UseCaseResult oidcServerConfigurationUseCaseResult = oidcDiscoveryUseCase.getValue().invoke(oidcDiscoveryUseCaseParams); diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt index da7c8cebe..200dfb603 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt @@ -48,6 +48,7 @@ import androidx.core.widget.doAfterTextChanged import eu.opencloud.android.BuildConfig import eu.opencloud.android.MainApp.Companion.accountType import eu.opencloud.android.R +import eu.opencloud.android.data.authentication.KEY_OIDC_ISSUER import eu.opencloud.android.data.authentication.KEY_PREFERRED_USERNAME import eu.opencloud.android.data.authentication.KEY_USER_ID import eu.opencloud.android.databinding.AccountSetupBinding @@ -59,7 +60,6 @@ import eu.opencloud.android.domain.exceptions.OpencloudVersionNotSupportedExcept import eu.opencloud.android.domain.exceptions.SSLErrorCode import eu.opencloud.android.domain.exceptions.SSLErrorException import eu.opencloud.android.domain.exceptions.ServerNotReachableException -import eu.opencloud.android.domain.exceptions.StateMismatchException import eu.opencloud.android.domain.exceptions.UnauthorizedException import eu.opencloud.android.domain.server.model.ServerInfo import eu.opencloud.android.extensions.checkPasscodeEnforced @@ -312,7 +312,13 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted authenticationViewModel.accountDiscovery.observe(this) { if (it.peekContent() is UIResult.Success) { notifyDocumentsProviderRoots(applicationContext) - launchFileDisplayActivity() + if (authenticationViewModel.launchedFromDeepLink || + (isTaskRoot && accountAuthenticatorResponse == null) + ) { + launchFileDisplayActivity() + } else { + finish() + } } else { binding.authStatusText.run { text = context.getString(R.string.login_account_preparing) @@ -559,10 +565,17 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted resultBundle = intent.extras setResult(Activity.RESULT_OK, intent) + val account = Account(accountName, contextProvider.getString(R.string.account_type)) + val am = AccountManager.get(this) + // Store preferred_username from id_token for login_hint on re-login - preferredUsername?.let { prefUsername -> - val account = Account(accountName, contextProvider.getString(R.string.account_type)) - AccountManager.get(this).setUserData(account, KEY_PREFERRED_USERNAME, prefUsername) + preferredUsername?.let { am.setUserData(account, KEY_PREFERRED_USERNAME, it) } + + // Store OIDC issuer from webfinger so AccountAuthenticator can do OIDC discovery + // against the correct IDP (not the cloud server which may proxy stale config) + val serverInfo = authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData() + if (serverInfo is ServerInfo.OIDCServer) { + am.setUserData(account, KEY_OIDC_ISSUER, serverInfo.oidcServerConfiguration.issuer) } authenticationViewModel.discoverAccount(accountName = accountName, discoveryNeeded = loginAction == ACTION_CREATE) @@ -686,6 +699,10 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted super.onNewIntent(intent) intent?.let { Timber.d("onNewIntent received with data: ${it.data}") + if (!::binding.isInitialized) { + Timber.w("onNewIntent received before binding initialized, ignoring OAuth response") + return + } setIntent(it) handleGetAuthorizationCodeResponse(it) } @@ -696,8 +713,9 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted val state = intent.data?.getQueryParameter("state") if (state != authenticationViewModel.oidcState) { - Timber.e("OAuth request to get authorization code failed. State mismatching, maybe somebody is trying a CSRF attack.") - updateOAuthStatusIconAndText(StateMismatchException()) + Timber.e("OAuth: state mismatch (expected=${authenticationViewModel.oidcState}, got=$state). Finishing.") + showMessageInSnackbar(message = getString(R.string.auth_oauth_error)) + finish() } else { if (authorizationCode != null) { Timber.d("Authorization code received [$authorizationCode]. Let's exchange it for access token") @@ -996,6 +1014,10 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted } private fun updateOAuthStatusIconAndText(authorizationException: Throwable?) { + if (!::binding.isInitialized) { + Timber.w("updateOAuthStatusIconAndText called before binding initialized, ignoring") + return + } binding.serverStatusText.run { setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) text = diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/thumbnails/ThumbnailsRequester.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/thumbnails/ThumbnailsRequester.kt index 46b12111a..ea3dde5f6 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/thumbnails/ThumbnailsRequester.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/thumbnails/ThumbnailsRequester.kt @@ -188,8 +188,16 @@ object ThumbnailsRequester : KoinComponent { override fun intercept(chain: Interceptor.Chain): Response { val openCloudClient = clientManager.getClientForCoilThumbnails(accountName) + val credentials = openCloudClient.credentials + ?: return Response.Builder() + .request(chain.request()) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(401) + .message("No credentials available") + .body(okhttp3.ResponseBody.create(null, "")) + .build() val requestHeaders = hashMapOf( - AUTHORIZATION_HEADER to openCloudClient.credentials.headerAuth, + AUTHORIZATION_HEADER to credentials.headerAuth, ACCEPT_ENCODING_HEADER to ACCEPT_ENCODING_IDENTITY, USER_AGENT_HEADER to SingleSessionManager.getUserAgent(), OC_X_REQUEST_ID to RandomUtils.generateRandomUUID(), diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt b/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt index 1700dc134..12c60e908 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/authentication/AuthenticationConstants.kt @@ -52,6 +52,11 @@ const val KEY_FEATURE_SPACES = "KEY_FEATURE_SPACES" */ const val KEY_PREFERRED_USERNAME = "oc_preferred_username" +/** + * OIDC issuer URL from webfinger, used for token refresh OIDC discovery + */ +const val KEY_OIDC_ISSUER = "oc_oidc_issuer" + const val KEY_CLIENT_REGISTRATION_CLIENT_ID = "client_id" const val KEY_CLIENT_REGISTRATION_CLIENT_SECRET = "client_secret" const val KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE = "client_secret_expires_at"