Summary

Custom CorsFilter is not being called if credential not matched on spring basic authentication.

Actual Behavior

I have integrated the basic authentication in my spring 4 project. It is rest full application. I am sending Authentication token in header. Problem cases: 1. Request goes my custom loadUserByUsername() function, If user/password succeed it move into CorsFilter's doFilter() function >> add required headers (allow origin etc.) and call requested api and return json result. 2.If user/password incorrect it doesn't move in CorsFilter, and also return html bad credential message (this should be json result).

Expected Behavior

I want to return followings if credential failed: 1. Json message . 2. Allow origin header (returned from CorsFilter class).

Configuration

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
    http://www.springframework.org/schema/security 
    http://www.springframework.org/schema/security/spring-security-4.0.xsd">

    <!-- Rest authentication entry point configuration -->  
    <http auto-config="true" use-expressions="true" create-session="stateless" 
        entry-point-ref="restServicesEntryPoint" 
        authentication-manager-ref="authenticationManagerForRest">      
        <intercept-url pattern="/login" access="permitAll" />
        <intercept-url pattern="/" access="permitAll" />        
        <intercept-url pattern="/security/**" access="isAuthenticated()" />

        <form-login login-page="/" username-parameter="username" login-processing-url="/login.action" 
        password-parameter="password" 
        authentication-success-handler-ref="customSuccessHandler" 
        authentication-failure-handler-ref="customFailureHandler" 
        authentication-failure-url="/accessDenied.action" /> 
        <security:access-denied-handler ref="customAuthenticationAccessDeniedHandler" />

        <!-- <security:custom-filter ref="basicAuthenticationFilter" after="BASIC_AUTH_FILTER" /> -->
        <http-basic />      
        <csrf disabled="true" /> 
    </http> 


    <!-- Authentication manager -->
    <authentication-manager alias="authenticationManagerForRest">
        <authentication-provider user-service-ref="customUserDetailsService" />
    </authentication-manager>

    <!-- Entry point for REST service. -->
    <beans:bean id="restServicesEntryPoint"
        class="gov.cgas.spring.security.RestUnauthorizedEntryPoint">        
    </beans:bean>       

    <!-- Custom User details service which is provide the user data -->
    <beans:bean id="customUserDetailsService"
        class="gov.cgas.spring.security.CustomUserDetailsService"></beans:bean>

    <!-- Connect the custom authentication success handler -->
    <beans:bean id="customSuccessHandler"
        class="gov.cgas.spring.security.RestAuthenticationSuccessHandler" />


    <!-- Using Authentication Access Denied handler -->
    <beans:bean id="customFailureHandler"
        class="gov.cgas.spring.security.RestAuthenticationFailureHandler" />

    <!-- Using Authentication Access Denied handler -->
    <beans:bean id="customAuthenticationAccessDeniedHandler"
        class="gov.cgas.spring.security.RestAccessDeniedHandler" />

    <beans:bean id="requestCacheFilter" 
        class="org.springframework.security.web.savedrequest.RequestCacheAwareFilter" />

    <!-- Enable the annotations for defining the secure role -->
    <global-method-security secured-annotations="enabled" />


</beans:beans>

Version

Sample

public class CorsFilter implements Filter {

    //private static final Logger log = Logger.getAnonymousLogger();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
      //  log.info("Adding Access Control Response Headers");
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, HEAD, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers,Authorization,Authorization1");
        filterChain.doFilter(servletRequest, servletResponse);

    }

    @Override
    public void destroy() {

    }
}


public class CustomUserDetailsService implements UserDetailsService {

    @Inject
    SecurityService scurityservice;

    @Inject
    UserService _userService;

    @Autowired
    PoUserRelationService _relationService;

    private CryptorEngine _encryption;
    private String credentialsCharset = "UTF-8";

    public CustomUserDetailsService() throws Exception {
        _encryption = new CryptorEngine();
    }

