I have a Spring boot (2.1.3)/Java based web app using Spring security with a form login. This all works fine.
I now need to connect to a remote service which uses OAuth2/OIDC. From my web app the user clicks a link and is redirected to a page on the remote service, completes some information and then is returned to my site. I receive the access token, verify it and then call the userInfo endpoint to query the additional user attributes.
So far this all partially works, and not requiring any additional customisation of the the Spring classes.
I have implemented an additional WebSecurityConfigurerAdpater to handle the oauth security configuration, and included my registration and remote provider details in a yaml properties file. I am now at the point where the user is logged into my app, clicks the link and is redirected to the remote site. They do what is required and then return. It then breaks - the user authentication has reverted to anonymous.
Update : I have included my security config here, showing the WebSecurityConfigurerAdapters for both the local login and the remote oauth2 connection.
@Configuration
@EnableWebSecurity
public class AppSecurityConfig {
@Autowired
@Qualifier("Basic")
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("bcrypt")
private PasswordEncoderBean passwordEncoderBean;
@Bean
public AppDaoAuthenticationProvider appAuthProvider() {
AppDaoAuthenticationProvider authProvider = new AppDaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoderBean.getEncoder());
return authProvider;
}
@Bean
public AppAuthenticationSuccessHandler appAuthenticationSuccessHandler() {
return new AppAuthenticationSuccessHandler();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(appAuthProvider());
}
@Configuration
@Order(1)
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
@Autowired
public OAuth2LoginSecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/identity/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/identity")
.authorizationEndpoint()
.baseUri("/identity/oauth2/authorization")
.authorizationRequestResolver(
new CustomOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/identity/oauth2/authorization"
))
.and()
.redirectionEndpoint()
.baseUri("/identity/oauth2/code/srvdev")
.and()
.defaultSuccessUrl("/identity/success")
;
}
}
@Configuration
@Order(2)
public static class UserApplConfigurationAdaptor extends WebSecurityConfigurerAdapter {
private final AccessDeniedHandler accessDeniedHandler;
private final AppWebAuthenticationDetailsSource webAuthenticationDetailsSource;
private final AppAuthenticationSuccessHandler appAuthenticationSuccessHandler;
@Autowired
public UserApplConfigurationAdaptor(AccessDeniedHandler accessDeniedHandler,
AppWebAuthenticationDetailsSource webAuthenticationDetailsSource,
AppAuthenticationSuccessHandler appAuthenticationSuccessHandler) {
this.accessDeniedHandler = accessDeniedHandler;
this.webAuthenticationDetailsSource = webAuthenticationDetailsSource;
this.appAuthenticationSuccessHandler = appAuthenticationSuccessHandler;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(
"/",
"/index",
"/signup",
"/homePage*",
"/email*",
"/register",
"/registrationConfirm*",
"/badUser*",
"/forgotPassword*",
"/resetPassword*",
"/changePassword*",
"/confirmaccount*",
"/confirmPasswordAdmin*",
"/savePassword*",
"/setupAuthenticator",
"/QRimage",
"/generalError",
"/icons/**",
"/scss/**",
"/css/**",
"/font/**",
"/img/**",
"/js/**",
"/policydocs/*",
"/favicon.ico").permitAll()
.antMatchers("/error/**").authenticated()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/loginUser").permitAll()
.loginProcessingUrl("/doLoginUser")
.defaultSuccessUrl("/landing")
.authenticationDetailsSource(webAuthenticationDetailsSource)
.successHandler(appAuthenticationSuccessHandler)
.and()
.logout().permitAll().logoutUrl("/logout")
.and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and()
.csrf().disable()
;
}
}
}
Using the debugger, I can see that in the class
org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider
I am able to receive an ID Token, and then use this to get the user attributes from the UserInfo endpoint.
At this point, having retrieved the user attributes, I want to discard this authentication and allow the user to continue using the local web app as before. But the user has now become anonymous and needs to log in again.
Any pointers to how I can achieve this? I think I need to somehow intercept the processing of the OAuth/OIDC authentication and then having obtained the user attributes, discard it and revert to the previous one.
Any pointers appreciated.
Thanks.
After going down several rabbit holes, I have found a solution that appears to work.
Basically I needed to prevent the authentication resulting from a successful OIDC call replacing the authentication of the currently logged in user. You can see where this happens explicitly in the call to successfulAuthentication
in AbstractAuthenticationProcessingFilter
. Tying to override this behaviour by extending the OAuth2LoginAuthenticationFilter
was one of the rabbit holes I went down.
Here is a summary of my solution.
Extended the OidcUserService
class to modify the loadUser
() method. Where the OidcUser object is created (DefaultOidcUser
), I now retrieve the current principal and add it my extended DefaultOidcUser.
I already have an AuthenticationSuccessHandler (an extension of SimpleUrlAuthenticationSuccessHandler
). I have updated this, so that when the authentication is an OidcUser, I retrieve the previous principal from my OidcUser object, create a new authetication using this, and then replace it in the security context.
Probably not the best solution, and seems a bit of a hacky workaround, but with a lack of any other suggestions.