Spring Boot: Calling an OAuth2 protected REST service - java

I have an existing REST API built using Spring Boot. On one of my functions on the service layer, I need to call an external REST service that is protected by OAuth2 (client-credentials).
Using Spring Boot 2.3, I realized OAuth2RestTemplate is deprecated, so I went with using WebClient.
Following this tutorial - https://www.baeldung.com/spring-webclient-oauth2, I now have my WebClientConfig class as follows:
#Configuration
class WebClientConfig {
#Bean
fun webClient(
clientRegistrations: ClientRegistrationRepository?,
authorizedClients: OAuth2AuthorizedClientRepository?): WebClient? {
val oauth2 = ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients)
oauth2.setDefaultOAuth2AuthorizedClient(false)
oauth2.setDefaultClientRegistrationId("test")
return WebClient.builder()
.apply(oauth2.oauth2Configuration())
.build()
}
}
And in my properties file, I have:
spring:
security:
oauth2:
client:
registration:
test:
client-id: <redacted>
client-secret: <redacted>
authorization-grant-type: client_credentials
provider:
test:
token-uri: <redacted>
I can't even tell if this is working or not, because I keep getting the following error when accessing a different endpoint on my API that has nothing to do with this OAuth2 authentication:
java.lang.IllegalArgumentException: Invalid Authorization Grant Type (client_credentials) for Client Registration with Id: test
I'm at my wits end because I can't overcome this issue... any help would be very appreciated! Thanks!

This is working for me:
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("test");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}

Related

Spring cloud Gateway with keycloak logout not work

I am trying to build application in which I am using Keycloak configuration and spring security with spring cloud gateway everything is working fine but when I am trying to logout it is not working.
Spring security configuration is as below:
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:8280/auth/realms/Default
user-name-attribute: preferred_username
authorization-grant-type: authorization_code
registration:
keycloak:
client-id: Default123
client-secret: Wk79csSdfgdffomzVX2nTlb2boYT9NrW
redirect-uri: http://localhost:9000/login/oauth2/code/keycloak
scope: openid
ANd Security Config file is as below:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
#Bean
public ServerLogoutSuccessHandler keycloakLogoutSuccessHandler(ReactiveClientRegistrationRepository repository) {
OidcClientInitiatedServerLogoutSuccessHandler successHandler = new OidcClientInitiatedServerLogoutSuccessHandler(repository);
successHandler.setPostLogoutRedirectUri("http://localhost:9000/app/logout");
return successHandler;
}
private ServerLogoutHandler logoutHandler() {
return new DelegatingServerLogoutHandler(new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler());
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ServerLogoutSuccessHandler handler) {
// Authenticate through configured OpenID Provider
http.authorizeExchange()
.pathMatchers("/app/logout").permitAll()
.pathMatchers("/app/").authenticated().and().cors().and().oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutHandler(logoutHandler()).logoutSuccessHandler(handler));
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
// Allow showing /home within a frame
http.headers().frameOptions().mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
}
I am not sure why it is not login out what configuration we are missing. Please Help.

How to get spring security context in reactive webflux

I have a problem with my spring boot application (version 2.6.3).
I have configured reactive spring security like there:
MyApplication.java:
#SpringBootApplication
#EnableWebFlux
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http, final ReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.cors().and()
.logout().disable()
.formLogin().disable()
.oauth2ResourceServer()
.opaqueToken()
.introspector(reactiveOpaqueTokenIntrospector)
.and().and()
.csrf()
.disable()
.build();
}
}
And this is my web resource (controller):
MyWebResource.java:
#RestController
public class MyWebResource implements MyWebResourceApi {
#PreAuthorize("hasRole('ROLE_USER')")
#Override
public Mono<String> details(String userId, ServerWebExchange exchange) {
return exchange.getPrincipal().map(Principal::getName);
}
}
It's work fine, when my access token is expired or incorrect the request should be denied. However when PreAuthorized allow request, my user principal will be never resolved in my exchange...
In reactive application authentication information is stored in the Reactive flow and accessible from Mono/Flux. You could use ReactiveSecurityContextHolder to obtain the currently authenticated principal, or an authentication request token.
#PreAuthorize("hasRole('ROLE_USER')")
public Mono<String> details() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ((Principal) ctx.getAuthentication().getPrincipal()).getName());
}
I found this answer looking for a way to add the access token to my webclient requests. If we are using OAuth2 or OpenID Connect and want to access the token instead of the principal's name, then this is not possible via the principal in the security context.
Instead we need to create a ServerOAuth2AuthorizedClientExchangeFilterFunction and register it as a filter function to the WebClient:
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
where clientRegistrations is an injectabile bean of type ReactiveClientRegistrationRepository and authorizedClients is an injectable bean of type ServerOAuth2AuthorizedClientRepository.
We can then use the filters method for the builder to add our filter function to the exchangeFilterFunctions:
WebClient.builder()
.filters(exchangeFilterFunctions -> {
exchangeFilterFunctions.add(oauth);
})
.build();
Baeldung has a nice background article about this, which explains it in more detail: https://www.baeldung.com/spring-webclient-oauth2

