CSRF Prevention with Spring Security and AngularJS
Asked Answered
B

3

0

I'm using Spring 4.3.12.RELEASE Version, AngularJS 1.4.8. I'm trying to prevent the CSRF Attack on the application.

@Configuration
    @Order(2)
    public static class SecurityConfig extends WebSecurityConfigurerAdapter {

        String[] pathsToRemoveAuthorizaton = {
                "/mobile/**",
                "/logout",
                "/j_spring_security_logout",
                "/login",
        };

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

        @Override
        public void configure(HttpSecurity http) throws Exception {

            logger.info("http configure");
            http.antMatcher("/**").authorizeRequests().antMatchers(pathsToRemoveAuthorizaton).permitAll()
                    .antMatchers("/**").authenticated()
                    .and().formLogin().loginPage("/login")
                    .usernameParameter("employeeId").passwordParameter("password")
                    .successForwardUrl("/dashboard").defaultSuccessUrl("/dashboard", true)
                    .successHandler(customAuthenticationSuccessHandler()).loginProcessingUrl("/j_spring_security_check")
                    .and().logout().logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
                    .logoutSuccessHandler(customLogoutSuccessHandler()).permitAll().invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID").and().sessionManagement().sessionFixation().newSession()
                    .maximumSessions(1).maxSessionsPreventsLogin(true).and()
                    .sessionCreationPolicy(SessionCreationPolicy.NEVER).invalidSessionUrl("/logout").and()
                    .exceptionHandling().accessDeniedPage("/logout");

//          http.csrf().csrfTokenRepository(csrfTokenRepository()).and()
//          .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);

            http.csrf().csrfTokenRepository(csrfTokenRepository());

//          http.csrf().disable();
//          http.csrf().ignoringAntMatchers("/mobile/**");

            http.authorizeRequests().anyRequest().authenticated();
        }

        private CsrfTokenRepository csrfTokenRepository() {
            HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
            repository.setHeaderName("X-XSRF-TOKEN");
            return repository;
        }


        @Bean
        public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
            return new CustomAuthenticationSuccessHandler();
        }

        @Bean
        public LogoutSuccessHandler customLogoutSuccessHandler() {
            return new CustomLogoutSuccessHandler();
        }
    }

Below is my angular service code

govtPMS.service('Interceptor', function($q, $location, $rootScope, pinesNotifications, Auth) {

  return {

    request: function(config) {
        config.headers.Authorization = 'Bearer '+$rootScope.authToken;
//        document.cookie = 'CSRF-TOKEN=' + $rootScope.generateKey;

       return config;
    },
    requestError: function (rejection) {
        return $q.reject(rejection);
    },
    response: function(res) {

        if(res.status === 200 || res.status === 201){
            if(res.data.response !== undefined){
                if(res.data.status === 1 || res.data.status === 3 || res.data.status === 2) {
                    pinesNotifications.notify({
                        'title': 'Success',
                        'text': res.data.message,
                        'type': 'success',
                        'delay': 5000
                    });
                }
                 else if(res.data.status === 5) {
                    pinesNotifications.notify({
                        'title': 'Warning',
                        'text': res.data.message,
                        'type': 'warning',
                        'delay': 5000
                    });
                }
            }
        }
      return res || $q.when(res);
    },
    responseError: function(error) {
      return $q.reject(error);
    }
  };
}).config(['$httpProvider', function($httpProvider) {
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
  $httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';

  $httpProvider.interceptors.push('Interceptor');

}])

I'm still not able to see the CSRF Token header along with requests.

In this application, we are using 3 jsp pages - login.jsp, logout.jsp and dashboard.jsp angular scope is defined in the dashboard.jsp, hence login and logout are out of scope of AngularJS. I've also tried it the stateless way from this and this examples, where angular is generating a UUID and appending with cookie and request header, the below filter was doing the job fine. Until the logout attack. In this attack, the attacker is trying to succesfully logout the user since to logout from the application, we are simply using a href.

<li><a href="j_spring_security_logout" ><i class="fa fa-sign-out"></i><span>Logout</span></a></li>

Now since its logout is out of angular, the angularjs interceptor is cannot attach the UUID there. I've been struggling on this since past week, Any help will be appreciated.

StatelessCSRFFilter.java

package com.leadwinner.sms.config.filters;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.filter.OncePerRequestFilter;

public class StatelessCSRFFilter extends OncePerRequestFilter {



    private static final String CSRF_TOKEN = "CSRF-TOKEN";
    private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN";
    private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        List<String> excludedUrls = new ArrayList<>();
        excludedUrls.add("/resources");
        excludedUrls.add("/j_spring_security_check");
        excludedUrls.add("/j_spring_security_logout");
        excludedUrls.add("/login");
        excludedUrls.add("/logout");
        excludedUrls.add("/mobile");
        excludedUrls.add("/migrate");
        excludedUrls.add("/dashboard");

        String path = request.getServletPath();
        System.out.println(path);

        AtomicBoolean ignoreUrl = new AtomicBoolean(false);

        excludedUrls.forEach(url -> {
            if (request.getServletPath().startsWith(url.toLowerCase()) || request.getServletPath().equals("/")) {
                ignoreUrl.set(true);
            }
        });

