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

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

Related

Log OAuth calls made by the webclient library

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();
}

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

ReactiveSpringSecurity role mapping

Based on this example https://blog.jdriven.com/2019/11/spring-cloud-gateway-with-openid-connect-and-token-relay I'm trying to use Spring Gateway, the latest version of Spring Security and Keycloak. Behind the Gateway there is a static application that I want to limit access to. I managed to configure everything so that authentication works. However, I am unable to validate the role correctly. Spring Security does not read it from the token and always assigns ROLE_USER. Token contains proper role and other parameters like username or scopes are read correctly. How to map a roles using ReactiveSecurity. Below is my configuration.
#Configuration
public class SecurityConfig {
#Bean
public OidcClientInitiatedServerLogoutSuccessHandler logoutSuccessHandler(#Value("${postLogoutRedirectUrl}") URI postLogoutRedirectUrl, ReactiveClientRegistrationRepository clientRegistrationRepository) {
OidcClientInitiatedServerLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri(postLogoutRedirectUrl);
return logoutSuccessHandler;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(#Value("${securedPaths}") String[] securedPaths, ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
OidcClientInitiatedServerLogoutSuccessHandler logoutSuccessHandler) {
http.oauth2Login();
http.logout(logout -> logout.logoutSuccessHandler(new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
http.authorizeExchange()
.pathMatchers(securedPaths).hasRole("admin")
.anyExchange().permitAll();
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler());
http.csrf().disable();
return http.build();
}
}

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.

Spring Cloud RestTemplate add auth token

How to correctly implement restTemplate with authorisation token?
I have a Zuul gateway which passes a JWT downstream to other services correctly, assuming I don't want to do anything on the gateway first, using a config like:
zuul:
sensitive-headers:
routes:
instance-service:
path: /instances/**
strip-prefix: false
And using a token relay filter:
#Component
public class TokenRelayFilter extends ZuulFilter {
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Set<String> headers = (Set<String>) ctx.get("ignoredHeaders");
headers.remove("authorization");
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 10000;
}
}
Which just forwards everything to the instance-service, works a treat.
However if I remove the routes config from the config.yml file because I want to handle some things on the gateway before manually calling the service I loose the access token and get a 401 back from the downstream services
#ApiOperation(value = "List all instances and their properties.")
#GetMapping("/instances")
public ResponseEntity<String> instances() {
ParameterizedTypeReference<String> reference = new ParameterizedTypeReference<String>() {
};
return restTemplate.exchange("http://instance-service", HttpMethod.GET, null, reference);
}
My RestTemplate is just wired up generically
#Configuration
public class MyConfiguration {
#LoadBalanced
#Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
How do I correctly get the JWT back into the new RestTemplate without having to manually create and add a header in each request?
Am I supposed to be using OAuth2RestTemplate?
After some discussion, it seems like you have two options:
Implement and endpoint and dig the Auth header out via #RequestParam on request. From there, you can add it back on for the subsequent outbound request via RestTemplate to your downstream service.
Use Zuul to proxy your request (Auth header included, make sure its excluded from the sensitive-headers config) and implement a pre filter to include any additional logic you might need.
If I had to pick, it sounds like something Zuul should be doing since it's likely acting as your gateway for both your queue and other services, and it looks like you are trying to implement a proxy request, which Zuul can already do, but it's tough to say without knowing the full scope of the architecture.

Resources