Getting 403 Forbidden for WebFluxTest in Oauth2 Secured (Client Credentials) Resource Server Application

I have a reactive(Spring WebFlux) web-application where I am having few REST APIs which are protected resources.(Oauth2) . To access them manually, I need to get an authorization token with client credentials grant type and use that token in the request.
Now, I need to write tests where I can invoke the APIs by making a call through Spring's WebTestClient. I am getting 403 forbidden on trying to access the API. Where am I doing wrong when writing the test case.
Below is my security configuration:
#EnableWebFluxSecurity
public class WebSecurityConfiguration {
#Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers(ACTUATOR_ENDPOINT_PATTERN)
.permitAll()
.pathMatchers("/my/api/*")
.hasAuthority("SCOPE_myApi")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
http.addFilterAfter(new SomeFilter(), SecurityWebFiltersOrder.AUTHORIZATION);
return http.build();
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder().filter(oauth).build();
}
}
Note:- I need this webclient bean because inside that filter (which I added to the SecurityWebFilterChain) I am calling another protected resource/API and the response of that API is being set in the reactive context
My application yaml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${oidc-issuer-uri}
client:
provider:
myProvider:
issuer-uri: ${oidc-issuer-uri}
registration:
myProvider:
client-id: another-service-client
client-secret: ${another-service-clientSecret}
scope: anotherServiceScope
authorization-grant-type: client_credentials
My Controller:
#RestController
public class MyController {
#GetMapping(value = "/my/api/greet")
public Mono<String> greet() {
return Mono.subscriberContext()
.flatMap(context -> {
String someVal = context.get("MY_CONTEXT"); //This context is being set inside the filter 'SomeFilter'
//Use this someVal
return Mono.just("Hello World");
});
}
}
My Test Case:
#RunWith(SpringRunner.class)
#WebFluxTest(controllers = {MyController.class})
#Import({WebSecurityConfiguration.class})
#WithMockUser
public class MyControllerTest {
#Autowired
private WebTestClient webTestClient;
#Test
public void test_greet() throws Exception {
webTestClient.mutateWith(csrf()).get()
.uri("/my/api/greet")
.exchange()
.expectStatus().isOk();
}
}
Note:- I cannot bypass by not using my WebSecurityConfiguration class. Because the reactive context is being set in the filter which is added in the websecurityconfiguration.
2 things are required here:
First to access the /my/api/greet, the webTestClient needs SCOPE_myApi and since no "user" is involved here so we dont need #WithMockUser
#Test
public void test_greet() {
webTestClient
.mutateWith(mockOidcLogin().authorities(new SimpleGrantedAuthority("SCOPE_myApi")))
.get()
.uri("/my/api/greet")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("mockSasToken");
}
Next we need a wiremock server to mock the response of the "another service"
For this one option is to use spring boot #AutoConfigureWireMock(port = 0) to automatically boot up a wiremock server and shutdown for us at a random port.
Next we stub the response for the "another service" and the Oauth2 token endpoint in the test method.
Lastly, we need a "test" spring profile and a corresponding application-test.yaml where we tell spring to use the wiremock endpoints to fetch token:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:${wiremock.server.port}/.well-known/jwks_uri
client:
provider:
myProvider:
token-uri: http://localhost:${wiremock.server.port}/.well-known/token
registration:
myProvider:
client-id: mockClient
client-secret: mockSecret