        if (!ignoreUrl.get()) {
            final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
            final Cookie[] cookies = request.getCookies();
            System.out.println("**************************************************");
            System.out.println("--------------------------------------------------");
            String csrfCookieValue = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals(CSRF_TOKEN)) {
                        csrfCookieValue = cookie.getValue();
                    }
                }
            }
            System.out.println("csrfTokenValue = "+csrfTokenValue);
            System.out.println("csrfCookieValue = "+csrfCookieValue);

            System.out.println("--------------------------------------------------");
            System.out.println("**************************************************");
            if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
                accessDeniedHandler.handle(request, response, new AccessDeniedException(
                        "Missing or non-matching CSRF-token"));
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

No CSRF Token being added

Bibbye answered 18/11, 2019 at 9:53 Comment(2)
I'm a little confused because you seem to be using Spring Security's CSRF protection (wiring a CsrfTokenRepository) while also disabling the protection (csrf().disable()). Also, you are writing your own CSRF Filter. Will you please clarify if you are trying to write your own CSRF protection? And if so, why is the one provided by Spring Security insufficient?Campground
Sorry. It's been commented. I need to use the Spring way of CSRF Protection only, with CSRF disabled for some mobile APIs. Since I couldn't do that, I tried an alternative way of angular appending the CSRF Token in Header and Cookie through angular interceptor, which was working fine except for the logout page since for logout, href is being used. Any help in Enabling the CSRF Protection in Spring with which I can disable the CSRF Checking for the mobile API's would suffice.Bibbye
C
1

If the request can be made by the browser and credentials be handed up automatically (session cookie, basic auth creds), then CSRF protection is necessary, even with a mobile API.

Given that you have a mobile API as part of the application, the question is can those APIs be successfully addressed by the browser?

What I'd recommend is that you create two separate filter chains, like so, one for the web app and one for the mobile API:

@Configuration
@Order(100)
public class WebAppConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatchers()
                .antMatchers("/app/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }
}

@Configuration
@Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatchers()
                .antMatchers("/api/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

What this achieves is it separates the two configurations. The endpoints relative to the web application use one setup, and the endpoints relative to the mobile API use another setup.

Now, a couple of comments about the mobile API. I'm assuming that you are authenticating using OAuth 2.0 Bearer tokens, which is why the configuration above uses oauth2ResourceServer() from Spring Security 5.1+. What this does is selectively disables CSRF for requests that contain an Authorization: Bearer xyz header.

But, since you are using Spring Security 4.3, then you may need to do something more like the following (unless you can upgrade):

@Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatchers()
                .antMatchers("/api/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .sessionManagement().sessionCreationPolicy(NEVER)
                .and()
            .addFilterBefore(new MyMobileAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable();
    }
}

What you'd need to make sure of, though, is that your custom authentication filter doesn't use an authentication mechanism that is automatically sent by the browser from any origin (session cookies, Authorization: Basic).

Campground answered 19/11, 2019 at 18:27 Comment(2)
1. The Mobile API's, we're not including in this Configuration, There is a MobileSessionAspector which is intercepting all the mobile-related APIs where we are checking for a JWT Token Authorization. 2. I'm Already using MultiHttpSecurity Config. #58231564Bibbye
3. All I need to do here is to Enable the CSRF in Spring and AngularJS so that all the requests(APIs) and the Logout functionality are Protected from the CSRF Attack, and an option for me to specify the List of Patterns which can be excluded from CSRF Protection.Bibbye
O
0

`Hi Shiva,

Your code in the configure method of SecurityConfig should look like the below code:

 http
     .authorizeRequests()
     .antMatchers(patterns)
     .permitAll()
       .antMatchers("/hello/**")
       .hasRole("USER")
       .and()
       .csrf()
       .csrfTokenRepository(csrfTokenRepository())
       .requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
       .and()
       .httpBasic()
       .and()
       .addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
       .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);

And in the StatelessCSRFFilter, use the following code:

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

 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
     CsrfToken csrf = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
     String token = csrf.getToken();
     if (token != null && isAuthenticating(servletRequest)) {
         HttpServletResponse response = (HttpServletResponse) servletResponse;
         Cookie cookie = new Cookie("XSRF-TOKEN", token);
         cookie.setPath("/");
         response.addCookie(cookie);
     }
     filterChain.doFilter(servletRequest, servletResponse);
 }

 private boolean isAuthenticating(ServletRequest servletRequest) {
     HttpServletRequest request = (HttpServletRequest) servletRequest;
     return request.getRequestURI().equals("/login");
 }`
Ogdon answered 22/11, 2019 at 16:16 Comment(1)
Can you define csrfFilter(patterns) method also?Bibbye
O
0

<pre>
	Add this code for the csrfTokenRepository method
	     private CsrfTokenRepository csrfTokenRepository() {
	         HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
	         repository.setHeaderName("X-XSRF-TOKEN");
	         return repository;
	     }
	Add this for the csrfFilter method
	     private Filter csrfFilter(String[] patterns) {
	         CsrfFilter csrfFilter = new CsrfFilter(csrfTokenRepository());
	         csrfFilter.setRequireCsrfProtectionMatcher(csrfProtectionMatcher(patterns));
	         return csrfFilter;
	     }
	Add this for the csrfProtectionMatcher method
	     private NoAntPathRequestMatcher csrfProtectionMatcher(String[] patterns) {
	         return new NoAntPathRequestMatcher(patterns);
	     }
	Also remove these lines in configure method
	       .csrfTokenRepository(csrfTokenRepository())
	       .requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
	Move these lines below .csrf() in configure method:
	       .and()
	       .addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
</pre>
Ogdon answered 23/11, 2019 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.