MLugo
MLugo

Reputation: 93

Spring Boot, OAuth2 authentication is lost between requests

EDIT:

log from org.springframework.security:

2022-01-17 12:31:03.495 IST
2022-01-17 10:31:03.495 DEBUG [080-exec-5] o.s.s.w.s.SessionManagementFilter - Request requested invalid session id D5F8BA31A3D7466AK3K3C8EA26A4F037
Default

2022-01-17 12:31:03.495 IST
2022-01-17 10:31:03.495 DEBUG [080-exec-5] o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
Debug

2022-01-17 12:31:03.495 IST
"Request requested invalid session id D5F8BA31A3D7466AK3K3C8EA26A4F037"
Debug

2022-01-17 12:31:03.495 IST
"Set SecurityContextHolder to anonymous SecurityContext"
Default

2022-01-17 12:31:03.494 IST
2022-01-17 10:31:03.494 DEBUG [080-exec-5] o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
Debug

2022-01-17 12:31:03.494 IST
"Set SecurityContextHolder to empty SecurityContext"
Default

2022-01-17 12:31:03.493 IST
2022-01-17 10:31:03.493 DEBUG [080-exec-5] o.s.security.web.FilterChainProxy - Securing GET /logo192.png
Debug

2022-01-17 12:31:03.493 IST
"Securing GET /logo192.png"

***But if I look in the logs some requests after I can get the valid auth:

Debug 2022-01-17 12:31:03.945 IST "Set SecurityContextHolder to SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=com..security.oauth.CustomOAuth2User@, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=***, SessionId=9438C880A19C93AADJI206B9B8B3386], Granted Authorities=[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]]]" Debug

2022-01-17 12:31:03.945 IST "Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=com..security.oauth.CustomOAuth2User@, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=***, SessionId=9438C880A19C93AADJI206B9B8B3386], Granted Authorities=[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]]]" Debug

2022-01-17 12:31:03.945 IST "Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=com..security.oauth.CustomOAuth2User@, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=***, SessionId=9438C880A19C93AADJI206B9B8B3386], Granted Authorities=[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]]]" Default

2022-01-17 12:31:03.944 IST 2022-01-17 10:31:03.944 DEBUG [080-exec-8] o.s.security.web.FilterChainProxy - Securing GET /auth/api/getBasicInfo

it looks like the session id is inconsistent


I use spring security builtin oauth2 social login option, I implemented an OAuth2LoginSuccess class with the onAuthenticationSuccess method and inside of it I fetch the user the corresponds to the social id I got from the oauth:

CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
int sociald = oAuth2User.getAttribute("id");
User user = usersUtils.getUserBySocailId(socialId);
enter code here
// add the user details to the Auth
SecurityContextHolder.clearContext();
((OAuth2AuthenticationToken) authentication).setDetails(user);
SecurityContextHolder.getContext().setAuthentication(authentication);

If I debug inside the onAuthenticationSuccess I can see a valid auth with all the user details.

after the login I redirect to the home page and i send a auth get request to the server to check if there is a user logged in.

the problem is that 50% of the times the request is completed successfuly and the user can make authenticated requets.

but the other 50% i get redirected automaticly to Login page and when i check the log is see that Spring boot says that the user is unauthenticated and the auth is lost.

But in the onAuthenticationSuccess i can always see the correct auth.

My ApplicationSecurityConfig looks like this:

    http.csrf().disable().authorizeRequests()
            .antMatchers("/login*", "/signin/**", "/signup/**", "/oauth2/**").permitAll()
            .antMatchers(Constants.ADMIN_PREFIX + "/**").hasRole("ADMIN")
            .antMatchers(Constants.AUTH_PREFIX + "/**").hasAnyRole("ADMIN", "USER")
            .antMatchers(Constants.PUBLIC_PREFIX + "/**").permitAll()
            .anyRequest().permitAll()
            .and()
            .exceptionHandling().authenticationEntryPoint(new UnauthenticatedRequestHandler())
            .and()
            .formLogin()
            .passwordParameter("password")
            .usernameParameter("email")
            .loginPage("/Login")
            .loginProcessingUrl("/loginSecure").permitAll().successHandler(new LoginSuccess()).failureHandler(new FailureSuccess())
            .and()
            .oauth2Login()
            .loginPage("/Login")
            .userInfoEndpoint()
            .userService(oAuth2UserService)
            .and()
            .successHandler(new OAuth2LoginSuccess())
            .and()
            .rememberMe()
            .rememberMeParameter("remember-me")
            .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(21))
.userDetailsService(this.applicationUserService)
            .and()
            .logout()
         .clearAuthentication(true).invalidateHttpSession(true).logoutSuccessUrl("/login")
            .addLogoutHandler(new CustomLogOutHandler());

