I'm building a Spring Boot webapp, and I've encountered a problem. Whenever I first log in with a user I'm still getting that user's credentials with the SecurityContextHolder.getContext().getAuthentication() method, even after logging out with the first user and logging in with another.
For example, if I log in with the "admin" user, and I log out, and I log back in with "viewer", I still get the credentials for "admin", and I have to refresh the page to stop this behavior.
And the interesting part is that when logging in, I get the correct credentials in the AuthenticationController class generateToken method log.info("user has just logged in: " + authentication.getName()); line, this weird behavior only applies when calling the SecurityContextHolder.getContext().getAuthentication() method.
My AuthenticationController (please pay attention to my comments, it includes stuff I've tried already)
package com.issue.tracker.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@RestController
public class AuthenticationController {
private final TokenService tokenService;
private final AuthenticationManager authenticationManager;
@PostMapping("/authenticate")
public ResponseEntity<LoginResponse> generateToken(@RequestBody LoginRequest loginRequest) {
//SecurityContextHolder.clearContext();
//^Including this clearContext() method doesn't seem to do anything, anywhere at all.
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
loginRequest.username(),
loginRequest.password());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
if (authentication.isAuthenticated()) {
log.info("user has just logged in: " + authentication.getName());
//I get the correct credentials here every single time!
}
String token = tokenService.generateToken(authentication);
List<String> roles = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
//SecurityContextHolder.getContext().setAuthentication(authentication);
//^I thought this would do something, but nothing, same behavior...
return ResponseEntity.ok(new LoginResponse(token, roles));
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletResponse response) {
/*
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
SecurityContextHolder.clearContext();
*/
return ResponseEntity.noContent().build();
//Probably nothing wrong with my logout method either, including the above commented out code doesn't do anything either
}
}
My SecurityConfig:
package com.issue.tracker.authentication;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableAspectJAutoProxy
@EnableMethodSecurity
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class SecurityConfig {
private final UserRepository userRepository;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/authenticate").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(Customizer.withDefaults())
.headers(header -> header.frameOptions().sameOrigin())
.logout(logout -> logout
.logoutUrl("/api/logout")
.logoutSuccessHandler((request, response, authentication) -> {
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_OK);
})
)
.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(authenticationProvider);
}
@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
JWKSet jwkSet = new JWKSet(rsaKey());
return (((jwkSelector, securityContext)
-> jwkSelector.select(jwkSet)));
}
@Bean
JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
JwtDecoder jwtDecoder() throws JOSEException {
return NimbusJwtDecoder.withPublicKey(rsaKey().toRSAPublicKey()).build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
public RSAKey rsaKey() {
KeyPair keyPair = keyPair();
return new RSAKey
.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
}
@Bean
public KeyPair keyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (Exception e) {
throw new IllegalStateException("RSA Key Pair can not be generated!", e);
}
}
@Bean
public AuditorAware<String> auditorAware() {
return new CustomAuditAware();
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource
= new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
My AuthenticationServiceImpl:
package com.issue.tracker.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class AuthenticationServiceImpl implements AuthenticationService {
@Override
public Authentication getAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info("USERNAME: "+authentication.getName()); //Wrong credentials here, it retains the first logged in user's info every single time...
return authentication;
}
}
Everything I've tried so far is included in the AuthenticationController comments.
Nothing worked therefore I just solved it with forcing page refresh upon logging out on the frontend. (window.location.reload(true)
);