简体   繁体   中英

Angular 2 Spring Security CSRF Token

Hi Everyone I'm having trouble setting up a security solution for my app!! So I have a REST API Backend which runs at http://localhost:51030 and developed with Spring Framework, and for the front side I have an Angular 2 application (the latest version AKA Angular 4) which runs at http://localhost:4200 . I have set the CORS configuration in the backend as seen below:

public class CORSFilter implements Filter
{
// The list of domains allowed to access the server
private final List<String> allowedOrigins = Arrays.asList("http://localhost:4200", "http://127.0.0.1:4200");

public void destroy()
{

}

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
{   
    // Lets make sure that we are working with HTTP (that is, against HttpServletRequest and HttpServletResponse objects)
    if (req instanceof HttpServletRequest && res instanceof HttpServletResponse)
    {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // Access-Control-Allow-Origin
        String origin = request.getHeader("Origin");
        response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "");
        response.setHeader("Vary", "Origin");

        // Access-Control-Max-Age
        response.setHeader("Access-Control-Max-Age", "3600");

        // Access-Control-Allow-Credentials
        response.setHeader("Access-Control-Allow-Credentials", "true");

        // Access-Control-Allow-Methods
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");

        // Access-Control-Allow-Headers
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, " + CSRF.REQUEST_HEADER_NAME); // + CSRF.REQUEST_HEADER_NAME
    }
    chain.doFilter(req, res);
}


public void init(FilterConfig filterConfig)
{

}
}

Using this configuration only works fine, I can execute requests from the angular app to the spring back and get response and do anything. But when I try to set up CSRF security solution nothing works. This is the CSRF and Security configuration setted up in the backend:

public class CSRF
{

     /**
     * The name of the cookie with the CSRF token sent by the server as a response.
     */
     public static final String RESPONSE_COOKIE_NAME = "XSRF-TOKEN"; //CSRF-TOKEN

     /**
      * The name of the header carrying the CSRF token, expected in CSRF-protected requests to the server.
      */
    public static final String REQUEST_HEADER_NAME = "X-XSRF-TOKEN"; //X-CSRF-TOKEN

    // In Angular the CookieXSRFStrategy looks for a cookie called XSRF-TOKEN 
    // and sets a header named X-XSRF-TOKEN with the value of that cookie.

    // The server must do its part by setting the initial XSRF-TOKEN cookie 
    // and confirming that each subsequent state-modifying request includes 
    // a matching XSRF-TOKEN cookie and X-XSRF-TOKEN header.

}

public class CSRFTokenResponseCookieBindingFilter extends OncePerRequestFilter
{

    protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException
    {
        CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);

        Cookie cookie = new Cookie(CSRF.RESPONSE_COOKIE_NAME, token.getToken());
        cookie.setPath("/");

        response.addCookie(cookie);

        filterChain.doFilter(request, response);
    }
}

