Log OAuth calls made by the webclient library - spring

I have an application that uses microservice architecture and services are protected by Spring OAuth2 Client Credentials Grant type. We use spring WebClient to call from one service to another along with Spring OAuth client library as it handles fetching OAuth credentials transparently.
As we know the Spring (OAuth client) makes a call to Authorization server to request a new token from Auth server when it doesn't have a token or the current token is expired. How can we log a statement on client side whenever Spring (OAuth client) makes a call to the OAuth server? We would like to log this statement to see how frequently the OAuth client library is making calls to the Authorization Server.
public WebClient webClient()
{
return WebClient.builder()
.apply(filter())
.baseUrl("http://localhost:8082/resource")
.build();
}
public Consumer<Builder> filter()
{
ServletOAuth2AuthorizedClientExchangeFilterFunction oAuth =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oAuth.setDefaultClientRegistrationId("my-client");
return oAuth.oauth2Configuration();
}

Define a custom token client that logs when sending a token request to the authorization server.
#Slf4j
public class LoggingClientCredentialsTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private DefaultClientCredentialsTokenResponseClient delegate =
new DefaultClientCredentialsTokenResponseClient();
#Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
log.debug("Sending request {}", clientCredentialsGrantRequest);
var response = delegate.getTokenResponse(clientCredentialsGrantRequest);
log.debug("Received response {}", response);
return response;
}
}
Create an authorized client manager that uses the custom token client.
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(builder ->
builder.accessTokenResponseClient(
new LoggingClientCredentialsTokenResponseClient()))
.build();
var authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
Create a WebClient that uses the authorized client manager.
#Bean
public WebClient oauth2WebClient(
WebClient.Builder webClientBuilder,
OAuth2AuthorizedClientManager authorizedClientManager) {
var oauth2Filter =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Filter.setDefaultClientRegistrationId("my-client");
return webClientBuilder.apply(oauth2Filter.oauth2Configuration())
.baseUrl("http://localhost:8082/resource")
.build();
}

Related

Spring Security OAuth2 token refreshed by WebClient but stored as "anonymousUser"

I'm trying to implement Spring OAuth2 with WebClient saving refresh tokens for auto renewal in a JdbcOAuth2AuthorizedClientService.
When I first grant access with the Google prompt the token is stored perfectly as a row in the oauth2_authorized_client table for the principal_name that granted access, let's say john#gmail.com.
Later, when I want to use that stored access_token in a scheduled task (no user interaction) I can fetch it like this and inject it in my webClient:
private final OAuth2AuthorizedClientService authorizedClientService;
// ...
OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("google", "john#gmail.com");
return webClient.get()
.uri(CALENDAR_URL, uriBuilder ->
uriBuilder.path("/calendars/primary/events")
.queryParam("key", API_KEY)
.build())
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
This time the expired token gets automatically renewed (hurray!) but for some reason it is stored in the database table as a new row with principal_name as anonymousUser instead of john#gmail.com. Why is that?
This is my WebClient configuration:
#Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(JdbcOperations jdbcOperations,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcOperations, clientRegistrationRepository);
}
#Bean
public WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
manager.setAuthorizedClientProvider(new DelegatingOAuth2AuthorizedClientProvider(
new RefreshTokenOAuth2AuthorizedClientProvider(),
new ClientCredentialsOAuth2AuthorizedClientProvider()));
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
manager);
oauth2.setDefaultClientRegistrationId("google");
return WebClient.builder()
.filter(oauth2)
.apply(oauth2.oauth2Configuration())
.build();
}

OAuth2RestOperations uses token obtained from request header instead of requesting the auth server

I have a spring boot client app which uses OAuth2RestTemplate as OAuth2Client. I have configured the OAuth2RestTemplate to call authserver and add token obtained from it to header for accessing resource server.
The problem occured is that whenever i call the method in client app to access resource server using restTemplate, it used the token coming from header of the request of the client app instead of calling the auth server.
It uses that token and the token gets rejected by my resource server. And after it is rejected, it then only calls the auth server and puts correct token and again sends the request to my resource server.
Is there any way to make rest template not use the token from the header and call the auth server for the token before connecting resource server?
Thank u
My config class
#Configuration
#EnableOAuth2Client
public class OAuth2ClientConfig {
#Autowired
ConfigProperties configProperties;
#Bean("oauth2AuthServer")
public OAuth2RestOperations restTemplate(OAuth2ClientContext oauth2ClientContext) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resource(), oauth2ClientContext);
oAuth2RestTemplate.setAccessTokenProvider(new CustomResourceOwnerPasswordAccessTokenProvider());
return oAuth2RestTemplate;
}
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setId(configProperties.getClientId());
resource.setAccessTokenUri(configProperties.getAccessTokenUri());
resource.setClientId(configProperties.getClientId());
resource.setClientSecret(configProperties.getClientSecret());
resource.setGrantType(configProperties.getGrantType());
resource.setClientAuthenticationScheme(AuthenticationScheme.header);
resource.setAuthenticationScheme(AuthenticationScheme.header); //
resource.setUsername(configProperties.getUsername());
resource.setPassword(configProperties.getPassword());
return resource;
}
}
My serviceImpl method is
#Autowired
#Qualifier("oauth2AuthServer")
private OAuth2RestOperations oauth2RestOperations;
RequestResponse callResourceServer(ResourceRequest request) {
try {
RequestResponse response;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<ReseourceRequest> entity = new HttpEntity<>(request, headers);
response = this.oauth2RestOperations.postForObject(
microServiceConfig.getUrl(),
entity,
RequestResponse.class
);
return response;
} catch (Exception ex) {
log.error(ex);
throw new exception("error");
}
}
I see BaseOAuth2ProtectedResourceDetails and Oauth2RestTemplate deprecated, Can we still use them ? or we should migrate to 5.x options

