@@ -34,13 +34,16 @@ import eu.opencloud.android.testutil.oauth.OC_CLIENT_REGISTRATION
3434import eu.opencloud.android.testutil.oauth.OC_CLIENT_REGISTRATION_REQUEST
3535import eu.opencloud.android.testutil.oauth.OC_OIDC_SERVER_CONFIGURATION
3636import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_ACCESS
37+ import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT
38+ import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT
3739import eu.opencloud.android.testutil.oauth.OC_TOKEN_RESPONSE
3840import eu.opencloud.android.utils.createRemoteOperationResultMock
3941import io.mockk.every
4042import io.mockk.mockk
4143import io.mockk.verify
4244import org.junit.Assert.assertEquals
4345import org.junit.Assert.assertNotNull
46+ import org.junit.Assert.assertTrue
4447import org.junit.Before
4548import org.junit.Test
4649
@@ -102,6 +105,98 @@ class OCRemoteOAuthDataSourceTest {
102105 }
103106 }
104107
108+ /* *
109+ * Test for public PKCE clients (RFC 7636).
110+ * Public clients MUST NOT send Authorization header during token exchange.
111+ * This test verifies that token requests work correctly with empty clientAuth.
112+ */
113+ @Test
114+ fun `performTokenRequest with public PKCE client returns a TokenResponse` () {
115+ val tokenResponseResult: RemoteOperationResult <TokenResponse > =
116+ createRemoteOperationResultMock(data = OC_REMOTE_TOKEN_RESPONSE , isSuccess = true )
117+
118+ every {
119+ oidcService.performTokenRequest(ocClientMocked, any())
120+ } returns tokenResponseResult
121+
122+ // Use the public client fixture which has empty clientAuth
123+ assertTrue(
124+ " clientAuth should be empty for public clients" ,
125+ OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT .clientAuth.isEmpty()
126+ )
127+
128+ val tokenResponse = remoteOAuthDataSource.performTokenRequest(OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT )
129+
130+ assertNotNull(tokenResponse)
131+ assertEquals(OC_TOKEN_RESPONSE , tokenResponse)
132+
133+ verify(exactly = 1 ) {
134+ clientManager.getClientForAnonymousCredentials(OC_SECURE_BASE_URL , any())
135+ oidcService.performTokenRequest(ocClientMocked, any())
136+ }
137+ }
138+
139+ /* *
140+ * Test for public PKCE clients (RFC 7636) using refresh token.
141+ * Public clients MUST NOT send Authorization header during token refresh.
142+ * This test verifies that refresh token requests work correctly with empty clientAuth.
143+ */
144+ @Test
145+ fun `performTokenRequest with public PKCE client refresh token returns a TokenResponse` () {
146+ val tokenResponseResult: RemoteOperationResult <TokenResponse > =
147+ createRemoteOperationResultMock(data = OC_REMOTE_TOKEN_RESPONSE , isSuccess = true )
148+
149+ every {
150+ oidcService.performTokenRequest(ocClientMocked, any())
151+ } returns tokenResponseResult
152+
153+ // Verify the refresh token fixture has empty clientAuth
154+ assertTrue(
155+ " clientAuth should be empty for public clients" ,
156+ OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT .clientAuth.isEmpty()
157+ )
158+
159+ val tokenResponse = remoteOAuthDataSource.performTokenRequest(OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT )
160+
161+ assertNotNull(tokenResponse)
162+ assertEquals(OC_TOKEN_RESPONSE , tokenResponse)
163+
164+ verify(exactly = 1 ) {
165+ clientManager.getClientForAnonymousCredentials(OC_SECURE_BASE_URL , any())
166+ oidcService.performTokenRequest(ocClientMocked, any())
167+ }
168+ }
169+
170+ /* *
171+ * RFC 7636 compliance verification:
172+ * This test ensures that our public client test fixtures correctly have empty clientAuth,
173+ * which means TokenRequestRemoteOperation will NOT add an Authorization header.
174+ * The actual header logic is in TokenRequestRemoteOperation:
175+ * if (tokenRequestParams.clientAuth.isNotEmpty()) {
176+ * postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth)
177+ * }
178+ */
179+ @Test
180+ fun `public PKCE client fixtures have empty clientAuth preventing Authorization header` () {
181+ // Verify access token fixture
182+ assertTrue(
183+ " Access token public client fixture should have empty clientAuth" ,
184+ OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT .clientAuth.isEmpty()
185+ )
186+
187+ // Verify refresh token fixture
188+ assertTrue(
189+ " Refresh token public client fixture should have empty clientAuth" ,
190+ OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT .clientAuth.isEmpty()
191+ )
192+
193+ // Verify confidential client fixtures have non-empty clientAuth (for comparison)
194+ assertTrue(
195+ " Confidential client fixture should have non-empty clientAuth" ,
196+ OC_TOKEN_REQUEST_ACCESS .clientAuth.isNotEmpty()
197+ )
198+ }
199+
105200 @Test
106201 fun `registerClient returns a ClientRegistrationInfo` () {
107202 val clientRegistrationResponse: RemoteOperationResult <ClientRegistrationResponse > =
0 commit comments