@Configuration
public class Conf extends WebMvcConfigurerAdapter
{
    @Bean
    public CORSFilter corsFilter()
    {
        return new CORSFilter();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry)
    {
        registry.addViewController("/login");
        registry.addViewController("/logout");
    }
}

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{

    @Autowired
    private RESTAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RESTAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private RESTLogoutSuccessHandler logoutSuccessHandler;

    @Resource
    private CORSFilter corsFilter;

    @Autowired
    private DataSource dataSource;


    @Autowired
    public void globalConfig(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select login as principal, password as credentials, true from user where login = ?")
            .authoritiesByUsernameQuery("select login as principal, profile as role from user where login = ?")
            .rolePrefix("ROLE_");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        //csrf is disabled for the moment
        //http.csrf().disable();

        //authorized requests
        http.authorizeRequests()
            .antMatchers("/api/users/**").permitAll()
            .antMatchers(HttpMethod.OPTIONS , "/*/**").permitAll()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated();

        //handling authentication exceptions
        http.exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint);

        //login configuration
        http.formLogin()
            .loginProcessingUrl("/login")
            .successHandler(authenticationSuccessHandler);
        http.formLogin()
            .failureHandler(authenticationFailureHandler);

        //logout configuration
        http.logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(logoutSuccessHandler);

        //CORS configuration
        http.addFilterBefore(corsFilter, ChannelProcessingFilter.class);


        //CSRF configuration
        http.csrf().requireCsrfProtectionMatcher(
                new AndRequestMatcher(
                // Apply CSRF protection to all paths that do NOT match the ones below

                // We disable CSRF at login/logout, but only for OPTIONS methods to enable the browser preflight
                new NegatedRequestMatcher(new AntPathRequestMatcher("/login*/**", HttpMethod.OPTIONS.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/logout*/**", HttpMethod.OPTIONS.toString())),

                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.GET.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.HEAD.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.OPTIONS.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.TRACE.toString()))
            )
        );

        // CSRF tokens handling
        http.addFilterAfter(new CSRFTokenResponseCookieBindingFilter(), CsrfFilter.class);

    }
}

The problem is in the front side and the angular 4 configuration, the CSRF documentation is so poor and there is no full example of CSRF implementation in the Internet. So below is my login service:

@Injectable()
export class LoginService {

    private loginUrl = 'http://localhost:51030/login';

    constructor(private http: Http) {}

    preFlight() {
        return this.http.options(this.loginUrl);
    }

    login(username: string , password: string) {

        let headers = new Headers();

        headers.append('Content-Type', 'application/x-www-form-urlencoded');

        let options = new RequestOptions({headers: headers});

        let body = "username="+username+"&password="+password;

        return this.http.post(this.loginUrl , body , options);

    }
}

And in the login component I execute the option request in the ngOnInit life cycle hook:

@Component({
    templateUrl: './login-layout.component.html'
})
export class LoginLayoutComponent implements OnInit {

    credentials = {username: '' , password: ''};

    constructor(private loginService: LoginService){}

    ngOnInit() {
        this.loginService.preFlight()
                         .subscribe();
    }

    login() {
        this.loginService.login(this.credentials.username , this.credentials.password)
                         .subscribe(
                            response=>{
                                console.log(response) ; 
                            },error=>{
                                console.log(error);
                            }
                         );
    }

}

The preflight goes well and I get the 200 OK status on the options request plus a temporary JSEEIONID and the XSRF-TOKEN Cookie.

So in my app module I added this as said in the angular docs:

{
    provide: XSRFStrategy,
    useValue: new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN')
  },

BUT, when I try to execute a POST request with the credentials or any request to the back I got 403 Forbidden: "Could not verify the provided CSRF token because your session was not found."

So Please how can I solve this, can any one point me to right direction cause I have no clue on how to make this work!! And Thanks!!!

To solve the csrf problem between spring security and angular, you have to do that.

In SecurityConfiguration (WebSecurityConfig),replace http.csrf().disable(); by

               http.csrf()
                .ignoringAntMatchers ("/login","/logout")
                .csrfTokenRepository (this.getCsrfTokenRepository());


    }
    private CsrfTokenRepository getCsrfTokenRepository() {
        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        tokenRepository.setCookiePath("/");
        return tokenRepository;
{

the default angular csrf interceptor does not always work.So you have to implement your own interceptor.

import {Injectable, Inject} from '@angular/core';
import {HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler,
  HttpEvent} from '@angular/common/http';
import {Observable} from "rxjs";


@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {

  constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    let requestMethod: string = req.method;
    requestMethod = requestMethod.toLowerCase();

    if (requestMethod && (requestMethod === 'post' || requestMethod === 'delete' || requestMethod === 'put')) {
      const headerName = 'X-XSRF-TOKEN';
      let token = this.tokenExtractor.getToken() as string;
      if (token !== null && !req.headers.has(headerName)) {
        req = req.clone({headers: req.headers.set(headerName, token)});
      }
    }

    return next.handle(req);
  }
}

And finally add it in your providers (app.module.ts)

providers: [{ provide: HTTP_INTERCEPTORS, useClass: HttpXsrfInterceptor, multi: true }]

Think about putting in your imports.

   HttpClientXsrfModule.withOptions({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-CSRF-TOKEN'
    }),

I am surprised that you are doing so much work for CSRF and CORS as Spring Security and Angular have support built in. Spring Security has CSRF enabled by default.

The spring security manual has good documentation about configuring csrf: https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#csrf

And googling for "Angular 2 Spring Security csrf" gives several examples (and also how I found your post). Here is one:

https://medium.com/spektrakel-blog/angular2-and-spring-a-friend-in-security-need-is-a-friend-against-csrf-indeed-9f83eaa9ca2e

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM