Skip to content

Conversation

@zerox80
Copy link
Contributor

@zerox80 zerox80 commented Jan 21, 2026

Fix: Remove Authorization Header for Public OAuth Clients (RFC 7636)

Summary

based on:
#61

Thanks to @rcdailey for his contributions

This pull request addresses a critical OAuth2 compliance issue where the application was incorrectly sending an Authorization header during token exchange for public clients using PKCE (Proof Key for Code Exchange). According to RFC 7636 and the OAuth 2.0 specification for public clients, native applications that cannot securely store client secrets must not include an Authorization header when exchanging authorization codes for tokens.

Problem Description

The previous implementation always attempted to generate a clientAuth value and include it in the Authorization header during the token exchange request, regardless of whether the client was configured as a public or confidential client. This behavior caused authentication failures with OAuth providers that strictly enforce RFC 7636 compliance, as they reject token requests from public clients that include an Authorization header.

Public clients are characterized by an empty clientSecret value. When the clientSecret is empty or not provided, the application should authenticate solely through the PKCE code verifier mechanism rather than through HTTP Basic Authentication headers.

Technical Changes

TokenRequestRemoteOperation.kt

The core fix resides in the TokenRequestRemoteOperation class, which handles the HTTP request to the token endpoint. The critical change is the conditional logic that now checks whether clientAuth is non-empty before adding the Authorization header:

if (tokenRequestParams.clientAuth.isNotEmpty()) {
    postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth)
}

Previously, this header was always added regardless of the clientAuth value. With this change, when clientAuth is an empty string, no Authorization header is sent, which is the correct behavior for public PKCE clients.

LoginActivity.kt

The exchangeAuthorizationCodeForTokens method was updated to properly determine the clientAuth value based on whether a clientSecret exists. The implementation now uses a clear conditional check:

val clientAuth = if (clientSecret.isEmpty()) {
    ""
} else {
    OAuthUtils.getClientAuth(clientSecret, clientId)
}

This ensures that when the application is configured as a public client with no client secret, an empty string is passed as clientAuth, which subsequently prevents the Authorization header from being added to the token request.

A variable shadowing bug was also fixed in this method. Previously, destructuring assignment created local variables that shadowed the intended values, which could lead to incorrect data being used when constructing the token request.

AccountAuthenticator.java

The AccountAuthenticator class was refactored to improve code organization and readability. The changes enhance the token refresh logic to properly handle public client scenarios where no Authorization header should be sent during token refresh operations.

Unit Tests

Comprehensive unit tests were added to ensure RFC 7636 compliance is maintained:

TokenRequest.kt (Test Fixtures)

Two new test fixtures were added for public PKCE clients:

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..."
)

OCRemoteOAuthDataSourceTest.kt

Three new test methods were added:

  1. performTokenRequest with public PKCE client returns a TokenResponse - Verifies that access token requests work correctly with empty clientAuth.

  2. performTokenRequest with public PKCE client refresh token returns a TokenResponse - Verifies that refresh token requests work correctly with empty clientAuth.

  3. public PKCE client fixtures have empty clientAuth preventing Authorization header - A compliance verification test that asserts public client fixtures have empty clientAuth while confidential client fixtures have non-empty clientAuth.

RemoteOAuthUtils.kt

Test utility classes were extended with corresponding remote parameter fixtures for both public and confidential client scenarios, ensuring complete test coverage across the OAuth data layer.

Verification

All changes have been verified through:

  • Detekt static analysis passed with zero issues
  • All existing unit tests pass
  • New unit tests pass for public PKCE client scenarios
  • Debug build compiles successfully

Related Standards

  • RFC 7636: Proof Key for Code Exchange by OAuth Public Clients
  • OAuth 2.0 for Native Apps (RFC 8252)
  • OpenID Connect Core 1.0

Files Changed

File Changes
TokenRequestRemoteOperation.kt Added conditional check for Authorization header
LoginActivity.kt Fixed clientAuth determination and variable shadowing
AccountAuthenticator.java Refactored token handling logic
TokenRequest.kt Added public client test fixtures
RemoteOAuthUtils.kt Added public client remote parameter fixtures
OCRemoteOAuthDataSourceTest.kt Added three new unit tests for RFC 7636 compliance

@zerox80 zerox80 force-pushed the feature/fix-oauth-pkce branch from 2d85132 to fc767ce Compare January 21, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant