From c74aa12893733d5f36079eaefae9d4a44e5dedb4 Mon Sep 17 00:00:00 2001 From: v1rusnl <18641204+v1rusnl@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:36:32 +0200 Subject: [PATCH 1/2] Request login consent when adding an OIDC account Request login consent when adding an OIDC account with the offline_access scope, ensuring consent is requested while preserving reauthentication. Other login flows, configured prompts, PKCE, and token handling remain unchanged. Adds three passing tests covering prompt selection and URL encoding. Fixes https://github.com/opencloud-eu/android/issues/243 --- .../authentication/LoginActivity.kt | 2 +- .../authentication/oauth/OAuthUtils.kt | 4 ++ .../authentication/oauth/OAuthPromptTest.kt | 53 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt 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 7583769712..310d9a6dc6 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 @@ -885,7 +885,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted clientId = clientId, responseType = ResponseType.CODE.string, scope = scope, - prompt = if (!oidcSupported) "" else if (loginAction == ACTION_CREATE) "login" + prompt = if (!oidcSupported) "" else if (loginAction == ACTION_CREATE) OAuthUtils.getCreateAccountPrompt(scope) else mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_PROMPT, R.string.oauth2_openid_prompt), codeChallenge = authenticationViewModel.codeChallenge, state = authenticationViewModel.oidcState, diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt index 393a4ec204..0a2bad55d7 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt @@ -96,6 +96,10 @@ class OAuthUtils { return "Basic " + Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP) } + fun getCreateAccountPrompt(scope: String): String = + // Request offline consent while still requiring fresh authentication. + if ("offline_access" in scope.split(' ')) "login consent" else "login" + fun buildAuthorizationRequest( authorizationEndpoint: Uri, redirectUri: String, diff --git a/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt new file mode 100644 index 0000000000..809b311003 --- /dev/null +++ b/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt @@ -0,0 +1,53 @@ +package eu.opencloud.android.presentation.authentication.oauth + +import android.net.Uri +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [28]) +class OAuthPromptTest { + @Test + fun offlineAccessRequiresLoginAndConsent() { + assertEquals("login consent", OAuthUtils.getCreateAccountPrompt("openid profile email groups offline_access")) + assertEquals("login consent", OAuthUtils.getCreateAccountPrompt(" offline_access openid ")) + } + + @Test + fun otherScopesKeepLogin() { + for (scope in listOf("", "openid profile email", "openid not_offline_access", "offline_access_extra")) { + assertEquals("login", OAuthUtils.getCreateAccountPrompt(scope)) + } + } + + @Test + fun authorizationRequestEncodesOnePromptAndPreservesPkceAndRedirect() { + val scope = "openid profile email groups offline_access" + val uri = OAuthUtils.buildAuthorizationRequest( + authorizationEndpoint = Uri.parse("https://idp.example/authorize?existing=value"), + redirectUri = "oc://android.opencloud.eu", + clientId = "OpenCloudAndroid", + responseType = "code", + scope = scope, + prompt = OAuthUtils.getCreateAccountPrompt(scope), + codeChallenge = "test-challenge", + state = "test-state", + username = null, + sendLoginHintAndUser = false, + ) + assertEquals(listOf("login consent"), uri.getQueryParameters("prompt")) + assertTrue(uri.encodedQuery!!.contains("prompt=login%20consent")) + assertEquals("oc://android.opencloud.eu", uri.getQueryParameter("redirect_uri")) + assertEquals("OpenCloudAndroid", uri.getQueryParameter("client_id")) + assertEquals(scope, uri.getQueryParameter("scope")) + assertEquals("S256", uri.getQueryParameter("code_challenge_method")) + assertEquals("test-challenge", uri.getQueryParameter("code_challenge")) + assertEquals("test-state", uri.getQueryParameter("state")) + assertEquals("value", uri.getQueryParameter("existing")) + println("Authorization request under test: $uri") + } +} From 904fd03eaf1d85f11b048d510d7af5c8f04661f6 Mon Sep 17 00:00:00 2001 From: v1rusnl <18641204+v1rusnl@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:52:45 +0200 Subject: [PATCH 2/2] Change "prompt=login consent" to "prompt=consent" --- .../presentation/authentication/oauth/OAuthUtils.kt | 4 ++-- .../authentication/oauth/OAuthPromptTest.kt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt index 0a2bad55d7..db4d7f1814 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/oauth/OAuthUtils.kt @@ -97,8 +97,8 @@ class OAuthUtils { } fun getCreateAccountPrompt(scope: String): String = - // Request offline consent while still requiring fresh authentication. - if ("offline_access" in scope.split(' ')) "login consent" else "login" + // Request offline consent without forcing fresh authentication. + if ("offline_access" in scope.split(' ')) "consent" else "login" fun buildAuthorizationRequest( authorizationEndpoint: Uri, diff --git a/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt index 809b311003..9fbf2d7866 100644 --- a/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt +++ b/opencloudApp/src/test/java/eu/opencloud/android/presentation/authentication/oauth/OAuthPromptTest.kt @@ -12,9 +12,9 @@ import org.robolectric.annotation.Config @Config(manifest = Config.NONE, sdk = [28]) class OAuthPromptTest { @Test - fun offlineAccessRequiresLoginAndConsent() { - assertEquals("login consent", OAuthUtils.getCreateAccountPrompt("openid profile email groups offline_access")) - assertEquals("login consent", OAuthUtils.getCreateAccountPrompt(" offline_access openid ")) + fun offlineAccessRequiresConsentOnly() { + assertEquals("consent", OAuthUtils.getCreateAccountPrompt("openid profile email groups offline_access")) + assertEquals("consent", OAuthUtils.getCreateAccountPrompt(" offline_access openid ")) } @Test @@ -39,8 +39,8 @@ class OAuthPromptTest { username = null, sendLoginHintAndUser = false, ) - assertEquals(listOf("login consent"), uri.getQueryParameters("prompt")) - assertTrue(uri.encodedQuery!!.contains("prompt=login%20consent")) + assertEquals(listOf("consent"), uri.getQueryParameters("prompt")) + assertTrue(uri.encodedQuery!!.contains("prompt=consent")) assertEquals("oc://android.opencloud.eu", uri.getQueryParameter("redirect_uri")) assertEquals("OpenCloudAndroid", uri.getQueryParameter("client_id")) assertEquals(scope, uri.getQueryParameter("scope"))