I'm trying to get OIDC working from behind a corporate firewall, and I'm running into some issues.
I am trying to use the Google as my OpenID provider. I have configured an OAuth 2.0 Client ID in Google with type "Web Application".
I have updated my application.yaml and added the following properties:
security:
oauth2:
client:
registration:
google:
client-id: <client-id from google>
client-secret: <client-secret from google>
I have a WebSecurityConfigurerAdapter class with the following configure() method (shamelessly copied from Baeldung):
@Override
protected void configure(HttpSecurity http) throws Exception{
Set<String> googleScopes = new HashSet<>();
googleScopes.add(
"https://www.googleapis.com/auth/userinfo.email");
googleScopes.add(
"https://www.googleapis.com/auth/userinfo.profile");
OidcUserService googleUserService = new OidcUserService();
googleUserService.setAccessibleScopes(googleScopes);
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin
.userInfoEndpoint()
.oidcUserService(googleUserService)
);
}
When I attempt view the application in a browser, everything initially looks good. I get redirected to google to authenticate, and then google redirects back to my application. However it all goes south when the application attempts to exchange the authorization code for a access token.
I get the following error:
[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token
Response: I/O error on POST request for "https://www.googleapis.com/oauth2/v4/token": Software caused
connection abort: recv failed; nested exception is java.net.SocketException: Software caused connection
abort: recv failed
Ok, looks like an issue with the corporate firewall. Cool, no problem I'll just use a RestTemplateCustomizer. So I declare the following beans:
@Bean
HttpHost proxyHost(@Value("${http.proxyHost}") String proxyHost) {
log.debug("Setting proxy host to: '{}'", proxyHost);
return HttpHost.create(proxyHost);
}
@Bean
DefaultProxyRoutePlanner proxyRoutePlanner(HttpHost proxyHost) {
return new DefaultProxyRoutePlanner(proxyHost);
}
@Bean
HttpClient httpClient(DefaultProxyRoutePlanner proxyRoutePlanner) {
return HttpClientBuilder.create().setRoutePlanner(proxyRoutePlanner).build();
}
@Bean
HttpComponentsClientHttpRequestFactory httpRequestFactory(HttpClient httpClient) {
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
@Bean
RestTemplateCustomizer restTemplateCustomizer(HttpComponentsClientHttpRequestFactory httpRequestFactory) {
RestTemplateCustomizer restTemplateCustomizer = (RestTemplate template)->{
log.debug("Returning a customized rest template.");
template.setRequestFactory(httpRequestFactory);
};
return restTemplateCustomizer;
}
This failed to fix the problem. After some debugging, I realized that the DefaultAuthorizationCodeTokenResponseClient is creating its restOperations by calling new RestTemplate() rather than using RestTemplateBuilder, so all of my work to create a RestTemplateCustomizer is for nothing.
So I updated my WebSecurityConfigurerAdapter:
public class OidcSecurityConfigurer extends WebSecurityConfigurerAdapter {
private RestTemplateBuilder restTemplateBuilder;
public OidcSecurityConfigurer(@Autowired RestTemplateBuilder restTemplateBuilder) {
this.restTemplateBuilder = restTemplateBuilder;
}
@Override
protected void configure(HttpSecurity http) throws Exception{
Set<String> googleScopes = new HashSet<>();
googleScopes.add(
"https://www.googleapis.com/auth/userinfo.email");
googleScopes.add(
"https://www.googleapis.com/auth/userinfo.profile");
OidcUserService googleUserService = new OidcUserService();
googleUserService.setAccessibleScopes(googleScopes);
http
.authorizeRequests(
authorizeRequests -> authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(oauthLogin -> oauthLogin
.tokenEndpoint(teCustomizer->{
DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
client.setRestOperations(this.restTemplateBuilder.build());
teCustomizer.accessTokenResponseClient(client);
})
.userInfoEndpoint()
.oidcUserService(googleUserService)
);
}
}
This got a little further. Now it was getting connected to google, but it wasn't able to read the response due to a NullPointerException on line 80 in DefaultAuthorizationCodeTokenResponseClient.java.
java.lang.NullPointerException: null
at org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient.getTokenResponse(DefaultAuthorizationCodeTokenResponseClient.java:80) ~[spring-security-oauth2-client-5.6.3.jar:5.6.3]
at org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient.getTokenResponse(DefaultAuthorizationCodeTokenResponseClient.java:57) ~[spring-security-oauth2-client-5.6.3.jar:5.6.3]
at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.getResponse(OidcAuthorizationCodeAuthenticationProvider.java:170) ~[spring-security-oauth2-client-5.6.3.jar:5.6.3]
at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.authenticate(OidcAuthorizationCodeAuthenticationProvider.java:144) ~[spring-security-oauth2-client-5.6.3.jar:5.6.3]
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-5.6.3.jar:5.6.3]
at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:195) ~[spring-security-oauth2-client-5.6.3.jar:5.6.3]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:223) ~[spring-security-web-5.6.3.jar:5.6.3]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:213) ~[spring-security-web-5.6.3.jar:5.6.3]
Looking at the code, it seems that the token isn't being decoded properly because I didn't add the right messageConverters to the RestTemplate. So updated the RestTemplateCustomizer declaration as follows:
@Bean
RestTemplateCustomizer restTemplateCustomizer(HttpComponentsClientHttpRequestFactory httpRequestFactory) {
RestTemplateCustomizer restTemplateCustomizer = (RestTemplate template)->{
log.debug("Returning a customized rest template.");
template.setRequestFactory(httpRequestFactory);
template.setMessageConverters(
Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()
)
);
template.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
};
return restTemplateCustomizer;
}
Now the access token is being decoded correctly, but the JWT user token is failing. WTH? So I looked at the class NimbusJwtDecoder and it also is calling new RestTemplate(). So I apparently need to figure out how to change the RestTemplate being used by the NimbusJwtDecoder similar to what I did for DefaultAuthorizationCodeTokenResponseClient.
At this point I'm wondering if I'm barking up the wrong tree. This seems like so much work just get the built-in OAuth2 support working. It almost seems like it would be easier to just handle the all the OAuth2 handshake stuff manually rather than using what's built into spring-security. The kicker is that this is primarily an issue that will only occur during development. For a production deployment of the app they can easily open the firewall to allow the application to make outgoing connections to the OpenID provider.
I would appreciate any guidance anyone could give.
Thanks, Dave