Spring SecurityContextHolder doesn't mantain authentication - java

I'm trying to manually authenticate to Spring Security through REST API. I've managed to open an endpoint where users can perfom the authentication. The endpoint works only whithin itself. If I try to get data from other secured endpoints the authentication is not provided. It seems that the user is authenticated but not remembered. Am I missing some steps?
Here's my endpoint where users can login:
#RestController
#RequestMapping("/api/account/login")
public class LoginRestController {
#Autowired
AuthenticationManager authenticationManager;
#PostMapping("")
public ResponseEntity<String> login(#RequestBody LoginPayload payload) {
try {
Authentication auth = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(payload.getEmail(), payload.getPassword()));
SecurityContextHolder.getContext().setAuthentication(auth);
return new ResponseEntity<>("Utente autenticato", HttpStatus.OK);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
}
}
here's my configuration:
#Configuration
public class SecurityConfig {
#Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/api/account/register", "/api/account/register/**").permitAll()
.requestMatchers("/api/account/login").permitAll()
.requestMatchers("/api/aziende/exists").permitAll()
.requestMatchers("/api", "/api/**").authenticated()
.requestMatchers("/app", "/app/**").authenticated()
.requestMatchers("/", "/**").permitAll()
)
.formLogin((form) -> form.loginPage("/accedi").permitAll())
.logout((logout) -> logout.logoutUrl("/esci").permitAll());
return http.build();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}

