How to configure a route in Spring Cloud Gateway to use an OAuth2 client with authorization-grant-type: password
? In other words, how to add the Authorization header with the token in the requests to an API? Because I'm integrating with a legacy application, I must use the grant type password.
I have this application:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("route_path", r -> r.path("/**")
.filters(f -> f.addRequestHeader("Authorization", "bearer <token>"))
.uri("http://localhost:8092/messages"))
.build();
}
}
Replacing the <token>
with an actual token, everything just works fine.
I found this project that does something similar: https://github.com/jgrandja/spring-security-oauth-5-2-migrate. It has a client (messaging-client-password
) that is used to configure the WebClient
to add OAuth2 support to make requests (i.e. by adding the Authorization header).
We can't use this sample project right away because Spring Cloud Gateway is reactive and the way we configure things changes significantly. I think to solve this problem is mostly about converting the WebClientConfig class.
UPDATE
I kinda make it work, but it is in very bad shape.
First, I found how to convert WebClientConfig
to be reactive:
@Configuration
public class WebClientConfig {
@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("messaging-client-password");
return WebClient.builder()
.filter(oauth)
.build();
}
@Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.password()
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// For the `password` grant, the `username` and `password` are supplied via request parameters,
// so map it to `OAuth2AuthorizationContext.getAttributes()`.
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
ServerWebExchange serverWebExchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName());
String username = serverWebExchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.USERNAME);
String password = serverWebExchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return Mono.just(contextAttributes);
};
}
}
With this configuration, we can use the WebClient
to make a request. This somehow initializes the OAuth2 client after calling the endpoint:
@GetMapping("/explicit")
public Mono<String[]> explicit() {
return this.webClient
.get()
.uri("http://localhost:8092/messages")
.attributes(clientRegistrationId("messaging-client-password"))
.retrieve()
.bodyToMono(String[].class);
}
Then, by calling this one we are able to get the reference to the authorized client:
private OAuth2AuthorizedClient authorizedClient;
@GetMapping("/token")
public String token(@RegisteredOAuth2AuthorizedClient("messaging-client-password") OAuth2AuthorizedClient authorizedClient) {
this.authorizedClient = authorizedClient;
return authorizedClient.getAccessToken().getTokenValue();
}
And finally, by configuring a global filter, we can modify the request to include the Authorization header:
@Bean
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> {
//adds header to proxied request
exchange.getRequest().mutate().header("Authorization", authorizedClient.getAccessToken().getTokenType().getValue() + " " + authorizedClient.getAccessToken().getTokenValue()).build();
return chain.filter(exchange);
};
}
After running this three requests in order, we can use the password grant with Spring Cloud Gateway.
Of course, this process is very messy. What still needs to be done:
- Get the reference for the authorized client inside the filter
- Initialize the authorized client with the credentials using
contextAttributesMapper
- Write all of this in a filter, not in a global filter. TokenRelayGatewayFilterFactory implementation can provide a good help to do this.