Exception Handling When Using Spring OAuth2 Client Enabled WebClient - spring-boot

I have an OAuth2 enabled WebClient configured using Spring's very convenient spring-boot-starter-oauth2-client library. It works perfectly in terms of getting and managing tokens when I make requests to my OAuth2 secured resource server API
I'd like to add global exception handling to the WebClient to handle exceptions arising from requests to my resource server. An ExchangeFilterFunction following something like the approach outlined in this article looks good.
I'm just wondering, as the WebClient already has a ServletOAuth2AuthorizedClientExchangeFilterFunction applied, would adding additional filters have any impact on the interactions with the OAuth2 authorization server?
Here is my current WebClient config:
#Bean
public WebClient edmsDataIntegrationServiceWebClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("my-auth-server");
return WebClient.builder()
.baseUrl(baeUrl)
.apply(oauth2Client.oauth2Configuration())
.build();
}
So the additional filter would be something like the approach outlined here, where handleResourceServerError() would return an ExchangeFilterFunction that handles exceptions caused by my resource server response:
return WebClient.builder()
.baseUrl(baeUrl)
.apply(oauth2Client.oauth2Configuration())
.filter(WebClientFilter.handleResourceServerError()) //handles exceptions caused by resource server response
.build();
Is the above approach ok without breaking any of the ServletOAuth2AuthorizedClientExchangeFilterFunction functionality?

Related

Reactive Spring: How to pass Access Token from OAuth 2.0 Authorization Code Flow to scheduled task

I'm trying to build an application that periodically fetches data from a Third-Party API that demands a reCAPTCHA protected OAuth 2.0 Authorization Code Flow with PKCE for authentication.
I guess, it wouldn't be a big deal to implement the authorization protocol manually but I'm willing to do that using the Spring Security OAuth Client in the reactive manner.
The goal is to have a scheduled task that fetches the data from the API only being blocked until I manually open up a login page (currently a REST endpoint) in the browser that forwards me to the login page of the API vendor. After successful authentication, the scheduled task should also be able to access the API.
Currently the class structure looks like this:
MyController#showData and MyScheduler#fetchData both call ApiClient#retrieveData which does the final API call using the reactive WebClient from Spring.
The WebClient configuration looks like this:
#Configuration
#EnableWebFluxSecurity
class WebClientConfiguration {
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegs,
ReactiveOAuth2AuthorizedClientService authClientService) {
ReactiveOAuth2AuthorizedClientManager authClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegs, authClientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("test");
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ServerOAuth2AuthorizationRequestResolver resolver) {
http.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
return http.build();
}
#Bean
public ServerOAuth2AuthorizationRequestResolver pkceResolver(
ReactiveClientRegistrationRepository repo) {
DefaultServerOAuth2AuthorizationRequestResolver resolver =
new DefaultServerOAuth2AuthorizationRequestResolver(repo);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
return resolver;
}
}
The authorization works fine. When I open /showData in the browser, I'm redirected to the vendor's login page and when I come back, the requested data is displayed as it should be.
But the Scheduler is still blocked. I guess that has something to do with the Security Context which is linked only to the browser session, but I'm not so familiar with Spring Security to understand how to share the access (and refresh) token within the whole application.
Disclaimer: The Third-Party API has specific endpoints which are explicitly meant to be called periodically and not only on a user's request, but they still demand authorization by Authorization Code instead of Client Credential.
If you want to call third-party API from the scheduler you should pass OAuth2AuthorizedClient to it. That class represent an OAuth2 authorized client and has information about access/refresh token.
Here is the documentation describing how to use it.

Spring Boot Authorization Server + Google OAuth2/OpenId Connect should work with access_token or id_token?

