Add additional user requirements in spring security login and handle various exceptions

227 Views Asked by At

I am new to Spring security, I have implemented a basic user login functionality for my app using JWT. Aside from checking for username and password at login I would like to add other parameters such as a "account is verified" boolean condition but I am not sure where to add this requirement. Additionally, I need to return a 403 forbidden response status message if the "account is verified" condition is false and return a different response status message if the username password combination isn't found at all. Here Is the code I currently have which correctly handles the login of an existing user (without checking for the "account is verified" condition) and always throws a 401 when the user is found. Any feedback would be helpful.

WebSecurityConfigurerAdapter

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final ApplicationUserDetailsService applicationUserDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public WebSecurityConfig(ApplicationUserDetailsService userDetailsService) {
        this.applicationUserDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
          .cors()
          .and()
          .csrf()
          .disable()
          .authorizeRequests()
          .antMatchers("/**")
          .permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .addFilter(new AuthenticationFilter(authenticationManager()))
          .addFilter(new AuthorizationFilter(authenticationManager()))
          .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    public PasswordEncoder encoder() {
        return this.bCryptPasswordEncoder;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(applicationUserDetailsService)
                .passwordEncoder(bCryptPasswordEncoder);
    }
}

UserDetailsService

public class ApplicationUserDetailsService implements UserDetailsService {

    private final ApplicationUserRepository applicationUserRepository;

    public ApplicationUserDetailsService(ApplicationUserRepository applicationUserRepository) {
        this.applicationUserRepository = applicationUserRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String nickname)
            throws UsernameNotFoundException, UserIsNotActiveException {
        Optional<ApplicationUser> applicationUser =
                applicationUserRepository.findByNickname(nickname);
        if (!applicationUser.isPresent()) {
            throw new UsernameNotFoundException(nickname);
        }

        return new User(
                applicationUser.get().getNickname(),
                applicationUser.get().getPassword(),
                emptyList());
    }
}

AuthenticationFilter

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;

    public AuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            ApplicationUser applicationUser =
                    new ObjectMapper().readValue(req.getInputStream(), ApplicationUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            applicationUser.getNickname(),
                            applicationUser.getPassword(),
                            new ArrayList<>()));

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res,
            FilterChain chain,
            Authentication auth) {
        Date exp = new Date(System.currentTimeMillis() + EXPIRATION_TIME);

        Key key = Keys.hmacShaKeyFor(KEY.getBytes());
        Claims claims = Jwts.claims().setSubject(((User) auth.getPrincipal()).getUsername());
        String token =
                Jwts.builder()
                        .setClaims(claims)
                        .signWith(key, SignatureAlgorithm.HS512)
                        .setExpiration(exp)
                        .compact();
        res.addHeader("token", token);
    }
}

AuthorizationFilter

public AuthorizationFilter(AuthenticationManager authManager) {
    super(authManager);
}

@Override
protected void doFilterInternal(
        HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    String header = request.getHeader(HEADER_NAME);

    if (header == null) {
        chain.doFilter(request, response);
        return;
    }

    UsernamePasswordAuthenticationToken authentication = authenticate(request);

    SecurityContextHolder.getContext().setAuthentication(authentication);
    chain.doFilter(request, response);
}

private UsernamePasswordAuthenticationToken authenticate(HttpServletRequest request) {
    String token = request.getHeader(HEADER_NAME);
    if (token != null) {
        Jws<Claims> user =
                Jwts.parserBuilder()
                        .setSigningKey(Keys.hmacShaKeyFor(KEY.getBytes()))
                        .build()
                        .parseClaimsJws(token);

        if (user != null) {
            return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
        } else {
            return null;
        }
    }
    return null;
}

ApplicationUser

public class ApplicationUser {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    @Column(unique = true)
    String email;

    @Column(unique = true)
    String nickname;

    String biography;

    String password; // Hashed

    @Builder.Default boolean isActive = false;
}
1

There are 1 best solutions below

0
On

The interface UserDetails (that is returned by the UserDetailsService) has some utility methods that can help you with it.

While the account is not activated, you can return false from the UserDetails#isEnabled method, or maybe you can use UserDetails#isAccountNonLocked as well.

Those methods will then be automatically validated on the AbstractUserDetailsAuthenticationProvider$Default(Pre/Post)AuthenticationChecks class.

After the user goes through the activation flow, you can change the property to true and it will allow the user to authenticate.

Tip: add the logging.level.org.springframework.security=TRACE to your application.properties to help to debug.