UPDATED:
As #MarcusHertdaCoregio said I had to save the context with the securitycontextrepository. Here's the code:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http = http.csrf().disable();
http = http.authorizeHttpRequests((requests) -> requests
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/aziende/exists").permitAll()
.requestMatchers("/api", "/api/**").authenticated()
.requestMatchers("/app", "/app/**").authenticated()
.requestMatchers("/admin", "/admin/**").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/", "/**").permitAll()
);
http = http.formLogin((form) -> form.loginPage("/accedi").permitAll());
http = http.logout((logout) -> logout.logoutUrl("/esci").logoutSuccessUrl("/").permitAll());
http = http.securityContext((securityContext) -> securityContext
.requireExplicitSave(true));
return http.build();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
#Bean
SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
}
#PostMapping("/login")
public ResponseEntity<?> login(#RequestBody Login payload, HttpServletRequest request,
HttpServletResponse response) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
try {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(payload.getEmail(),
payload.getPassword());
Authentication auth = authManager.authenticate(token);
if (auth.isAuthenticated()) {
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
securityContextRepository.saveContext(context, request, response);
return ResponseEntity.ok("utente autenticato");
} else {
return ResponseEntity.status(401).body("credenziali non valide");
}
} catch (Exception e) {
return ResponseEntity.status(401).body(e.getMessage());
}
}

Related

Cycle between AuthenticationConfiguration and Filters

I have a problem with cycle injection and don't know how to fix it :(
For my SecurityConfig I need JWTAuthenticationFilter.
For my JWTAuthenticationFilter I need AuthenticationManager.
For my AuthenticationManager I need SecurityConfig.
Can you help me with resolving this problem ? Thanks !
#Configuration
#RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
#Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
#Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder auth ) throws Exception {
return auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder()).and().build();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfig {
private final JWTVerifierFilter jwtVerifierFilter;
private final AuthenticationProvider authenticationProvider;
private final JWTAuthenticationFilter jwtAuthenticationFilter;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/auth/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerifierFilter, JWTAuthenticationFilter.class);
return http.build();
}
}
#RequiredArgsConstructor
#Component
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final TokenRepository tokenRepository;
private final JwtService jwtService;
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
JwtAuthenticationRequest authModel = OBJECT_MAPPER.readValue(request.getInputStream(), JwtAuthenticationRequest.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(authModel.getUsername(), authModel.getPassword());
return authenticationManager.authenticate(authentication);
} catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
String token = jwtService.generateToken((UserEntity) authResult.getPrincipal());
TokenEntity tokenEntity = TokenEntity.builder()
.id(UUID.randomUUID().toString())
.authenticationToken(token)
.username(authResult.getName())
.build();
tokenEntity = tokenRepository.save(tokenEntity);
response.addHeader(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", tokenEntity.getId()));
ConnValidationResponse respModel = ConnValidationResponse.builder().isAuthenticated(true).build();
response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(OBJECT_MAPPER.writeValueAsBytes(respModel));
}
}
I tried to replace
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
by
#Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
but for HttpSecurity I need SecurityConfig too.
I also tried add #Lazy for AuthenticationConfiguration config but it doesn't work

How can you make a confirmation dialog appear when logging out with Keycloak in spring boot?

I have a Spring boot application, and I use Keycloak for security. Logging in and out works, but I'd like to have the confirmation dialog from Keycloak like this when the logout button is clicked. How can I get that? Right now, it logs out directly.
SecurityConfig class:
#Configuration
#EnableWebSecurity
public class SecurityConfigKeycloak {
private final KeycloakLogoutHandler keycloakLogoutHandler;
public SecurityConfigKeycloak(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
#Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/someurl/**", "/anotherurl/**").permitAll()
.anyRequest()
.fullyAuthenticated();
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
return http.build();
}
}
Logout Handler:
#Slf4j
#Component
public class KeycloakLogoutHandler implements LogoutHandler {
private final RestTemplate restTemplate;
public KeycloakLogoutHandler(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
logoutFromKeycloak((OidcUser) auth.getPrincipal());
}
private void logoutFromKeycloak(OidcUser user) {
log.debug("Entering logout...");
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
if (logoutResponse.getStatusCode().is2xxSuccessful()) {
log.info("Successfulley logged out from Keycloak");
} else {
log.error("Could not propagate logout to Keycloak");
}
}
}
Controller class:
#Slf4j
#RestController
#RequestMapping("/logout")
public class LogoutController {
#GetMapping(value = "/*")
public ResponseEntity logout(HttpServletRequest request) {
try {
request.logout();
return new ResponseEntity<>(HttpStatus.OK);
} catch (ServletException e) {
log.error(e.getMessage(), e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

Spring boot security JWT- Bad credential

I am trying to implement JWT security. Somewhere, service is being called before controller with null value #RequestBody.
Below are the codes.
WebSecurityConfig.java
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
#Service
public class JWTUserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if("jailhouse".equals(username))
return new User("jailhouse","jailhouse",new ArrayList<>());
else {
throw new BadCredentialsException("User not found with Username: "+username);
}
}
}
#Controller
#PostMapping("/authenticate")
public JWTResponse authenticate(#RequestBody JWTRequest jwtRequest)throws Exception{
try{
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(jwtRequest.getUsername(),jwtRequest.getPassword())
);
}catch (BadCredentialsException e){
throw new BadCredentialsException("INVALID_CREDENTIAL",e);
}
final UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(jwtRequest.getUsername());
final String token = jwtUtil.generateToken(userDetails);
return new JWTResponse(token);
}
Everytime, I hit on authenticate endpoint somehow, service is being executed first with null value.
.security.authentication.BadCredentialsException: User not found with Username: NONE_PROVIDED
at com.fiserv.dc.service.JwtUserDetailsService.loadUserByUsername(JwtUserDetailsService.java:18) ~[main/:na]

How to show Spring Security Username Password Authentication Filter url in Swagger-ui?

I am using Spring Security and using Username Password Authentication Filter. I want to know if it is possible to show the authentication endpoint in Swagger. This endpoint is generated automatically by the filter (as far I understood).
I really want to make this endpoint appear in Swagger-ui, otherwise I will need to login in Postman and then use Swagger with the Jwt Token, which is little strange.
This is the UsernamePasswordAuthenticationFilter:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) {
try {
LoginDTO userEntity = new ObjectMapper().readValue(req.getInputStream(), LoginDTO.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userEntity.getEmail(),
userEntity.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException {
Token token = new Token(JwtUtils.createAccessToken(ZonedDateTime.now().plusMinutes(10), TenantContext.getCurrentUserUniqueIdentifier(), TenantContext.getCurrentTenantId()),
JwtUtils.createRefreshToken(ZonedDateTime.now().plusMonths(3)));
PrintWriter out = res.getWriter();
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
out.print(new ObjectMapper().writeValueAsString(token));
out.flush();
}
}
This is my class extending from WebSecurityAdapter. As you can see, I set the url as /v1/login.
#EnableWebSecurity
#AllArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserEntityDetailsService userEntityDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/v1/account").permitAll()
.antMatchers(HttpMethod.GET, "/v1/healthcheck").permitAll()
.antMatchers("/v1/recoverpassword/**").permitAll()
.antMatchers("/swagger-ui/**", "/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html", "/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(getAuthenticationFilter())
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userEntityDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
public AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/v1/login");
return filter;
}
}
This is my SpringFox config:
#Configuration
#EnableSwagger2
public class SpringFoxConfig {
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Projeto List")
.description("All endpoints of Projeto List Api")
.build();
}
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("br.com.projetolist.resource"))
.paths(PathSelectors.ant("/**"))
.build()
.securitySchemes(Arrays.asList(apiKey()));
}
/**
* SwaggerUI information
*/
#Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder.builder()
.deepLinking(true)
.displayOperationId(false)
.defaultModelsExpandDepth(1)
.defaultModelExpandDepth(1)
.defaultModelRendering(ModelRendering.MODEL)
.displayRequestDuration(false)
.docExpansion(DocExpansion.NONE)
.filter(false)
.maxDisplayedTags(null)
.operationsSorter(OperationsSorter.ALPHA)
.showExtensions(false)
.tagsSorter(TagsSorter.ALPHA)
.validatorUrl(null)
.build();
}
private ApiKey apiKey() {
return new ApiKey("jwtToken", "Authorization", "header");
}
}
You can just add a similar method to one of your controllers. In your case, it will be the POST method with URL "/v1/login".
#PostMapping("/v1/login")
#ResponseStatus(OK)
fun login(#RequestBody userCreds: UserCredentials) {}

JWT authentication filter not being triggered in Spring Security

I have created a JWT authentication filter for my Spring Rest backend. Creating a JWT doesn't appear to be an issue, however with my current setup, any request is authenticated, no request triggers a 401 despite the fact that the client is not passing any tokens in the header.
My WebSecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true,
jsr250Enabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private JwtAuthenticationEntryPoint unauthorizedHandler;
private CustomUserDetailsService customUserDetailsService;
#Autowired
public WebSecurityConfig(final JwtAuthenticationEntryPoint unauthorizedHandler,
final CustomUserDetailsService customUserDetailsService) {
this.unauthorizedHandler = unauthorizedHandler;
this.customUserDetailsService = customUserDetailsService;
}
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
#Bean
public JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
return new JwtAuthenticationSuccessHandler();
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean(BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* {#inheritDoc}
*/
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.antMatcher("/api")
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* Sets security evaluation context.
*
* #return {#link SecurityEvaluationContextExtension}
*/
#Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
I have set this up such that all requests require authorisation. My JwtAuthenticationEntryPoint is as expected: a generic 401 error being thrown.
My JwtAuthenticationFilter:
#Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain
filterChain) throws ServletException, IOException {
logger.debug("Filtering request for JWT header verification");
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken
(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
logger.debug("Attempting to get token from request header");
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
Found the problem.
I had to include a reference to the filter in my web.xml file, this isn't picked up automatically using component scanner.
something like:
<filter>
<filter-name>jwtFilter</filter-name>
<filter-class>com.path.to.JwtFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>jwtFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Categories