I'm a bit confused regarding whether I should be accessing my Spring Boot Resource Server via an access_token or an id_token.
First, let me quickly explain my setup:
Spring Boot app as an OAuth 2.0 Resource Server. This is configured as described in the Spring docs: Minimal Configuration for JWTs This app provides secured #Controllers that will provide data for a JavaScript SPA (eg. React)
Google's OAuth 2.0 AP / OpenID Connect already configured (Credentials, Client Id, Client Secret)
A JavaScript SPA app (eg. React) that logs the user into Google and makes requests to the Spring Boot Resource Server for secured data. These requests include the Authorization header (with Bearer token obtained from Google) for the logged in user.
For development purposes, I'm also using Postman to make requests to the Spring Boot Resource Server
I can easily configure Postman to get a token from Google. This token response from Google includes values for access_token, id_token, scope, expries_in and token_type.
However, my requests to the Resource Server are denied when Postman tries to use the value from retrieved token's access_token field as the Bearer in the Authorization header
The only way I'm able to successfully access the secured #Controllers is by using the id_token as the Bearer in the Authorization header.
Is it expected that I should use the id_token as the Bearer in the Authorization header? Or is it expected that I should use the access_token?
Some additional relevant info:
The value of the id_token is a JWT token. The value of the access_token is not a JWT token. I know this because I can decode the id_token on jwt.io but it is unable to decode the value of the access_token. Further, the Spring Boot Resource Server fails with the following when I send the access_token as the Bearer in the Authorization header:
An error occurred while attempting to decode the Jwt: Invalid unsecured/JWS/JWE header: Invalid JSON: Unexpected token ɭ� at position 2.
This blog post Understanding identity tokens says the following:
You should not use an identity token to authorize access to an API.
To access an API, you should be using OAuth’s access tokens, which are intended only for the protected resource (API) and come with scoping built-in.
Looking at at the spring-security-samples for using OAuth2 Resource Server, I see the value of there hard-coded access_token (for testing purposes) is indeed a valid JWT. As opposed to the access_token returned from Google which is not a JWT.
In summary:
I can access my Spring Boot Resource Server using the value of the id_token obtained from Google. The value of the access_token is not a JWT and fails to parse by Spring Boot.
Is there something wrong with my understanding, my configuration or what? Does Google's OpenId Connect behave differently regarding how the access_token works?
Happy to clarify or add more info if needed. Thanks for your consideration and your patience!
The blog post you mentioned is correct in my view, and I believe the OpenID Connect 1.0 spec does not intend for an id_token to be used for access purposes.
Like you, I expected that using Google as an Authorization Server would work out of the box, because Spring Security works with Google as a common OAuth2 provider for providing social login. However, this is not the case, and I believe it is not really intended, because Google is not really your authorization server. For example, I don't believe you can configure Google to work with scopes/permissions/authorities of your domain-specific application. This is different from something like Okta, where there are many options for configuring things in your own tenant.
I would actually recommend checking out Spring Authorization Server, and configuring Google as a federated identity provider. I'm working on a sample for this currently and it will be published within the next week or so (see this branch).
Having said that, if you're still interested in a simple use case where Google access tokens are used for authenticating with your resource server, you would need to provide your own opaque token introspector that uses Google's tokeninfo endpoint. It doesn't match what Spring Security expects, so it's a bit involved.
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests((authorizeRequests) -> authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
// #formatter:on
return http.build();
}
#Bean
public OpaqueTokenIntrospector introspector() {
return new GoogleTokenIntrospector("https://oauth2.googleapis.com/tokeninfo");
}
}
public final class GoogleTokenIntrospector implements OpaqueTokenIntrospector {
private final RestTemplate restTemplate = new RestTemplate();
private final String introspectionUri;
public GoogleTokenIntrospector(String introspectionUri) {
this.introspectionUri = introspectionUri;
}
#Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
RequestEntity<?> requestEntity = buildRequest(token);
try {
ResponseEntity<Map<String, Object>> responseEntity = this.restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {});
// TODO: Create and return OAuth2IntrospectionAuthenticatedPrincipal based on response...
} catch (Exception ex) {
throw new BadOpaqueTokenException(ex.getMessage(), ex);
}
}
private RequestEntity<?> buildRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("access_token", token);
return new RequestEntity<>(body, headers, HttpMethod.POST, URI.create(introspectionUri));
}
}
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://accounts.google.com
jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs

403 Forbidden Using Springboot When Hitting Okta Userinfo endpoint

