I have a Spring Web application that has a oauth2 resource server configured for it's API endpoints and a completely different oauth2 client for REST calls it makes. The oauth2 client needs to be of a password grant type. Username and password is fixed (not coming from HTTP request). My problem is that after 30 minutes both access token and refresh token expire, so there is no way to do a refresh. I would expect that Spring Security would just ask for new access token, but no. It calls the REST endpoint with the expired one and it returns 403. Here's what I have:
application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://our.idp.keycloak.host/auth/realms/firstrealm
client:
registration:
my-client-authorization:
client-id: my_client
client-secret: ${CLIENT_SECRET}
authorization-grant-type: password
scope: openid, profile
provider:
my-client-authorization:
token-uri: https://our.idp.keycloak.host/auth/realms/secondrealm/protocol/openid-connect/token
MyClientConfig.java:
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Map;
@Configuration
@RequiredArgsConstructor
public class MyClientConfig {
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.baseUrl("https://the.api.host.to.call")
.build();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository
) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
DefaultOAuth2AuthorizedClientManager result = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository
);
result.setAuthorizedClientProvider(authorizedClientProvider);
result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
));
return result;
}
}
The API call itself:
private <T> T callApi(Function<UriBuilder, URI> uriFunction, Class<T> resultType) {
return this.webClient
.get()
.uri(uriFunction)
.retrieve()
.bodyToMono(resultType)
.block();
}
It works the first time I call it. But after 30 minutes the token is dead and I have no idea how to get a new one. If I switch this to client_credentials grant type, it works, it automatically gets a new token when it is needed. But for some reason I cannot do the same for password grant type.
Edit: So I managed to fix this thanks to this: https://github.com/spring-projects/spring-security/issues/8831 . When I configure the WebClient also for refreshToken, then it crashes when the refresh token expires. But when you do the request again after that, it obtains a new token. So I had to wrap the API call in try catch and if the error is the one that I care about, I call the API again. It's not a very elegant solution, but it works.