0

I am trying to send token information back to the server over http.post(). If I remove the options from it it sends a POST but if I add them back on it sends OPTIONS which is rejected from the server code. I tried removing the "withCredentials" as well.

export class EntityService {

    public entity: EntityModel;
    private options: RequestOptions;

    constructor( @Inject(Http) private http: Http, @Inject(AuthenticationService) authService) {
        let headers = new Headers({ 'X-Authorization': 'Bearer ' + authService.token});
        this.options = new RequestOptions({ headers: headers, withCredentials: true });
    }

    public store(entity: EntityModel): Observable<string> {

        var request;
        if (!entity.uuid) {
            request = this.http.post("http://localhost:8080/api/entity", JSON.stringify(entity), this.options);
        }
        else {
            request = this.http.put("http://localhost:8080/api/entity", JSON.stringify(fact), this.options);
        }
        return request.map((res: Response) => res.text());
    }
}

My authentication service looks like this:

import { Injectable, Inject } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map'

//http://jasonwatmore.com/post/2016/08/16/angular-2-jwt-authentication-example-tutorial
@Injectable()
export class AuthenticationService {
    public token: string;

    constructor(@Inject(Http) private http: Http) {
        // set token if saved in local storage
        var currentUser = JSON.parse(localStorage.getItem('currentUser'));
        this.token = currentUser && currentUser.token;
    }

    login(username: string, password: string): Observable<boolean> {;
        console.log("login...");
        return this.http.post('http://localhost:8080/api/auth/login', JSON.stringify({ username: username, password: password }))
            .map((response: Response) => {
                // login successful if there's a jwt token in the response
                let token = response.json() && response.json().token;
                if (token) {
                    // set token property
                    this.token = token;

                    // store username and jwt token in local storage to keep user logged in between page refreshes
                    localStorage.setItem('currentUser', JSON.stringify({ username: username, token: token }));

                    // return true to indicate successful login
                    return true;
                } else {
                    // return false to indicate failed login
                    return false;
                }
            });
    }

    logout(): void {
        // clear token remove user from local storage to log user out
        this.token = null;
        localStorage.removeItem('currentUser');
    }
}

Here is my Spring configuration:

@SpringBootApplication
public class SpringBootApp extends WebMvcConfigurerAdapter {

    private boolean workOffline = true;
    private boolean setupSchema = false;
    private IGraphService graphService;
    private DbC conf;

    @Autowired
    public SpringBootApp(IGraphService graphService, DbC conf)
    {
        this.graphService = graphService;
        this.conf = conf;
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SpringBootApp.class, args);
    }

    @Bean
    public Filter caseInsensitiveRequestFilter() {
        return new CaseInsensitiveRequestFilter();
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "PUT", "POST", "DELETE","OPTIONS");
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:3000");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}

I really don't know what to do since I am following what is being said in Angular2 OPTIONS method sent when asking for http.GET and this is not a preflight request. I had this issue earlier with the wrong content-type.

Community
  • 1
  • 1
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
  • 1
    Commenting on downvotes is not required. Also downvote does not necessarily mean duplicate - some duplicates are actually useful, for example. – jonrsharpe Feb 05 '17 at 13:47

3 Answers3

2

The OPTIONS request is made by the browser alone. Angular is not involved at all.

"and this is not a preflight request." - it definitely is.

You need to configure your server to respond properly to the OPTIONS request or ensure that the Angular application is loaded from the same server (also same port) as where you make the request to.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Thanks Gunter! But I haven't found a way that I can send custom header with token information without making the browser send an OPTIONS request. What I am supposed to do when someone sends OPTIONS instead of POST.. I just return nothing.. under what condition will the browser send POST after OPTIONS? I have configured my server to respond to OPTIONS and I can't have those two on the same port unfortunately since one is node.js and one is jetty – Michail Michailidis Feb 05 '17 at 13:40
  • 2
    http://restlet.com/blog/2015/12/15/understanding-and-using-cors/ "POST methods, only content types with the following values are supported: text/plain, application/x-www-form-urlencoded and multipart/form-data." `withCredentials` also causes the `OPTIONS` preflight request. – Günter Zöchbauer Feb 05 '17 at 13:42
  • The server needs to return CORS headers `AccessControlAllowXxx` – Günter Zöchbauer Feb 05 '17 at 13:42
  • See https://spring.io/blog/2015/06/08/cors-support-in-spring-framework - search for "allowCredentials" - needs to be set to `true` in your case. – Günter Zöchbauer Feb 05 '17 at 13:53
  • I am doing that: `config.setAllowCredentials(true);` – Michail Michailidis Feb 05 '17 at 13:55
  • Sorry, don't know about Java, just did a Google search. In this linked doc they use `registry.allowCredentials()`. I can't tell if your approach should work as well. – Günter Zöchbauer Feb 05 '17 at 14:02
  • What's the exact error message you get in the browser? – Günter Zöchbauer Feb 05 '17 at 14:02
  • i don't get any error - it is just that it sends OPTIONS and it is rejected by my authentication filter `if (!HttpMethod.POST.name().equals(request.getMethod()))` – Michail Michailidis Feb 05 '17 at 14:27
  • Then you should probably allow `HttpMethod.OPTIONS` here as well. – Günter Zöchbauer Feb 05 '17 at 14:34
  • This is stopping OPTION methods: https://github.com/svlada/springboot-security-jwt/blob/7091258de487f1a5f7e82db7d67e6bcbb0a4c16a/src/main/java/com/svlada/security/auth/ajax/AjaxLoginProcessingFilter.java I don't know what to do so that client sends a POST after initial OPTIONS – Michail Michailidis Feb 05 '17 at 15:02
  • 3
    The browser sends `POST` after `OPTIONS` if it gets the right headers in the response to the `OPTIONS` request. If it doesn't, you get an error in the browser console instead telling what headers are missing. – Günter Zöchbauer Feb 05 '17 at 15:16
2

The actual fix was due to two reasons: improper CORS implementation - for more please have a look here: Spring 4/5 global CORS configuration doesn't work giving `No 'Access-Control-Allow-Origin' header is present on the requested resource`

Then when I was POSTing after the login I was getting error 415 Unsupported Media Type. After following the instructions here: POST JSON fails with 415 Unsupported media type, Spring 3 mvc

I added Content-Type and Accept header on my request and it fixed the problem. It seems that Content-Type is what was actually needed.

export class EntityService {

    public entity: EntityModel;
    private options: RequestOptions;

    constructor( @Inject(Http) private http: Http, @Inject(AuthenticationService) authService) {
        let headers = new Headers({ 
           'X-Authorization': 'Bearer ' + authService.token,
           'Content-Type': 'application/json'
        });
        this.options = new RequestOptions({ headers: headers, withCredentials: true });
    }

    public store(entity: EntityModel): Observable<string> {

        var request;
        if (!entity.uuid) {
            request = this.http.post("http://localhost:8080/api/entity", JSON.stringify(entity), this.options);
        }
        else {
            request = this.http.put("http://localhost:8080/api/entity", JSON.stringify(fact), this.options);
        }
        return request.map((res: Response) => res.text());
    }
}
Community
  • 1
  • 1
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
0

Using http post like this

import { Http, Headers, Response, Request } from '@angular/http';

let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('X-Authorization', this.token);
headers.append('Authorization', 'Bearer ' + jwtToken);

return this.http.post(url, data, {headers})
  .map(res => { console.log(res) })
  .catch(err => { console.log(err) } );

Note this example returns an Observable that you can subscribe to. Also my example

Evans M.
  • 1,791
  • 16
  • 20