I'm trying to set up Okta as a sign on for a set of subpaths in my spring boot app.
I'm configuring the auth resource details with:
#Bean(name = "oktaOAuthClient")
public AuthorizationCodeResourceDetails oktaOAuthAdminClient(#Qualifier("oktaAdminConfiguration") OktaConfigurationProperties oktaAdminCongfig,
ICredentialsApi credentialsApi) {
String redirectUrl = UriComponentsBuilder.fromUriString("http://localhost:8091/")
.path(ConfigurationRequestPaths.ADMINISTRATION_LANDING)
.build(false)
.toUriString();
AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
client.setClientId(oktaAdminCongfig.getClientId());
client.setClientSecret(oktaAdminCongfig.getClientSecret());
client.setAccessTokenUri(oktaAdminCongfig.getAccessTokenUri());
client.setUserAuthorizationUri(oktaAdminCongfig.getUserAuthorizationUri());
client.setClientAuthenticationScheme(AuthenticationScheme.header);
client.setPreEstablishedRedirectUri(redirectUrl);
client.setScope(OKTA_SCOPES);
client.setUseCurrentUri(false);
client.setScope(OKTA_SCOPES);
return client;
}
These and other settings are found from the application.properties manually and are set as:
okta.admin.clientId={id}
okta.admin.clientSecret={secret}
okta.admin.accessTokenUri=https://dev-{value}.okta.com/oauth2/default/v1/token
okta.admin.userAuthorizationUri=https://dev-{value}.okta.com/oauth2/default/v1/authorize
okta.admin.issuer=https://dev-{value}.okta.com/oauth2/default
okta.admin.userInfoUrl=https://dev-{value}.okta.com/oauth2/default/v1/userinfo
Then I've made a filter with (Note, is the clientId set in the UserTokenInfoServices meant to be the client id from the okta client id/client secret?):
#Bean(name = "oktaFilter")
public Filter oktaFilter(#Qualifier("oktaOAuthClient") AuthorizationCodeResourceDetails oktaOAuthClient,
#Qualifier("oktaOAuthResource") ResourceServerProperties resource,
#Qualifier("oktaOAuthRestTemplate") OAuth2RestTemplate oktaOAuthRestTemplate) {
ExceptionMappingAuthenticationFailureHandler failureHandler = new ExceptionMappingAuthenticationFailureHandler();
failureHandler.setDefaultFailureUrl("/");
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(ConfigurationRequestPaths.ADMINISTRATION_LANDING);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(resource.getUserInfoUri(), oktaOAuthClient.getClientId());
tokenServices.setRestTemplate(oktaOAuthRestTemplate);
filter.setRestTemplate(oktaOAuthRestTemplate);
filter.setTokenServices(tokenServices);
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setUseReferer(true);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationFailureHandler(failureHandler);
return filter;
}
Finally I've set up the WebSecurityConfigurerAdapter with the following:
http.antMatcher("/config/**")
.authorizeRequests()
.antMatchers("/config")
.permitAll()
.anyRequest().authenticated().and()
.exceptionHandling()
.authenticationEntryPoint(oktaLoginHandler)SimpleUrlAuthenticationSuccessHandler(ConfigurationRequestPaths.ADMINISTRATION_LANDING))
.and()
.logout().addLogoutHandler(oktaLogoutHandler).logoutSuccessUrl(externalAccessUrl).permitAll().and()
.addFilterBefore(oktaFilter, BasicAuthenticationFilter.class);
}
The redirect for the subpath works correctly and goes to a sign in page, but I get an error after signing in that warns:
org.springframework.security.authentication.BadCredentialsException: Could not obtain user details from token...Caused by: org.springframework.security.oauth2.common.exceptions.InvalidTokenException:
I believe this likely has to do with getting a 403 when hitting the okta userinfo endpoint:
Request is to process authentication
Retrieving token from https://dev-{value}.okta.com/oauth2/default/v1/token
Encoding and sending form: {grant_type=[authorization_code], code=[{code}], redirect_uri=[http://localhost:8091/config], client_id=[{id}], client_secret=[{secret}]}
HTTP GET https://dev-{value}.okta.com/oauth2/default/v1/userinfo
Accept=[application/json, application/*+json]
Response 403
I've also tried the okta starter but it seems to break when used with another oauth login to github for another set of subpaths in the application. The spring version I'm using doesn't include the .oauthLogin() and other settings for httpsecurity that I've seen some guides on.
edit: Adding my spring dependency list for more clarification:
org.springframework:spring-beans:5.1.20.RELEASE
org.springframework:spring-context:5.1.20.RELEASE
org.springframework:spring-jdbc:5.1.20.RELEASE
org.springframework:spring-tx:5.1.20.RELEASE
org.springframework:spring-web:5.1.20.RELEASE
org.springframework:spring-webmvc:5.1.20.RELEASE
org.springframework:spring-test:5.1.20.RELEASE
org.springframework.boot:spring-boot-actuator:2.1.18.RELEASE
org.springframework.boot:spring-boot-autoconfigure:2.1.18.RELEASE
org.springframework.boot:spring-boot-configuration-processor:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-actuator:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-security:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-thymeleaf:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-web:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-test:2.1.18.RELEASE
org.springframework.retry:spring-retry:1.3.1
org.springframework.security:spring-security-config:5.1.13.RELEASE
org.springframework.security:spring-security-core:5.1.13.RELEASE
org.springframework.security:spring-security-ldap:5.1.13.RELEASE
org.springframework.security:spring-security-web:5.1.13.RELEASE
org.springframework.security.oauth:spring-security-oauth2:2.3.8.RELEASE
org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.1.18.RELEASE
It sounds like you might be using the older Spring Security OAuth project:
spring-security-oauth?
This project has been deprecated. The newer Spring Security OAuth2 modules are great, and they are now first-class citizens, in Spring Security (they live in the official project now). Along with this Spring Boot 1.x is EoL, and is no longer getting patches and security updates.
Most of the guides you are seeing likely reference the newer libraries (e.g. things like .oauthLogin()).
Sorry for the typical StackOverflow answer of "don't do X", but here is what I would recommend:
Update your Spring Boot version
Migrate the newer OAuth libraries
Then add your new logic (this should be much easier after updating)
If you are already on Spring Boot 2 and the newer OAuth lib, let me know, and we can try to figure out why you don't have the newer HttpSecurity methods.

Handling token response in spring oauth2 client

I am using Spring Cloud Gateway as a API Gateway for our system. We would like to delegate all authentication (oauth) to that component. I was looking at the source code of Spring Oauth2 Client but I don't see any place where I can "plug in" to do what I need.
I would like to catch the moment, when the code exchange is successful and make a redirect with id_token and refresh_token in cookie or query param. We don't store any session as well - whole authentication is meant to stateless.
I am configuring SecurityWebFilterChain (security for WebFlux) like this:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(Customizer.withDefaults())
.oauth2Login();
http.csrf().disable();
return http.build();
}
I tried to use successHandler
.oauth2Login(c -> c.authenticationSuccessHandler(successHandler));, but in that moment I don't access to refresh_token (have only WebFilterExchange, Authentication in arguments) and I am not even sure how should I perform the redirect form that place.
Is there any way to achieve this?

How should I use the Spring WebClient to non-interactively access an OAuth protected resource on behalf of another user?

I have a Spring (not Boot) application which has to access non-interactively (in a scheduled task) some 3rd-party resources on behalf of our users. These resources use OAuth 2.0 for authorization. We already have a workflow that gets us the required tokens and are accessing the resources using either Spring Social or our own implementation neither of which is optimal (Spring Social seems to be not maintained, we'd rather use a library than maintain our OAuth "framework").
I'm trying to use the WebClient from Spring Security 5.1, but I'm not sure I'm using it correctly.
The WebClient is created this way:
final ClientRegistration 3rdParty = 3rdParty();
final ReactiveClientRegistrationRepository clientRegistrationRepository =
new InMemoryReactiveClientRegistrationRepository(3rdParty);
final ReactiveOAuth2AuthorizedClientService authorizedClientService =
new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
final ServerOAuth2AuthorizedClientRepository authorizedClientRepository =
new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
final ServerOAuth2AuthorizedClientExchangeFilterFunction autorizedClientExchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository);
return WebClient.builder()
.filter(autorizedClientExchangeFilterFunction)
.build();
and accessing the resource this way works:
final OAuth2AuthorizedClient oAuth2AuthorizedClient = ... // (OAuth2AuthorizedClient with OAuth2AccessToken)
final Mono<SomeResource> someResourceMono = webClient().get()
.uri(3rdpartyUrl)
.attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(oAuth2AuthorizedClient))
.retrieve()
.bodyToMono(SomeResource.class);
The problem is I don't see how the ReactiveClientRegistrationRepository and ServerOAuth2AuthorizedClientRepository are used in this approach. If I have to create a fully populated OAuth2AuthorizedClient to access the resource, why are these repositories needed?
I expected, that I have to pass the clientRegistrationId, some "principalName", implement our ReactiveOAuth2AuthorizedClientService loading OAuth2AuthorizedClient's by "principalName" and let the ServerOAuth2AuthorizedClientRepository do its work, but the only way I see to pass a principal to the WebClient is by using ServerOAuth2AuthorizedClientExchangeFilterFunction#oauth2AuthorizedClient which requires a complete OAuth2AuthorizedClient. Which is the part I'm doing it wrong?
Instead of supplying the OAuth2AuthorizedClient via oauth2AuthorizedClient(), you can also provide the clientRegistrationId via clientRegistrationId() and ServerWebExchange via serverWebExchange(). The combination of the latter 2 options will resolve the Principal from the ServerWebExchange and use both ReactiveClientRegistrationRepository and ServerOAuth2AuthorizedClientRepository to resolve the OAuth2AuthorizedClient. I understand your use-case is a bit different given you are running outside of a request context - this is just a FYI.
...The problem is I don't see how the
ReactiveClientRegistrationRepository and
ServerOAuth2AuthorizedClientRepository are used in this approach
You still need to provide ReactiveClientRegistrationRepository and ServerOAuth2AuthorizedClientRepository as the ServerOAuth2AuthorizedClientExchangeFilterFunction supports the refreshing (authorization_code client) and renewing (client_credentials client) of an expired access token.
For your specific use case, take a look at UnAuthenticatedServerOAuth2AuthorizedClientRepository as this implementation supports WebClient running outside of a request context, e.g. background thread. Here is a sample for your reference.

Resources