Expected Behavior

Support OAuth2 Token Revocation, either by properties (spring.security.oauth2.client.provider.{provider}.token-revocation...) Or by a filter / exchange function that can be registered to the WebClient instance that manages OAuth2 details.

Current Behavior

I am struggling to be able to revoke an OAuth2 Token I have received from the Twitch API - revocation method looks like this: https://dev.twitch.tv/docs/authentication#revoking-access-tokens

I can't seem to get access to the underlying OAuth2 client_credentials token in order to revoke it.

Context

The Twitch API only grants a limited number of OAuth2 Tokens before tokens are rate-limited, and I am returned a 401 UNAUTHORIZED when trying to retrieve a new token.

I am only dealing with client credentials right now, and I want to register a shutdown handler in my Spring application that is able to correctly handle the token revocation, but there is no clear way to handle this.

I'm hoping to either get something added to the spring security project to aid with Token revocation, or see an example of how I can retrieve the client credentials OAuth token in order to revoke it in a shutdown handler.

In general, I feel like having the ability to revoke OAuth2 tokens would be a good feature to have anyways.

Comment From: jgrandja

@Bwvolleyball

see an example of how I can retrieve the client credentials OAuth token

Have you gone over the reference documentation?

Comment From: Bwvolleyball

TL;DR - I found a way to at least get the OAuth2 Token and then revoke it manually - is there any plans for providing some sort of mechanism for token revocation in the future? Especially when relying on the OAuth2AuthorizedClientManager to hold credentials and ServletOAuth2AuthorizedClientExchangeFilterFunction to abstract away token management?

For my specific issue (for now) ->

So, I ended up solving this for my specific use case as follows:

@Component
class ApplicationCleanup(
    private val twitchAuthorizedClientProvider: OAuth2AuthorizedClientProvider,
    private val clientRegistrationRepository: ClientRegistrationRepository,
) : DisposableBean {

    override fun destroy() {

        logger.info { "In shutdown phase of the application. Attempting to revoke all OAuth2 Tokens." }

        val twitchRegistration = clientRegistrationRepository.findByRegistrationId("twitch")
        val context = OAuth2AuthorizationContext
            .withClientRegistration(twitchRegistration)
            .principal(
                AnonymousAuthenticationToken(
                    "ApplicationCleanup",
                    "anonymousUser",
                    mutableListOf(
                        SimpleGrantedAuthority("ROLE_ANONYMOUS")
                    )
                )
            )
            .build()

        val token = twitchAuthorizedClientProvider.authorize(context)

        logger.info { "Attempting to revoke retrieved Twitch OAuth2 token: [${token?.accessToken?.tokenValue}]" }

        WebClient.builder()
            .baseUrl("https://id.twitch.tv")
            .build()
            .post()
            .uri("/oauth2/revoke?client_id=${token?.clientRegistration?.clientId}&token=${token?.accessToken?.tokenValue}")
            .retrieve()
            .bodyToMono<String>()
            .doOnEach { logger.info { "Raw Response Body: ${it.get()} <- successful calls have no body." } }
            .block(Duration.ofMinutes(1L))

        logger.info { "Completed custom application cleanup, resuming with normal scheduled duties." }
    }
}

Where I mimic authenticating as an anonymous user with the client registration repository. This client registration repository is configured into another web client though in this fashion:

    /**
     * The twitchAuthorizedClientManager created by deals with client credentials + refresh tokens
     * for interacting with the Twitch API.
     */
    @Bean
    fun twitchAuthorizedClientManager(
        clientRegistrationRepository: ClientRegistrationRepository,
        authorizedClientRepository: OAuth2AuthorizedClientRepository,
        twitchAuthorizedClientProvider: OAuth2AuthorizedClientProvider
    ): OAuth2AuthorizedClientManager {
        // create a client manager with this authorized client repository
        val twitchAuthorizedClientManager = DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository
        )
        twitchAuthorizedClientManager.setAuthorizedClientProvider(twitchAuthorizedClientProvider)
        return twitchAuthorizedClientManager
    }

    // declare which pieces of the OAuth2 Spec this provider should enable
    @Bean
    fun twitchAuthorizedClientProvider(): OAuth2AuthorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
            .clientCredentials()
            .refreshToken()
            .build()

    /**
     * Create a web client for the Twitch API
     *
     * (configured via properties groups
     *     spring.security.oauth2.client.registration.twitch.*
     *     spring.security.oauth2.client.provider.twitch.* )
     *
     * Using the secrets and configuration properties for the twitch OAuth2 API,
     * an outbound OAuth2 filter function is added to this [WebClient] that will
     *
     *     a) Request an OAuth2 Token if it _doesn't_ have one.
     *     b) Re-use a valid OAuth2 Token if it _does_ have one.
     *     c) Refresh the OAuth2 Token if the refresh token is not expired but token is.
     *     d) Rinse and repeat to serendipity. Yay Spring!
     *
     * The OAuth2 Token from this filter is added as the 'Authorization' header for all requests
     * made by this web client.
     */
    @Bean
    fun twitchWebClient(
        twitchAuthorizedClientManager: OAuth2AuthorizedClientManager,
        twitch: TwitchProperties,
        @Qualifier("twitchObjectMapper")
        twitchObjectMapper: ObjectMapper
    ): WebClient {
        val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(twitchAuthorizedClientManager)
        oauth2Client.setDefaultClientRegistrationId("twitch")

        return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            // the domain of the Twitch API
            .baseUrl("https://api.twitch.tv")
            .build()
    }

From this twitchWebClient bean, I didn't see any way to deal with token revocations.

Comment From: jgrandja

@Bwvolleyball

is there any plans for providing some sort of mechanism for token revocation in the future?

There are no plans at the moment as this is the first issue logged around token revocation on client.

I don't feel that revoking tokens on application shutdown (your specific use case) is a common use case.

We would be open to adding an enhancement around token revocation but it has to be a use case that is quite common and therefore would be widely used. Edge cases would need to provide their own customizations.

Comment From: Bwvolleyball

Right, I agree with that statement, a shutdown hook to revoke tokens is niche, mainly I was suggesting a mechanism to revoke tokens, but it's up to the consuming applications to decide when/how to revoke those tokens.

Comment From: jgrandja

@Bwvolleyball

it's up to the consuming applications to decide when/how to revoke those tokens

Agreed.

I'm going to close this issue. However, if you come up with a common use case, please log a new issue and we'll consider adding at that point. Thanks for your feedback!