Home > Software design >  Spring @Validated is appears to be giving AuthenticationException
Spring @Validated is appears to be giving AuthenticationException

Time:01-08

I'm trying to make a blog w/ spring boot. I am using @Validated and groupings for certain requests. For Example hitting my signup route, I validate the email using @NotBlank and @Email on my User model

Controller:

package com.vweinert.fedditbackend.controllers;

import com.vweinert.fedditbackend.security.jwt.AuthEntryPointJwt;
import org.modelmapper.ModelMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.vweinert.fedditbackend.entities.User;
import com.vweinert.fedditbackend.request.auth.LoginRequest;
import com.vweinert.fedditbackend.request.auth.SignupRequest;
import com.vweinert.fedditbackend.dto.AuthDto;
import com.vweinert.fedditbackend.service.inter.UserService;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    private final UserService userService;
    private final ModelMapper modelMapper;
    private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
    public AuthController(UserService userService, ModelMapper modelMapper) {
        this.userService = userService;
        this.modelMapper = modelMapper;
        logger.debug("Auth controller initialized");
    }

    @PostMapping("/signin")
    public ResponseEntity<?> loginUser(@Validated(LoginRequest.class) @RequestBody User loginRequest) {
        try {
            logger.debug("logging in user {},",loginRequest);
            User user = userService.sigInUser(loginRequest);
            AuthDto authDto = modelMapper.map(user, AuthDto.class);
            return ResponseEntity.ok(authDto);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@Validated(SignupRequest.class) @RequestBody User signUpRequest) {
        try {
            logger.debug("signing up user {},", signUpRequest);
            User user = userService.registerUser(signUpRequest);
            AuthDto authDto = modelMapper.map(user, AuthDto.class);
            return ResponseEntity.ok(authDto);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }
}

Model:

package com.vweinert.fedditbackend.entities;

import java.time.LocalDateTime;
import java.util.Set;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.JoinTable;
import jakarta.persistence.Transient;
import jakarta.persistence.FetchType;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonInclude;

import org.hibernate.annotations.CreationTimestamp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import com.vweinert.fedditbackend.request.auth.LoginRequest;
import com.vweinert.fedditbackend.request.auth.SignupRequest;

@Entity
@Table(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable=false,unique = true)
    @NotBlank(groups = {SignupRequest.class}, message = "email is blank")
    @Email(groups = {SignupRequest.class}, message = "invalid email")
    private String email;
    @Column(nullable=false,updatable = false,unique = true)
    @NotBlank(groups = {LoginRequest.class, SignupRequest.class},message = "username is blank")
    @Size(min = 8, max = 32, groups = {LoginRequest.class, SignupRequest.class}, message = "username must be between 8 and 32 characters")
    private String username;
    @Column(nullable=false)
    @NotBlank(groups = {LoginRequest.class, SignupRequest.class},message = "Missing password")
    @Size(min = 8, max = 32, groups = {LoginRequest.class, SignupRequest.class}, message = "password must be between 8 and 32 characters")
    private String password;
    @Column(columnDefinition = "text")
    private String about;
    @Column(nullable=false,updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;
    private LocalDateTime passwordChangedAt;
    private LocalDateTime aboutChangedAt;
    @Column(nullable = false)
    @Builder.Default
    private Boolean deleted = false;
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable( name = "user_roles", 
                joinColumns = @JoinColumn(name = "user_id"), 
                inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles;
  
    @OneToMany(mappedBy = "user",fetch = FetchType.LAZY)
    private Set<Post> posts;

    @OneToMany(mappedBy = "user",fetch = FetchType.LAZY)
    private Set<Comment> comments;
    @Transient
    @JsonInclude()
    private String jwt;
    public User(String username, String email, String password){
        this.username = username;
        this.email = email;
        this.password = password;
    }
}

Signup request interface for the @Validated groups

package com.vweinert.fedditbackend.request.auth;

public interface SignupRequest {

}

Web Security Config:

package com.vweinert.fedditbackend.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.vweinert.fedditbackend.security.jwt.AuthEntryPointJwt;
import com.vweinert.fedditbackend.security.jwt.AuthTokenFilter;
import com.vweinert.fedditbackend.security.services.UserDetailsServiceImpl;
import com.vweinert.fedditbackend.security.jwt.JwtUtils;

@Configuration
@EnableGlobalMethodSecurity(
    prePostEnabled = true)
public class WebSecurityConfig {
  private final UserDetailsServiceImpl userDetailsService;
  private final AuthEntryPointJwt unauthorizedHandler;
  private final JwtUtils jwtUtils;
  public WebSecurityConfig(JwtUtils jwtUtils, UserDetailsServiceImpl userDetailsService, AuthEntryPointJwt unauthorizedHandler) {
    this.jwtUtils = jwtUtils;
    this.userDetailsService = userDetailsService;
    this.unauthorizedHandler = unauthorizedHandler;
  }
  @Bean
  public AuthTokenFilter authenticationJwtTokenFilter() {
    return new AuthTokenFilter(jwtUtils,userDetailsService);
  }
  
  @Bean
  public DaoAuthenticationProvider authenticationProvider() {
      DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
       
      authProvider.setUserDetailsService(userDetailsService);
      authProvider.setPasswordEncoder(passwordEncoder());
   
      return authProvider;
  }
  
  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    return authConfig.getAuthenticationManager();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeHttpRequests((requests) -> requests
                    .requestMatchers("/api/auth/**").permitAll()
                    .requestMatchers("/api/home/**").permitAll()
                    .anyRequest().authenticated()
            );
    http.authenticationProvider(authenticationProvider());
    http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
  }
}

Authentication entry Point:

package com.vweinert.fedditbackend.security.jwt;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {

  private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
      throws IOException, ServletException {
    logger.error("Unauthorized error: {}", authException.getMessage());

    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    final Map<String, Object> body = new HashMap<>();
    body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
    body.put("error", "Unauthorized");
    body.put("message", authException.getMessage());
    body.put("path", request.getServletPath());

    final ObjectMapper mapper = new ObjectMapper();
    mapper.writeValue(response.getOutputStream(), body);
  }

}

Currently I am getting an AuthenticationException going through my commence method that implements the AuthenticationEntryPoint class. I would like to have custom exceptions that simply return the message attached to my @Email annotation, though I'm really not sure how to do this.

When I do a postman call with this (with no authorization and an intentionally invalid email):

{
    "email":"emailssss",
    "username":"emailsssssssss",
    "password":"emailly123theemail"
}

I get:

{
    "path": "/error",
    "error": "Unauthorized",
    "message": "Full authentication is required to access this resource",
    "status": 401
}

and in the console I get:

2023-01-04T14:37:25.768-06:00  WARN 36022 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<?> com.vweinert.fedditbackend.controllers.AuthController.registerUser(com.vweinert.fedditbackend.entities.User): [Field error in object 'user' on field 'email': rejected value [emailssss]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljakarta.validation.constraints.Pattern$Flag;@49be6e2e,.*]; default message [invalid email]] ]
2023-01-04T14:37:25.774-06:00 ERROR 36022 --- [nio-8080-exec-1] c.v.f.security.jwt.AuthEntryPointJwt     : Unauthorized error: Full authentication is required to access this resource

I would like to get something like:

{
    "message": "invalid email"
}

CodePudding user response:

added

.requestMatchers("/error").permitAll()

To my filterChain in my WebSecurityConfig file. I did not know that when @Validated fails to validate it redirects to /error

  • Related