Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configuring security in a Spring Boot application

I'm upgrading an application to Spring Boot 2.0.3.

But my login request is unauthorized:

curl -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/users/login" -X POST -d "{ \"email\" : \"[email protected]\", \"password\" : \"xxxxx\" }" -i

The response is a 401 Unauthorized access. You failed to authenticate.

It is given by my custom entry point:

@Component
public final class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {

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

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        logger.debug("Security - RESTAuthenticationEntryPoint - Entry point 401");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access. You failed to authenticate.");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        setRealmName("User REST");
        super.afterPropertiesSet();
    }

}

The debugger shows the authenticate method of my CustomAuthenticationProvider is not called as I expect it to be:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    CredentialsService credentialsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String email = authentication.getName();
        String password = authentication.getCredentials().toString();
        List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<SimpleGrantedAuthority>();
        User user = null;
        try {
            user = credentialsService.findByEmail(new EmailAddress(email));
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException("The login " + email + " and password could not match.");             
        }
        if (user != null) {
            if (credentialsService.checkPassword(user, password)) {
                grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
                return new UsernamePasswordAuthenticationToken(email, password, grantedAuthorities);
            } else {
                throw new BadCredentialsException("The login " + user.getEmail() + " and password could not match.");               
            }
        }
        throw new BadCredentialsException("The login " + authentication.getPrincipal() + " and password could not match.");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

But the filter is exercised and a null token is found:

@Component
public class AuthenticationFromTokenFilter extends OncePerRequestFilter {

    @Autowired
    private TokenAuthenticationService tokenAuthenticationService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        tokenAuthenticationService.authenticateFromToken(request);
        chain.doFilter(request, response);
    }

}

@Service
public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {

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

    private static final long ONE_WEEK = 1000 * 60 * 60 * 24 * 7;
    private static final String TOKEN_URL_PARAM_NAME = "token";

    @Autowired
    private ApplicationProperties applicationProperties;

    @Autowired
    private UserDetailsService userDetailsService;

    public void addTokenToResponseHeader(HttpHeaders headers, String username) {
        String token = buildToken(username);
        headers.add(CommonConstants.AUTH_HEADER_NAME, token);
    }

    public void addTokenToResponseHeader(HttpServletResponse response, Authentication authentication) {
        String username = authentication.getName();
        if (username != null) {
            String token = buildToken(username);
            response.addHeader(CommonConstants.AUTH_HEADER_NAME, token);
        }
    }

    private String buildToken(String username) {
        String token = null;
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails != null) {
            Date expirationDate = new Date(System.currentTimeMillis() + ONE_WEEK);
            token = CommonConstants.AUTH_BEARER + " " + Jwts.builder().signWith(HS256, getEncodedPrivateKey()).setExpiration(expirationDate).setSubject(userDetails.getUsername()).compact();       
        }
        return token;
    }

    public Authentication authenticateFromToken(HttpServletRequest request) {
        String token = extractAuthTokenFromRequest(request);
        logger.debug("The request contained the JWT token: " + token);
        if (token != null && !token.isEmpty()) {
            try {
                String username = Jwts.parser().setSigningKey(getEncodedPrivateKey()).parseClaimsJws(token).getBody().getSubject();
                if (username != null) {
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    logger.debug("Security - The filter authenticated fine from the JWT token");
                }
            } catch (SignatureException e) {
                logger.info("The JWT token " + token + " could not be parsed.");
            }
        }
        return null;
    }

    private String extractAuthTokenFromRequest(HttpServletRequest request) {
        String token = null;
        String header = request.getHeader(CommonConstants.AUTH_HEADER_NAME);
        if (header != null && header.contains(CommonConstants.AUTH_BEARER)) {
            int start = (CommonConstants.AUTH_BEARER + " ").length();
            if (header.length() > start) {
                token = header.substring(start - 1);
            }
        } else {
            // The token may be set as an HTTP parameter in case the client could not set it as an HTTP header
            token = request.getParameter(TOKEN_URL_PARAM_NAME);
        }
        return token;
    }

    private String getEncodedPrivateKey() {
        String privateKey = applicationProperties.getAuthenticationTokenPrivateKey();
        return Base64.getEncoder().encodeToString(privateKey.getBytes());
    }

}

My security configuration is:

@Configuration
@EnableWebSecurity
@ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "com.thalasoft.user.rest.security", "com.thalasoft.user.rest.filter" })
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationFromTokenFilter authenticationFromTokenFilter;

    @Autowired
    private SimpleCORSFilter simpleCORSFilter;

    @Autowired
    private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.authenticationProvider(new CustomAuthenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
            .authenticationEntryPoint(restAuthenticationEntryPoint)
        .and()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .addFilterBefore(simpleCORSFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .headers().cacheControl().disable().frameOptions().disable()
        .and()
            .userDetailsService(userDetailsService)
            .authorizeRequests()
            .antMatchers(RESTConstants.SLASH + UserDomainConstants.USERS + RESTConstants.SLASH + UserDomainConstants.LOGIN).permitAll()
            .antMatchers(RESTConstants.SLASH + RESTConstants.ERROR).permitAll()
            .antMatchers("/**").hasRole(UserDomainConstants.ROLE_ADMIN).anyRequest().authenticated();
    }

}

The user details service is:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private CredentialsService credentialsService;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username != null && !username.isEmpty()) {
            User user = credentialsService.findByEmail(new EmailAddress(username));
            if (user != null) {
                return new UserDetailsWrapper(user);
            }
        }
        throw new UsernameNotFoundException("The user " + username + " was not found.");
    }

}

Why is the custom authentication provider not authenticating the username and password ?

UPDATE: I read something interesting and puzzling in this guide

Note that the AuthenticationManagerBuilder is @Autowired into a method in a @Bean - that is what makes it build the global (parent) AuthenticationManager. In contrast if we had done it this way (using an @Override of a method in the configurer) then the AuthenticationManagerBuilder is only used to build a "local" AuthenticationManager, which is a child of the global one. In a Spring Boot application you can @Autowired the global one into another bean, but you can’t do that with the local one unless you explicitly expose it yourself.

So, is there anything wrong with my usage of the configure method for setting up the authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider); ? Instead of the above configuration, I tried the following configuration:

@Autowired
public void initialize(AuthenticationManagerBuilder authenticationManagerBuilder) {
    authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);
}

But it still didn't exercise the custom authentication provider upon a request.

I also tried to have the filter after as in:

http.addFilterAfter(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class);

instead of addFilterBefore but it didn't change anything to the issue.

like image 882
Stephane Avatar asked Jul 25 '18 08:07

Stephane


1 Answers

In WebSecurityConfiguration inside configure(HttpSecurity http) method:

http.authorizeRequests().antMatchers("/api/users/login").permitAll(); 
http.authorizeRequests().anyRequest().authenticated();

Add in the same order.

Explanation: Login and logout requests should be permitted without any authentication

A sample configure method that works is:

http.formLogin().disable().logout().disable().httpBasic().disable();

        http.authorizeRequests().antMatchers("/logout", "/login", "/").permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        http.addFilterBefore(new SomeFilter(), SecurityContextHolderAwareRequestFilter.class);
        http.addFilterBefore(new CORSFilter(env), ChannelProcessingFilter.class);
         http.addFilterBefore(new XSSFilter(),CORSFilter.class);
like image 150
hellojava Avatar answered Oct 22 '22 18:10

hellojava