Search code examples
javaspring-bootspring-securityoauth-2.0openid-connect

Spring Boot security/OAuth2 - discard remote authentication and restore local once accessed remote userInfo endpoint


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.


Solution

  • 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.

    1. 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.

    2. 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.