Create route in Spring Cloud Gateway with OAuth2 Resource Owner Password grant type

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.
I implemented authorization-grant-type: password using WebClientHttpRoutingFilter.
By default, spring cloud gateway use Netty Routing Filter but there is an alternative that not requires Netty (https://cloud.spring.io/spring-cloud-gateway/reference/html/#the-netty-routing-filter)
WebClientHttpRoutingFilter uses WebClient for route the requests.
The WebClient can be configured with a ReactiveOAuth2AuthorizedClientManager through of an ExchangeFilterFunction (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webclient). The ReactiveOAuth2AuthorizedClientManager will be responsible of management the access/refresh tokens and will do all the hard work for you
Here you can review this implementation. In addition, I implemented the client-credentials grant with this approach

Connect Spring Authorization server to external IDP and trigger authentication

We created an authorization server with JDBC backend token store. A similar implementation is hosted on GitHub.
It is working perfectly fine in our environment using different grant types. Different web applications use this for SSO, and it issues tokens, which are then used to consume API as well.
We need a way to log a user in, and issue token if the user is returned as authenticated from external IDP, kind of simulating a user logging in manually from the login form.
We have to extend this server with external IDP authentication. So if a user is connected to their domain network, and has ADFS (as an example), expected flow is as follows:
User tries to access a client app
Redirected to authorization server
Instead of entering credentials user can click on a button to authenticate via ADFS (this can be automated too later on)
ADFS should return authentication ok, with user information
Trigger login of that user in the authorization server, so that an OAuth2 token is issued, and redirected back to the client app
We have tried multiple ways to achieve it, and have referred to multiple resources online, but no success yet. Please note that we do not have the need to connect to social media IDP, rather we have to consume response from enterprise-grade like ADFS, One-login etc.
Any initial pointers would be much appreciated.
To authenticate with GitHub and generate spring token which can be used downstream application we can change our codes like below.
In WebSecurityConfigurerAdapter add below code additional to configure(HttpSecurity http)
http.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class).addFilter(customBasicAuthFilter);
then in WebSecurityConfigurerAdapter again
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(github(), "/login/github"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationFilter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
oAuth2ClientAuthenticationFilter.setRestTemplate(oAuth2RestTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(oAuth2RestTemplate);
oAuth2ClientAuthenticationFilter.setTokenServices(tokenServices);
return oAuth2ClientAuthenticationFilter;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManager);
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder);
}
add one class ClientResources
class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
additional to all we need to add GitHub setting in our application.
github.client.clientId = <<Clientid>>
github.client.clientSecret = <<clientSecret>>
github.client.accessTokenUri = https://github.com/login/oauth/access_token
github.client.userAuthorizationUri = https://github.com/login/oauth/authorize
github.client.clientAuthenticationScheme = form
github.resource.userInfoUri = https://api.github.com/user
logging.level.org.springframework.security = DEBUG
Similar way you can do it for other which supports OAuth. I am also exploring for working with ADFS authentication. Query posted on Stackoverflow for the same.

Unable to Refresh Token using Spring ClientCredentialsResourceDetails

I am using ClientCredentialsResourceDetails to setup my OAuth2RestTemplate object.
My OAuth2 enabled api is sending me back refresh_token in the response, which can be used to refresh access tokens.
However, Spring does not implement token refresh for ClientCredentialsResourceDetails object as it can be seen here
Is there a way to get token refresh working or am I doing anything incorrect ?
FYI, this is my piece of spring code for oauth beans
#Bean
#Primary
public OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails() {
final ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setClientId(clientKey);
resourceDetails.setClientSecret(clientSecret);
final URI accessTokenUri = UriComponentsBuilder.newInstance()
.host(host)
.path(tokenUrlPath)
.scheme(scheme)
.build()
.toUri();
resourceDetails.setAccessTokenUri(accessTokenUri.toString());
resourceDetails.setClientAuthenticationScheme(AuthenticationScheme.header);
return resourceDetails;
}
#Bean
public OAuth2RestTemplate oAuth2RestTemplate(
OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails
) {
final OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails);
oAuth2RestTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
return oAuth2RestTemplate;
}
It's because "4.4.3. Access Token Response" in RFC 6749 (The OAuth 2.0 Authorization Framework) says as follows:
If the access token request is valid and authorized, the authorization server issues an access token as described in Section 5.1. A refresh token SHOULD NOT be included. If the request failed client authentication or is invalid, the authorization server returns an error response as described in Section 5.2.
In short, Client Credentials Flow should not issue a refresh token. Spring complies with the requirement.

Resources