Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KTOR-7775 BasicAuth: Add clearToken method #4645

Merged
merged 4 commits into from
Feb 11, 2025
Merged

Conversation

osipxd
Copy link
Member

@osipxd osipxd commented Feb 3, 2025

Subsystem
Client, Basic Auth

Motivation
KTOR-7775 Auth: BasicAuthProvider caches credentials until process death

Solution
Add BasicAuthProvider.clearToken similarly to BearerAuthProvider. So now the token can be cleared the following way:

import io.ktor.client.plugins.auth.authProvider

client.authProvider<BasicAuthProvider>()?.clearToken()

Probably, we should come up with a common solution for clearing authorization tokens, but unlikely in 3.1.0.

@osipxd osipxd self-assigned this Feb 3, 2025
@osipxd osipxd force-pushed the osipxd/basic-auth-clear-token branch 3 times, most recently from 70360f1 to 85bde92 Compare February 3, 2025 16:33
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BasicAuthProvider.clearToken)
*/
public fun clearToken() {
Copy link
Member Author

@osipxd osipxd Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make this method a part of AuthProvider interface, but it would be potentially breaking change. Only if not declare an empty implementation by default.
However, it makes sense for me to make clearToken a required for auth provider.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could introduce it with a default implementation of throwing an UnsupportedOperationException. Then we can drop the default implementation for the next major release as a breaking change. It's breaking the Liskov rule, but I think the pros outweigh the cons for having a standard function here.

Copy link
Member Author

@osipxd osipxd Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... If we add this method, we assume that all implementations of AuthProvider should be caching, as this method doesn't make any sense for stateless implementations. Probably it would be better to introduce CachingAuthProvider containing clear, and providing a way to create custom auth providers using AuthTokenHolder under the hood?

/**
 * A base implementation for an authentication provider with authentication state caching.
 *
 * @param AuthState The type representing the authentication state, which contains necessary token or credential information.
 * @param fetchState A function for fetching the authentication state, typically used during first access
 * or during refresh (if [refreshState] is not overridden).
 */
public abstract class CachingAuthProvider<AuthState : Any>(
    private val fetchState: suspend () -> AuthState?,
) : AuthProvider {

    // If we go this way, AuthTokenHolder should be renamed to something else
    private val tokenHolder = AuthTokenHolder(fetchState)

    /** Returns the cached authentication state, or fetches it if it wasn't loaded before. */
    protected suspend fun loadState(): AuthState? = tokenHolder.loadToken()

    final override suspend fun addRequestHeaders(request: HttpRequestBuilder, authHeader: HttpAuthHeader?) {
        val tokens = loadState() ?: return
        addRequestHeaders(request, authHeader, tokens)
    }

    /**
     * Adds an authentication method headers and credentials.
     *
     * @param request The builder for an authenticated request
     * @param authHeader The value of `WWW-Authenticate` header from failed response, if exists
     * @param state The authentication state containing token or credential information for the request
     */
    protected abstract suspend fun addRequestHeaders(
        request: HttpRequestBuilder,
        authHeader: HttpAuthHeader?,
        state: AuthState,
    )

    final override suspend fun refreshToken(response: HttpResponse): Boolean {
        val newState = tokenHolder.setToken { refreshState(response) }
        return newState != null
    }

    /**
     * Refreshes the authentication state if required.
     *
     * @param response The HTTP response received after a request.
     * @return The updated authentication state or `null` if the state couldn't be refreshed.
     */
    protected open suspend fun refreshState(response: HttpResponse): AuthState? = fetchState()

    /** Clears the currently stored authentication state from the cache. */
    public fun clear() {
        tokenHolder.clearToken()
    }
}

Copy link
Member Author

@osipxd osipxd Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, just noticed that we have a request to make AuthTokenHolder re-usable:

  • KTOR-8126 ktor-client-auth with custom providers

But we also have requests to make it possible to completely disable caching for auth providers:

  • KTOR-4759 Auth: BearerAuthProvider caches result of loadToken until process death
  • KTOR-6569 Bearer auth: Don't cache client bearer token (option)
  • KTOR-4946 Auth: Bearer authentication - unable to update tokens

My proposed CachingAuthProvider implementation fixes the first issue, but doesn’t address the second one.

Copy link
Member Author

@osipxd osipxd Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have two different use cases:

  1. A user uses some third-party token storage and wants to delegate responsibility for token lifecycle management to this storage (for example, Firebase SDK). In this case, AuthProvider shouldn't cache the token
  2. A user uses their own backend to get a token and (likely) doesn't want extra requests to be made when the token is still valid. For this case caching works fine, but there is a danger of forgetting to clear the token on logout.

Considering these two scenarios, I'd force users to explicitly choose whether they want caching enabled or not. For example, passing a flag to loadToken:

install(Auth) {
    bearer {
        loadTokens(cacheEnabled = false) { getFirebaseTokens() }

        // Deprecated. Specifies cacheEnabled = true implicitly.
        loadTokens { getFirebaseTokens() }
    }
}

Alternative solution would be to make it possible to override the implementation of token holder, but it seems a more complex though more flexible solution.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjhham, @e5l, @marychatte, what do you think?
For me, v3.1 seems like a good moment to fix all these issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created an issue to rework token storage:

  • KTOR-8180 Auth: Provide control over tokens to user code

And marked new methods as @InternalAPI. For now it is better to keep clearToken out of the AuthProvider interface.

Copy link
Contributor

@bjhham bjhham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd like to add it to the interface, I'd also be happy with that 👍

@osipxd osipxd force-pushed the osipxd/basic-auth-clear-token branch 2 times, most recently from 77cc175 to 0bab8d4 Compare February 10, 2025 15:09
@osipxd osipxd enabled auto-merge (squash) February 10, 2025 15:12
@osipxd osipxd force-pushed the osipxd/basic-auth-clear-token branch 2 times, most recently from e6a0810 to b7e4963 Compare February 10, 2025 16:34
@osipxd osipxd force-pushed the osipxd/basic-auth-clear-token branch from b7e4963 to 860c05b Compare February 11, 2025 10:06
@osipxd osipxd merged commit 07dd782 into main Feb 11, 2025
14 of 17 checks passed
@osipxd osipxd deleted the osipxd/basic-auth-clear-token branch February 11, 2025 10:57
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.

2 participants