And this is the function i check if the user is logged in:

   @GetMapping(Constants.AUTH_PREFIX + "/checkUserLogged")
public Integer checkUserLogged(Authentication authentication,HttpServletRequest request) {
    try{
        if (authentication != null) {
            User (User) authentication.getDetails();
            if (user == null) {
                return -1;
            }
            return user.getId();
        }
    }
    catch (Exception e){
        logger.warning(e.getLocalizedMessage());
    }
    return -1;
}

but when the problem occur it dosen't get to run the controller because spring security return unauthrozed error before.

Thank you in advance for your help

Upvotes: 6

Views: 2478

Answers (2)

MLugo
MLugo

Reputation: 93

I found the solution, I hope this could help.

The thing that caused the problem for me was that GCP and GAE use multiple instances of the server, and if the user is logged in a certain instance does not mean the other instances are familiar with it too because the Spring HTTPSession is in-memory.

I Switched the Session platform to use the spring-session jdbc using the following configuration in the application.properties :

spring.session.store-type=jdbc

-- you can use redis instead of jdbc, as long as the session is stored in a shared place among all instances.

also added the transaction manager to the SecurtityConfig:

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

and added the following configurations :

    http.csrf().disable()
            .sessionManagement()
            .maximumSessions(1)
            .and()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)

In addition like @stringy05 mentioned the authrizenClient Repository needs ti be updated too:

    /**
 * Use the servlet container session store for authorized OAuth2 Clients
 */
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository() {
    return new HttpSessionOAuth2AuthorizedClientRepository();
}

and add the .authorizedClientRepository line to the httpconfig:

....
                .oauth2Login()
            .loginPage("/Login")
            .authorizedClientRepository(authorizedClientRepository)
            .authorizationEndpoint().and()
            .userInfoEndpoint()
            .userService(oAuth2UserService)
            .and()
            .successHandler(new OAuth2LoginSuccess())

....

Regarding the GAE, I added the following line to the app.yaml file:

  network:
    session_affinity: true

Upvotes: 1

stringy05
stringy05

Reputation: 7067

This isn't an answer, however too long for a comment..

It looks like the session is getting lost for some reason, definitely focus on that.

In a default Spring Boot config the session is managed by the underlying servlet container, so its worth checking that is functioning properly. Things to check:

  • Are you running more than 1 app server node? If so, ensure the session is using some sort of cluster aware config (ie Redis / JDBC), local session will fail here for sure
  • It's worth checking the defaults with OAuth2 login in Spring Boot. eg you could try and specify the OAuth2 session using the HttpSessionOAuth2AuthorizedClientRepository and a SpringSessionBackedSessionRegistry

Basically enable all the logs and try and observe the session states from the servlet container when the problem occurs.

Getting the oauth2 session working correctly can be non-trivial, especially given there are not many good blog / docs that describe what spring boot is doing.

That said, here's an example of a working Redis backed Spring Boot config with OAuth 2 login, which might be useful as a reference for you:

app config:

spring:
  session:
    store-type: redis
    redis:
      namespace: sample:api
      flush-mode: immediate
  redis:
    host: localhost
    port: 6379
  security:
    oauth2:
      client:
        registration:
# add your oauth2 client details here 
public class SecurityConfig<S extends Session> extends WebSecurityConfigurerAdapter {

    private final ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    private RedisIndexedSessionRepository redisIndexedSessionRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors(Customizer.withDefaults())
                .sessionManagement()
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
                .and()
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                .and()
                .authorizeRequests(
                        a -> a.antMatchers("/api/login/callback").permitAll().anyRequest().authenticated())
                .oauth2Login()
                .authorizationEndpoint()
                .authorizationRequestResolver(
                        new DefaultOauth2AuthorizationRequestResolver(
                                this.clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI))
                .and()
                .defaultSuccessUrl("http://localhost:3000/users")
                .authorizedClientRepository(authorizedClientRepository());
    }

    @Bean
    public SpringSessionBackedSessionRegistry<?> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(this.redisIndexedSessionRepository);
    }

    /**
     * Use the servlet container session store for authorized OAuth2 Clients
     */
    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository() {
        return new HttpSessionOAuth2AuthorizedClientRepository();
    }

    /**
     * specify CORS to work from SPA UI
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(List.of("http://localhost:3000*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Configuration
    public static class HttpSessionEventPublisherConfig {

        /**
         * enables session expiry notification
         *
         * <p>Needs to be declared in a different class from the `SpringSessionBackedSessionRegistry` to
         * avoid a circular dependency
         */
        @Bean
        public HttpSessionEventPublisher httpSessionEventPublisher() {
            return new HttpSessionEventPublisher();
        }
    }

Upvotes: 1

Related Questions