From 2704436b0c570b8eb5943b3aff779827662a19c5 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Fri, 21 Nov 2025 21:14:44 -0600 Subject: [PATCH 1/2] fix: remove Authorization header for public clients in OAuth2 token exchange Public OAuth2 clients using PKCE should not send Authorization headers during token exchange per RFC 7636. This was causing token refresh failures with external OIDC providers like Authelia and Zitadel. Changes: - Made clientAuth nullable in TokenRequest and TokenRequestParams - Added conditional Authorization header in TokenRequestRemoteOperation - Added isTokenEndpointAuthMethodNone() helper in OIDCServerConfiguration - Updated LoginActivity and AccountAuthenticator for public client auth Related to #55 --- .../authentication/AccountAuthenticator.java | 16 ++++++++-- .../authentication/LoginActivity.kt | 29 ++++++++++++++----- .../oauth/TokenRequestRemoteOperation.kt | 4 ++- .../oauth/params/TokenRequestParams.kt | 6 ++-- .../oauth/model/OIDCServerConfiguration.kt | 3 ++ .../oauth/model/TokenRequest.kt | 6 ++-- 6 files changed, 46 insertions(+), 18 deletions(-) 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 40580ed59..e22f2cd7c 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 @@ -345,6 +345,7 @@ private String refreshToken( String clientIdForRequest = null; String clientSecretForRequest = null; + String clientAuth = null; if (clientId == null) { Timber.d("Client Id not stored. Let's use the hardcoded one"); @@ -362,20 +363,29 @@ private String refreshToken( // Use token endpoint retrieved from oidc discovery tokenEndpoint = oidcServerConfigurationUseCaseResult.getDataOrNull().getTokenEndpoint(); + // RFC 7636: Public clients (token_endpoint_auth_method: none) must not send Authorization header if (oidcServerConfigurationUseCaseResult.getDataOrNull() != null && - oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodSupportedClientSecretPost()) { + oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodNone()) { + clientAuth = null; + clientIdForRequest = clientId; + } else if (oidcServerConfigurationUseCaseResult.getDataOrNull() != null && + oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodSupportedClientSecretPost()) { + // For client_secret_post, credentials go in body, not Authorization header + clientAuth = null; clientIdForRequest = clientId; clientSecretForRequest = clientSecret; + } else { + // For other methods (e.g., client_secret_basic), use Basic auth header + clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId); } } else { Timber.d("OIDC Discovery failed. Server discovery info: [ %s ]", oidcServerConfigurationUseCaseResult.getThrowableOrNull().toString()); tokenEndpoint = baseUrl + File.separator + mContext.getString(R.string.oauth2_url_endpoint_access); + clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId); } - String clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId); - String scope = mContext.getResources().getString(R.string.oauth2_openid_scope); TokenRequest oauthTokenRequest = new TokenRequest.RefreshToken( 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 4e1819a0a..e548750ec 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 @@ -666,28 +666,41 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted val clientRegistrationInfo = authenticationViewModel.registerClient.value?.peekContent()?.getStoredData() - val clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { - OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId) - - } else { - OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id)) - } - // Use oidc discovery one, or build an oauth endpoint using serverBaseUrl + Setup string. val tokenEndPoint: String var clientId: String? = null var clientSecret: String? = null + var clientAuth: String? = null val serverInfo = authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData() if (serverInfo is ServerInfo.OIDCServer) { tokenEndPoint = serverInfo.oidcServerConfiguration.tokenEndpoint - if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodSupportedClientSecretPost()) { + + // RFC 7636: Public clients (token_endpoint_auth_method: none) must not send Authorization header + if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodNone()) { + clientAuth = null + clientId = clientRegistrationInfo?.clientId ?: contextProvider.getString(R.string.oauth2_client_id) + } else if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodSupportedClientSecretPost()) { + // For client_secret_post, credentials go in body, not Authorization header + clientAuth = null clientId = clientRegistrationInfo?.clientId ?: contextProvider.getString(R.string.oauth2_client_id) clientSecret = clientRegistrationInfo?.clientSecret ?: contextProvider.getString(R.string.oauth2_client_secret) + } else { + // For other methods (e.g., client_secret_basic), use Basic auth header + clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { + OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId) + } else { + OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id)) + } } } else { tokenEndPoint = "$serverBaseUrl${File.separator}${contextProvider.getString(R.string.oauth2_url_endpoint_access)}" + clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { + OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId) + } else { + OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id)) + } } val scope = resources.getString(R.string.oauth2_openid_scope) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt index 25064d141..cf29a5263 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt @@ -54,7 +54,9 @@ class TokenRequestRemoteOperation( val postMethod = PostMethod(URL(tokenRequestParams.tokenEndpoint), requestBody) - postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth) + tokenRequestParams.clientAuth?.takeIf { it.isNotEmpty() }?.let { + postMethod.addRequestHeader(AUTHORIZATION_HEADER, it) + } val status = client.executeHttpMethod(postMethod) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt index 0aac1ce04..6fd61a707 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt @@ -30,7 +30,7 @@ import okhttp3.RequestBody sealed class TokenRequestParams( val tokenEndpoint: String, - val clientAuth: String, + val clientAuth: String?, val grantType: String, val scope: String, val clientId: String?, @@ -40,7 +40,7 @@ sealed class TokenRequestParams( class Authorization( tokenEndpoint: String, - clientAuth: String, + clientAuth: String?, grantType: String, scope: String, clientId: String?, @@ -65,7 +65,7 @@ sealed class TokenRequestParams( class RefreshToken( tokenEndpoint: String, - clientAuth: String, + clientAuth: String?, grantType: String, scope: String, clientId: String?, diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/OIDCServerConfiguration.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/OIDCServerConfiguration.kt index d871569b5..395083067 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/OIDCServerConfiguration.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/OIDCServerConfiguration.kt @@ -35,4 +35,7 @@ data class OIDCServerConfiguration( ) { fun isTokenEndpointAuthMethodSupportedClientSecretPost(): Boolean = tokenEndpointAuthMethodsSupported?.any { it == "client_secret_post" } ?: false + + fun isTokenEndpointAuthMethodNone(): Boolean = + tokenEndpointAuthMethodsSupported?.any { it == "none" } ?: false } diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt index 8b61ba59b..900a230db 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt @@ -24,7 +24,7 @@ package eu.opencloud.android.domain.authentication.oauth.model sealed class TokenRequest( val baseUrl: String, val tokenEndpoint: String, - val clientAuth: String, + val clientAuth: String?, val grantType: String, val scope: String, val clientId: String?, @@ -33,7 +33,7 @@ sealed class TokenRequest( class AccessToken( baseUrl: String, tokenEndpoint: String, - clientAuth: String, + clientAuth: String?, scope: String, clientId: String? = null, clientSecret: String? = null, @@ -45,7 +45,7 @@ sealed class TokenRequest( class RefreshToken( baseUrl: String, tokenEndpoint: String, - clientAuth: String, + clientAuth: String?, scope: String, clientId: String? = null, clientSecret: String? = null, From 37a1bc250a4e118b9a0a68416003c3e86f123c33 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Wed, 21 Jan 2026 08:15:49 -0600 Subject: [PATCH 2/2] fix: use non-nullable empty string for clientAuth in public PKCE clients Address PR review feedback from @zerox80 to use empty string instead of nullable String? for clientAuth parameter. - Change clientAuth from String? to String across domain/data layers - Use empty string "" for public clients instead of null - Simplify header check to isNotEmpty() instead of null-safe chain - Add unit test for public PKCE client token exchange scenario --- .../authentication/AccountAuthenticator.java | 6 +-- .../authentication/LoginActivity.kt | 39 +++++++++---------- .../oauth/TokenRequestRemoteOperation.kt | 4 +- .../oauth/params/TokenRequestParams.kt | 6 +-- .../OCRemoteOAuthDataSourceTest.kt | 33 ++++++++++++++++ .../oauth/model/TokenRequest.kt | 6 +-- .../android/testutil/oauth/TokenRequest.kt | 22 +++++++++++ 7 files changed, 85 insertions(+), 31 deletions(-) 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 e22f2cd7c..b73a9a683 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 @@ -345,7 +345,7 @@ private String refreshToken( String clientIdForRequest = null; String clientSecretForRequest = null; - String clientAuth = null; + String clientAuth; if (clientId == null) { Timber.d("Client Id not stored. Let's use the hardcoded one"); @@ -366,12 +366,12 @@ private String refreshToken( // RFC 7636: Public clients (token_endpoint_auth_method: none) must not send Authorization header if (oidcServerConfigurationUseCaseResult.getDataOrNull() != null && oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodNone()) { - clientAuth = null; + clientAuth = ""; clientIdForRequest = clientId; } else if (oidcServerConfigurationUseCaseResult.getDataOrNull() != null && oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodSupportedClientSecretPost()) { // For client_secret_post, credentials go in body, not Authorization header - clientAuth = null; + clientAuth = ""; clientIdForRequest = clientId; clientSecretForRequest = clientSecret; } else { 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 e548750ec..7fd2800a2 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 @@ -669,9 +669,16 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted // Use oidc discovery one, or build an oauth endpoint using serverBaseUrl + Setup string. val tokenEndPoint: String - var clientId: String? = null - var clientSecret: String? = null - var clientAuth: String? = null + // Determine clientId and clientSecret + val (clientId, clientSecret) = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { + Pair(clientRegistrationInfo.clientId, clientRegistrationInfo.clientSecret as String) + } else { + Pair(getString(R.string.oauth2_client_id), getString(R.string.oauth2_client_secret)) + } + + var clientIdForRequest: String? = null + var clientSecretForRequest: String? = null + var clientAuth: String val serverInfo = authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData() if (serverInfo is ServerInfo.OIDCServer) { @@ -679,28 +686,20 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted // RFC 7636: Public clients (token_endpoint_auth_method: none) must not send Authorization header if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodNone()) { - clientAuth = null - clientId = clientRegistrationInfo?.clientId ?: contextProvider.getString(R.string.oauth2_client_id) + clientAuth = "" + clientIdForRequest = clientId } else if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodSupportedClientSecretPost()) { // For client_secret_post, credentials go in body, not Authorization header - clientAuth = null - clientId = clientRegistrationInfo?.clientId ?: contextProvider.getString(R.string.oauth2_client_id) - clientSecret = clientRegistrationInfo?.clientSecret ?: contextProvider.getString(R.string.oauth2_client_secret) + clientAuth = "" + clientIdForRequest = clientId + clientSecretForRequest = clientSecret } else { // For other methods (e.g., client_secret_basic), use Basic auth header - clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { - OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId) - } else { - OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id)) - } + clientAuth = OAuthUtils.getClientAuth(clientSecret, clientId) } } else { tokenEndPoint = "$serverBaseUrl${File.separator}${contextProvider.getString(R.string.oauth2_url_endpoint_access)}" - clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) { - OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId) - } else { - OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id)) - } + clientAuth = OAuthUtils.getClientAuth(clientSecret, clientId) } val scope = resources.getString(R.string.oauth2_openid_scope) @@ -710,8 +709,8 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted tokenEndpoint = tokenEndPoint, clientAuth = clientAuth, scope = scope, - clientId = clientId, - clientSecret = clientSecret, + clientId = clientIdForRequest, + clientSecret = clientSecretForRequest, authorizationCode = authorizationCode, redirectUri = OAuthUtils.buildRedirectUri(applicationContext).toString(), codeVerifier = authenticationViewModel.codeVerifier diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt index cf29a5263..a35b8bef0 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt @@ -54,8 +54,8 @@ class TokenRequestRemoteOperation( val postMethod = PostMethod(URL(tokenRequestParams.tokenEndpoint), requestBody) - tokenRequestParams.clientAuth?.takeIf { it.isNotEmpty() }?.let { - postMethod.addRequestHeader(AUTHORIZATION_HEADER, it) + if (tokenRequestParams.clientAuth.isNotEmpty()) { + postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth) } val status = client.executeHttpMethod(postMethod) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt index 6fd61a707..0aac1ce04 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/oauth/params/TokenRequestParams.kt @@ -30,7 +30,7 @@ import okhttp3.RequestBody sealed class TokenRequestParams( val tokenEndpoint: String, - val clientAuth: String?, + val clientAuth: String, val grantType: String, val scope: String, val clientId: String?, @@ -40,7 +40,7 @@ sealed class TokenRequestParams( class Authorization( tokenEndpoint: String, - clientAuth: String?, + clientAuth: String, grantType: String, scope: String, clientId: String?, @@ -65,7 +65,7 @@ sealed class TokenRequestParams( class RefreshToken( tokenEndpoint: String, - clientAuth: String?, + clientAuth: String, grantType: String, scope: String, clientId: String?, diff --git a/opencloudData/src/test/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSourceTest.kt b/opencloudData/src/test/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSourceTest.kt index a513e8282..8f0d899a3 100644 --- a/opencloudData/src/test/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSourceTest.kt +++ b/opencloudData/src/test/java/eu/opencloud/android/data/oauth/datasources/implementation/OCRemoteOAuthDataSourceTest.kt @@ -34,6 +34,7 @@ import eu.opencloud.android.testutil.oauth.OC_CLIENT_REGISTRATION import eu.opencloud.android.testutil.oauth.OC_CLIENT_REGISTRATION_REQUEST import eu.opencloud.android.testutil.oauth.OC_OIDC_SERVER_CONFIGURATION import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_ACCESS +import eu.opencloud.android.testutil.oauth.OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT import eu.opencloud.android.testutil.oauth.OC_TOKEN_RESPONSE import eu.opencloud.android.utils.createRemoteOperationResultMock import io.mockk.every @@ -41,6 +42,7 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -102,6 +104,37 @@ class OCRemoteOAuthDataSourceTest { } } + /** + * Test for public PKCE clients (RFC 7636). + * Public clients MUST NOT send Authorization header during token exchange. + * This test verifies that token requests work correctly with empty clientAuth. + */ + @Test + fun `performTokenRequest with public PKCE client returns a TokenResponse`() { + val tokenResponseResult: RemoteOperationResult = + createRemoteOperationResultMock(data = OC_REMOTE_TOKEN_RESPONSE, isSuccess = true) + + every { + oidcService.performTokenRequest(ocClientMocked, any()) + } returns tokenResponseResult + + // Verify the fixture has empty clientAuth for public clients + assertTrue( + "clientAuth should be empty for public clients", + OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT.clientAuth.isEmpty() + ) + + val tokenResponse = remoteOAuthDataSource.performTokenRequest(OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT) + + assertNotNull(tokenResponse) + assertEquals(OC_TOKEN_RESPONSE, tokenResponse) + + verify(exactly = 1) { + clientManager.getClientForAnonymousCredentials(OC_SECURE_BASE_URL, any()) + oidcService.performTokenRequest(ocClientMocked, any()) + } + } + @Test fun `registerClient returns a ClientRegistrationInfo`() { val clientRegistrationResponse: RemoteOperationResult = diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt index 900a230db..8b61ba59b 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/authentication/oauth/model/TokenRequest.kt @@ -24,7 +24,7 @@ package eu.opencloud.android.domain.authentication.oauth.model sealed class TokenRequest( val baseUrl: String, val tokenEndpoint: String, - val clientAuth: String?, + val clientAuth: String, val grantType: String, val scope: String, val clientId: String?, @@ -33,7 +33,7 @@ sealed class TokenRequest( class AccessToken( baseUrl: String, tokenEndpoint: String, - clientAuth: String?, + clientAuth: String, scope: String, clientId: String? = null, clientSecret: String? = null, @@ -45,7 +45,7 @@ sealed class TokenRequest( class RefreshToken( baseUrl: String, tokenEndpoint: String, - clientAuth: String?, + clientAuth: String, scope: String, clientId: String? = null, clientSecret: String? = null, diff --git a/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/oauth/TokenRequest.kt b/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/oauth/TokenRequest.kt index 7f69e5564..85c46bcc7 100644 --- a/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/oauth/TokenRequest.kt +++ b/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/oauth/TokenRequest.kt @@ -43,3 +43,25 @@ val OC_TOKEN_REQUEST_ACCESS = TokenRequest.AccessToken( redirectUri = OC_REDIRECT_URI, codeVerifier = "A high-entropy cryptographic random STRING using the unreserved characters" ) + +/** + * Test fixtures for public PKCE clients (RFC 7636). + * Public clients MUST NOT send Authorization header during token exchange. + */ +val OC_TOKEN_REQUEST_REFRESH_PUBLIC_CLIENT = TokenRequest.RefreshToken( + baseUrl = OC_SECURE_BASE_URL, + tokenEndpoint = OC_TOKEN_ENDPOINT, + clientAuth = "", // Empty for public clients per RFC 7636 + scope = OC_SCOPE, + refreshToken = OC_REFRESH_TOKEN +) + +val OC_TOKEN_REQUEST_ACCESS_PUBLIC_CLIENT = TokenRequest.AccessToken( + baseUrl = OC_SECURE_BASE_URL, + tokenEndpoint = OC_TOKEN_ENDPOINT, + clientAuth = "", // Empty for public clients per RFC 7636 + scope = OC_SCOPE, + authorizationCode = "4uth0r1z4t10nC0d3", + redirectUri = OC_REDIRECT_URI, + codeVerifier = "A high-entropy cryptographic random STRING using the unreserved characters" +)