I'm having a problem implementing OpenID connect built on Spring Security Oauth2 library. (Read more about the problem in a separate question.) While researching it, I read the documentation for the @EnableOauth2Client
annotation, which says:
Enable configuration for an OAuth2 client in a web application that uses Spring Security and wants to use the Authorization Code Grant from one or more OAuth2 Authorization servers. To take advantage of this feature you need a global servlet filter in your application of the DelegatingFilterProxy that delegates to a bean named "oauth2ClientContextFilter". Once that filter is in place your client app can use another bean provided by this annotation (an AccessTokenRequest) to create an OAuth2RestTemplate, e.g.
@Configuration @EnableOAuth2Client public class RemoteResourceConfiguration { @Bean public OAuth2RestOperations restTemplate(OAuth2ClientContext oauth2ClientContext) { return new OAuth2RestTemplate(remote(), oauth2ClientContext); } }
Client apps that use client credentials grants do not need the AccessTokenRequest or the scoped RestOperations (the state is global for the app), but they should still use the filter to trigger the OAuth2RestOperations to obtain a token when necessary. Apps that us [sic] password grants need to set the authentication properties in the OAuth2ProtectedResourceDetails before using the RestOperations, and this means the resource details themselves also have to be per session (assuming there are multiple users in the system).
A Note About Versions and Documentation: this documentation is the 2.0.4 release, which is all that is linked to from the Spring Security project page even for the newer 2.3.5 link, which my project is using. Our other Spring versions: Spring Boot 1.3.0, Spring Security 3.2.5, Spring Framework 4.2.3.
I don't understand quite what it means, particularly
a global servlet filter in your application of the DelegatingFilterProxy that delegates to a bean named "oauth2ClientContextFilter"
Here is how we are configuring our rest template.
@Configuration
@EnableOAuth2Client
public class OpenIdConnectConfig {
@Bean
public OAuth2ProtectedResourceDetails openIdResourceDetails() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
details.setScope(oidcScopes);
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(false);
return details;
}
@Bean(name = "my.company.ui.security.OpenIdRestTemplate")
// ToDo: fix org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active for the current thread
public OAuth2RestTemplate OpenIdRestTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(openIdResourceDetails(), clientContext);
}
@Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
@Value("${oidc.clientId}")
private String clientId;
@Value("${oidc.clientSecret}")
private String clientSecret;
@Value("${oidc.accessTokenUrl}")
private String accessTokenUri;
@Value("${oidc.userAuthorizationUri}")
private String userAuthorizationUri;
@Value("${oidc.redirectUri}")
private String redirectUri;
@Value("#{'${oidc.scopes}'.split(',')}")
private List<String> oidcScopes;
}
The filter that performs the authentication (some exception handling and user processing code removed):
public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {
public OpenIdConnectFilter(
RequestMatcher requiresAuthenticationRequestMatcher,
AuthenticationService authenticationService
) {
super(requiresAuthenticationRequestMatcher);
setAuthenticationManager(new NoopAuthenticationManager());
}
@SuppressWarnings("RedundantThrows") // Matching overridden method
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException, IOException, ServletException {
// Required parameters (one-time access code, state) are retrieved from the context
OAuth2AccessToken oAuth2AccessToken = restTemplate.getAccessToken();
// Process the token, get the user details, return an Authentication object.
}
public void setRestTemplate(OAuth2RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
private static class NoopAuthenticationManager implements AuthenticationManager {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
throw new UnsupportedOperationException("No authentication should be done with this AuthenticationManager");
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdConnectFilter.class);
@Value("${oidc.clientId}")
private String clientId;
@Value("${oidc.issuer}")
private String issuer;
@Value("${oidc.jwt.jwk.url}")
private String jwkUrl;
private final AuthenticationService authenticationService;
private OAuth2RestTemplate restTemplate;
}
And the Security Config that sets up the Spring Security FilterProxyChain:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
@SuppressWarnings("unchecked")
protected void configure(HttpSecurity http)
throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.csrf()
.disable()
.authorizeRequests()
.expressionHandler(securityExpressionHandler)
.antMatchers("/asset/**").access("permitAll")
.antMatchers("/ws/ssoEnabled").access("permitAll")
.antMatchers("/**").access("hasRole('ROLE_USER') or hasRole('ROLE_TOKEN_ACCESS')")
.and()
.httpBasic()
.authenticationEntryPoint(ajaxAwareLoginUrlAuthenticationEntryPoint)
.and()
// Handles unauthenticated requests, catching UserRedirectRequiredExceptions and redirecting to OAuth provider
.addFilterAfter(new OAuth2ClientContextFilter(), SecurityContextPersistenceFilter.class)
// Handles the oauth callback, exchanging the one-time code for a durable token
.addFilterAfter(openIdConnectFilter, OAuth2ClientContextFilter.class)
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/logincheck")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(ajaxAwareAuthenticationSuccessHandler)
.failureHandler(ajaxAwareAuthenticationFailureHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.and()
.rememberMe()
.rememberMeServices(rememberMeServices)
// Even though this key has been added directly to the rememberMeServices instance, the RememberMeConfigurer
// can instantiate a new RememberMeServices with a made-up key if the same key is not provided.
.key("the key value")
;
// We do not configure a bean for the SessionAuthenticationStrategy. We want to use the Spring default strategy,
// which is configured by the above builder chain. In order to share the correct, configured instance with our
// custom OpenIdConnectFilter, we first tell the builder to perform the configuration (normally this would be
// done long after this method returns)...
http.getConfigurer(SessionManagementConfigurer.class).init(http);
// ... then we get the shared object by interface (SessionAuthenticationStrategy) class name...
final SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
// ... then set it in our custom filter.
openIdConnectFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
}
You'll notice multiple authentication mechanisms in there. We're in the process of migrating from Form Login to Oauth login and there is a feature flag in place for the initial release. The flag is working quite well, except for the problem described in my linked question at the top that occurs for a while after flipping the flag, then seems to resolve itself.
Is the configuration I've shown above sufficient fulfillment of the documented instructions for the @EnableOauth2Client
annotation?
Or is there something else I need to do with a DelegationFilterProxy
? If so, how?