How to create Feign Oauth2RequestIntercetor using WebClient with Spring Security 5.2.x?

I am using Feign client in my application with Spring Security 5.2 which is required swapping API bindings from RestTemplate to the WebClient. I configured my web client to use web client with OAuth2AuthorizedClientManager to manage access token provided by client_credentials flow. And now I want to create Feign's RequestInterceptor for my Feign client which will use this WebClient. How do I do this?
Here's my WebClientConfiguration:
#Configuration
public class WebClientConfig {
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService clientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, clientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
WebClient azureGraphAPIWebClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("registration-id");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
}

Why doesn't Spring use my PrincipalExtractor bean?

Spring doesn't want to use my PrincipalExtractor bean. Instead it uses default FixedPrincipalExtractor.
I'm trying to follow Spring's tutorial to OAuth2:
https://spring.io/guides/tutorials/spring-boot-oauth2/
And everything went almost fine untill I decided to save an authenticated user to my database. The tutorial simply says: "It's too easy, so we won't show how to do this". Of course that is a moment where I've been stuck for days.
There is WebSecurityConfig class. It's a mess but it's used for educational purposes.
#Configuration
#EnableWebSecurity
#EnableOAuth2Client
#RestController
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**", "/js/**", "/error**", "/webjars/**").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(google(), "/login/google"));
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;
}
#Bean
#ConfigurationProperties("google")
public ClientResources google() {
return new ClientResources();
}
#Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
public PrincipalExtractor principalExtractor(UserDetailsRepo userDetailsRepo) {
return map -> {
String id = (String) map.get("sub");
User user = userDetailsRepo.findById(id).orElseGet(() -> {
User newUser = new User();
newUser.setId(id);
newUser.setEmail((String) map.get("email"));
// and so on...
return newUser;
});
return userDetailsRepo.save(user);
};
}
}
class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
And application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost/my_db
username: postgres
password: password
jpa:
generate-ddl: true
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
google:
client:
clientId: 437986124027-7072jmbsba04d11fft0h9megkqcpem2t.apps.googleusercontent.com
clientSecret: ${clientSecret}
accessTokenUri: https://www.googleapis.com/oauth2/v4/token
userAuthorizationUri: https://accounts.google.com/o/oauth2/v2/auth
clientAuthenticationScheme: form
scope: openid,email,profile
resource:
userInfoUri: https://www.googleapis.com/oauth2/v3/userinfo
preferTokenInfo: true
As I wrote above, Spring doesn't really want to use my PrincipalExtractor bean and uses default FixedPrincipalExtractor instead. I've spent a lot of time trying to solve this issue but nothing helps. Except for changing application.yml like this:
security:
oauth2:
client:
clientId: 620652621050-v6a9uqrjq0ejspm5oqbek48sl6od55gt.apps.googleusercontent.com
clientSecret: ${clientSecret}
[...]
resource:
userInfoUri: https://www.googleapis.com/oauth2/v3/userinfo
preferTokenInfo: true
There was google.client.clientId and it changes to security.oauth2.client.clientId as you can see.
And if you delete all the filter methods and everything related to them, then it works, yes. It does use my PrincipleExtractor. But how can I add more authentication providers (Facebook, GitHub, etc) and local authentication now?
Finally, I have a few questions:
How to make Spring use my PrincipalExtractor?
Should I use PrincipalExtractor at all? Maybe there is another way to do the same?
Is something wrong with my application.yml?
Things I tried:
Adding the #EnableAuthorizationServer (Why is my spring #bean never instantiated?)
Nothing changes.
Adding ResourceServerTokenServices (PrincipalExtractor and AuthoritiesExtractor doesn't hit)
Spring can't find UserInfoRestTemplateFactory. Adding the bean manually is not right I guess, and simply doesn't work.
Many different solutions. None of them worked.
When you are defining your ssoFilter add something like this:
tokenServices.setPrincipalExtractor(myCustomPrincipalExtractor());
Bonus: the same goes for AuthorityExtractor.

Categories