21

I am having some trouble integrating spring security (namely the login part) in my web application. My BackEnd is running on localhost:8080 and the FrontEnd (Ionic 2 with AngularJS 2) is running on localhost:8100. I managed to log in (at least I think so) but the rest of the requests become unavailable and I get the following error in the Chrome Developer Console:

XMLHttpRequest cannot load http://localhost:8080/company/getAll. Response for preflight is invalid (redirect)

When testing with Postman it seems to be working, I log in and then I am able to perform POST requests on http://localhost:8080/company/getAll and everything works as intended.

Presumably I'm missing something (like a token) but can't figure out what. I am new to both Ionic and Spring Security so please forgive me if this is something trivial. I tried googling various tutorials but was unable to find anything (most of them were using JSP).

How could I get this to work? How should my requests from the frontend look like?

Here is my login method from the BackEnd controller:

@RequestMapping(value = "/login", method = RequestMethod.GET)
    @CrossOrigin(origins = "*")
    public ResponseEntity<GeneralResponse> login(Model model, String error, String logout) {
        System.out.println("LOGIN"+model.toString());

        if (error != null) {
            System.out.println("1.IF");
            model.addAttribute("error", "Your username and password is invalid.");
        }

        if (logout != null) {
            System.out.println("2.IF");
            model.addAttribute("message", "You have been logged out successfully.");
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new GeneralResponse(true, "FAILURE: Error"));
    }

This is my WebSecurityConfig:

@Configuration
@EnableWebSecurity
@ComponentScan("com.SAB.service")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/resources/**", "/registration").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/user/login")
            .permitAll()
            .and()
            .logout()
            .permitAll();
        http
            .csrf().disable();
        http.formLogin().defaultSuccessUrl("http://localhost:8100/", true);
        //.and().exceptionHandling().accessDeniedPage("/403")
        //.and().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

And finally my FrontEnd code responsible for logging in:

  login(){
    var body = 'username='+this.loginForm.controls['username'].value+'&password='+this.loginForm.controls['password'].value;
    var headers = new Headers();
    headers.append('Content-Type', 'application/x-www-form-urlencoded');
    //headers.append("Access-Control-Allow-Origin", "*");

    this.http
      .post('http://localhost:8080/user/login',
        body, {
          headers: headers
        })
      .subscribe(data => {
        console.log('ok');
        console.log(data)
      }, error => {
        console.log(JSON.stringify(error.json()));
      });
  }

If you require any additional details please let me know and I will provide them.

Thanks in advance!

I don't get the Access-COntroll-Origin error but I tried to change it according to the comments anyway and now I get a "Invalid CORS request"

Request and Response Header for login from postman: enter image description here

EDIT: Here are the Spring Security logs. However Spring only registers a OPTIONS request and no POST even though the FrontEnd is calling POST.

************************************************************

Request received for OPTIONS '/login':

org.apache.catalina.connector.RequestFacade@18859793

servletPath:/login
pathInfo:null
headers: 
host: localhost:8080
connection: keep-alive
access-control-request-method: POST
origin: http://localhost:8100
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36
access-control-request-headers: access-control-allow-origin
accept: */*
referer: http://localhost:8100/
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8


Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CorsFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]


************************************************************
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2017-05-12 19:18:28.529 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2017-05-12 19:18:28.529 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 4 of 12 in additional filter chain; firing Filter: 'CorsFilter'
2017-05-12 19:18:28.541 DEBUG 16500 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@5baaaa2b
2017-05-12 19:18:28.541 DEBUG 16500 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2017-05-12 19:18:28.541 DEBUG 16500 --- [nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed

UPDATE: So after adding the CORSFilter suggested in the accepted answer I also had to modify my requests in the frontend in order for the cookie with the JSESSIONID to be sent on each request. Bassically I had to add the following to ALL my reqquests: let options = new RequestOptions({ headers: headers, withCredentials: true });

2

5 Answers 5

9
+50

This seems to be problem related to CORS configuration. Browser does make the OPTION preflight request before your actual GET call. I don't have much knowledge in this area, but you could try adding the filter below to the spring back end.

public class CORSFilter extends GenericFilterBean {

    public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse response = (HttpServletResponse) res;
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.addHeader("Access-Control-Max-Age", "3600");
        response.addHeader("Access-Control-Allow-Headers", "X-Requested-With, X-Auth-Token");
        response.addHeader("Access-Control-Allow-Credentials", "true");

        if(req.getMethod().equalsIgnoreCase("options")){
             return;
        }
        chain.doFilter(request, response);
    }
}

And add the line below in the configure method of your WebSecurityConfig.

.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class);

Sign up to request clarification or add additional context in comments.

2 Comments