    public UserDetails loadUserByUsername(final String login) throws UsernameNotFoundException {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String header = request.getHeader("Authorization");
        String header1 = request.getHeader("Authorization1");

        LoginModel loginModel = new LoginModel();
        String postedPassword = "";

        if (header == null || !header.startsWith("Basic ")) {           
            throw new UsernameNotFoundException("Header Authorization was not found or not valid");
        }

        try {
            String[] tokens = extractAndDecodeHeader(header, request);          
            if(tokens.length !=2)
            {
                throw new UsernameNotFoundException("Header Authorization was not valid");
            }           
            postedPassword = tokens[1];

            String[] tokens1 = extractAndDecodeHeader(header1, request);            
            if(tokens1.length !=2)
            {
                throw new UsernameNotFoundException("Header Authorization1 was not valid");
            }       
            String fiscalYear = tokens1[0];
            String poCode = tokens1[1];                     
            loginModel.setFiscalYear(Integer.parseInt(fiscalYear));
            loginModel.setPayingOffice(Long.parseLong(poCode));         
        }
        catch (AuthenticationException | IOException failed) {
            SecurityContextHolder.clearContext();
            throw new UsernameNotFoundException(failed.getLocalizedMessage());          
        }

        MUsers r = _userService.selectByCode(login);

        if (r == null) {
            throw new UsernameNotFoundException("User " + login + " was not found in the database");
        }

        String f = r.getUserPassword();
        String de = _encryption.decrypt(f);
        de = de.replace(r.getSaltKey(), "");

        if(!de.equals(postedPassword))
        {
            throw new UsernameNotFoundException("User " + login + " was not found or password is incorrect");
        }

        /*TODO: add a loginUser view/sp/model and pull data in single hit and authencation. 
        * and add logic for if logged in as admin or super admin.
        */ 

        //If authencatation is successed, checking is user authorized for login as posted paying office.
        VCgasOfficeUserRelation _relationFilter = new VCgasOfficeUserRelation();
        _relationFilter.setPoCode(loginModel.getPayingOffice());
        _relationFilter.setUserCd(r.getUserCd());
        MultiResult<VCgasOfficeUserRelation> _relation =  _relationService.select(_relationFilter, null); 
        if(_relation.models==null || _relation.models.isEmpty())
        {
            throw new UsernameNotFoundException("User " + login + " not authorized to login as Paying Office "+loginModel.getPayingOffice());
        }


        Collection<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();


        loginModel.setUsername(login);
        loginModel.setPassword(r.getUserPassword());


        LoginDetailsModel grantedAuthority = scurityservice.getLoginDetailModel(r.getUserCd(),loginModel);
        grantedAuthorities.add(grantedAuthority);



        return new org.springframework.security.core.userdetails.User(login, de, grantedAuthorities);
    }

    /**
     * Decodes the header into a username and password.
     *
     * @throws BadCredentialsException if the Basic header is not present or is not valid
     * Base64
     */
    public String[] extractAndDecodeHeader(String header, HttpServletRequest request)
            throws IOException {

        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        }
        catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }

        String token = new String(decoded, getCredentialsCharset(request));

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }

        return token.split("\\:");

        //return new String[] { token.substring(0, delim), token.substring(delim + 1) };
    }

    protected String getCredentialsCharset(HttpServletRequest httpRequest) {
        return credentialsCharset;
    }

}

Comment From: magicalne

Hi @krgsoft

  1. Json message .

You need to define success handler or failure handler by yourself. You can redirect to another url, or return a JSON message. All you need is a handler.

2.Allow origin header (returned from CorsFilter class).

Of course you can define your cors filter. But spring security already supports this. All you need to do is configure what your header looks like or just use the default one.

I highly recommend you to read this https://spring.io/guides/tutorials/spring-security-and-angular-js/

It's long. But it will save your more time.

Thanks.

Comment From: rwinch

Why are you using a custom CorsFilter and not the built in support?

If you must use your custom implementation, "How are you registering your custom CorsFilter"? In order for it to work, you should have it placed just after Spring Security's HEADERS_FILTER.