Did you once try after removing if condition if(req.getMethod().equalsIgnoreCase("options")) from CORS filter, may be due to this "Unauthorized" is coming. Otherwise I cant see a reason why it is giving unauthorised after adding this filter. Basically what I am trying to do here is allow 'option' Http method using 'Access-Control-Allow-Methods' header because it does require for preflight request.
ok, so after massive changes and adding the corsfilter, it finally worked
2

The solution is very simple. The username and password on POST needs to be submitted as FormData and not part of body. I had the same issue with my app and after a long struggle I created a HTML form and did a POST then it worked like a charm.

See below live example. You should be able to signup with any id and try to login and debut it in your browser. It uses Spring boot for server side and Angular 4 on UI.

Live Demo: http://shop-venkatvp.rhcloud.com/#/

Login HTML: https://github.com/reflexdemon/shop/blob/master/src/app/login/login.component.html#L7-L31

Spring Security Configuration: https://github.com/reflexdemon/shop/blob/master/src/main/java/org/shop/WebSecurityConfig.java#L20-L34

Login controller: https://github.com/reflexdemon/shop/blob/master/src/main/java/org/shop/page/controller/LoginController.java

Hope this is helpful.

1 Comment

login works, but the problem persists: for all other requests it is not sending the JSESSIONID cookie as part of the requests
2

I'm in the middle of writing an Angular 4 and spring boot integration article. I don't have the article finished yet but the source code is done. I'm not sure why you are trying to serve your application from two different hosts, this unnecessarily complicates your application by injecting a cors requirement and having to manage two security context's. If this isn't an absolute requirement I would serve everything from the same server.

Take a look at this github project for a full integration of angular and spring boot: https://github.com/tschiman/tutorials/tree/master/spring-cloud/spring-cloud-bootstrap/gateway. You can ignore the zuul stuff in there if you don't need that. It has no impact on the support for the angular ui.

If you want to enable csrf, and you should, look at this commit: be4b206478c136d24ea1bf8b6f93da0f3d86a6c3

If reading that isn't making sense these are the steps you need to follow:

  1. Move your angular source code into your spring boot app
  2. Modify you angular-cli.json file to have an output directory in your resources static or resources public folder: "outDir":"../../resources/static/home" This sets the output directory for when you run ng build from the command line using the angular cli.
  3. Modify your login's default success url to redirect to /home/index.html .formLogin().defaultSuccessUrl("/home/index.html", true) this will force a successful login to always redirect to your angular app.
  4. If you want to automate building your angular app into maven then add this plugin to your POM file:

        <plugin>
            <artifactId>maven-antrun-plugin</artifactId>
            <executions>
                <execution>
                    <phase>generate-resources</phase>
                    <configuration>
                        <tasks>
                            <exec executable="cmd" osfamily="windows" dir="${project.basedir}/src/main/angular/ui">
                                <arg value="/c"/>
                                <arg value="ng"/>
                                <arg value="build"/>
                            </exec>
                            <exec executable="/bin/sh" osfamily="mac" dir="${project.basedir}/src/main/angular/ui">
                                <arg value="-c"/>
                                <arg value="ng build"/>
                            </exec>
                        </tasks>
                    </configuration>
                    <goals>
                        <goal>run</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    

If you have additional questions I am happy to respond further if this was not enough for you to get it working.

1 Comment

I think the problems start when you want to use live reload (like Webpack or whatever) for UI development. With such configuration, it is not possible and you need to restart java server which is slow down development.
1

Did you try to explicitly permit OPTIONS requests? Like this:

protected void configure(HttpSecurity http) throws Exception {
   ...
   http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll();
   ...
}

Comments

1

First of all - this is definitely a CORS configuration issue because you are running backend and frontend on different addresses.

You can twick backend to support CORS requests but this is a dirty solution, espesialy since you probably do not want CORS in production version of you app. Much cleaner approach would be to configure either frontend or backend to server as a proxy to the other.

I prefer to configure frontend to proxy requests to backend. This way it will have no effect on production version of the app.

Next part assumes you are using angular-cli (as you should with angular2 app).

They have a section in their docs specifically for proxy configuration: https://www.npmjs.com/package/angular-cli#proxy-to-backend.

I have all my REST services inside /api context, so my proxy.conf.json looks like this:

{
  "/api": {
    "target": "http://localhost:8083",
    "secure": false
  },
  "/login": {
    "target": "http://localhost:8083",
    "secure": false
  },
  "/logout": {
    "target": "http://localhost:8083",
    "secure": false
  }
}

Where http://localhost:8083 is my backend. You can then run your app with:

ng serve --proxy-config proxy.conf.json

and use only your frontend address. No CORS configuration needed.

P.S. If you dont use angular-cli Im sure you can use this approach with whatever tool you use to serve frontent. Before switching to angular-cli I used this with lite-server as well.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.