2

I have an application with an Angular front end and a Asp.Net web service. I am using the HttpClient entity within Angular to make my web service calls.I have configured CORS and my GETs are working perfectly across the application using the following basic structure

let url = environment.base_url + 'api/SubDoc/GetDocument?docID=' + docID;

this.httpClient.get(url,
  {
    withCredentials: true
  })
  .subscribe(
    response => {
      console.log(response);
      newData = response;
      this.currentDocument = newData;
    }
  );

I have proven that CORS is working because I can hit the same service endpoint from my localhost session or from application sessions running on our QA and our Production servers as long as I edit the Origin values correctly in the my call to EnableCors

        config.EnableCors(new EnableCorsAttribute(origins: "http://localhost:2453,http://localhost:2452,http://***,http://10.100.101.46", headers: headers, methods: methods) { SupportsCredentials = true });

headers and methods are defined as the following strings:

        headers = "Origin, Content-Type, X-Auth-Token, content-type,application/x-www-form-urlencoded";
        methods = "GET,PUT,POST,OPTIONS,DELETE";

However, I an unable to make a PUT request to the same service using the following HttpClient structure

let url = this.base_url + 'api/ContactGroup/Put';
// Try the new HttpClient object
this.httpClient.put(url, body,
  {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
    withCredentials: true
  })
  .subscribe(
    response=>{
      let temp:number;
      temp = parseInt(response.toString(),10);
      if(!isNaN(temp)){
        this.groupID = temp;          
      }
    }
  );

This PUT request runs perfectly when I connect from the same server (i.e. no CORS applicable) but fails with a preflight error when I attempt to connect from any other server.

Failed to load http://***/api/ContactGroup/Put: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:2452' is therefore not allowed access.

As I explained above I do have Access-Control-Allow-Origin headers defined and I believe I have included PUT and OPTIONS as supported methods.

I think the issue is related to the PUT request not sending the credentials. The GET requests have a credential item in the request header but the PUT does not even though I am using the withCredentials option.

This is what the headers look like for the PUT request:

Request URL: http://reitgc01/REITServicesQA/api/ContactGroup/Put
Request Method: OPTIONS
Status Code: 401 Unauthorized
Remote Address: 10.100.101.46:80
Referrer Policy: no-referrer-when-downgrade
Content-Type: text/html
Provisional headers are shown
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: PUT
DNT: 1
Origin: http://localhost:2452
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36

I have now spent multiple days on this issue and any assistance would be greatly appreciated.

BruceK
  • 150
  • 1
  • 10
  • 2
    You need to configure the ASP.Net server backend for the `http://reitgc01/REITServicesQA/api/ContactGroup/Put` endpoint to not require authentication credentials for OPTIONS requests. The request headers shown indicate the browser is sending a CORS preflight OPTIONS request before trying the PUT request from your frontend code. And the preflight fails with a 401 response because the server is requiring authentication, but the browser does not include authentication credentials in that OPTIONS request — because the CORS spec requires the browser to make the request without credentials. – sideshowbarker Oct 26 '18 at 03:43
  • Thank for the feedback that is a very good start on the solution. I do have clarifying question. We are using IIS and we have Anonymouse Authentication disabled because this is an intranet site which uses integrated authentication. I have read some posts about custom configs in the Web.config to enable anonymous only on OPTIONS or to write a custom handler in the begin_request method. Do you have experience with either of these approaches and have you ever made this work for an integrated security site? – BruceK Oct 26 '18 at 11:19

2 Answers2

2

Thanks to sideshowbarker for pointing me in the right direction. I finally figured out the fundamental issue I was struggling with and implemented a solution.

The basic problem is that our site is an internal site which uses integrated security for the connections and this creates an incompatibility with CORS and CORS support in IIS. The front end is an Angular 6 interface with an ASP.NET web service. In order to use integrated security you must disable Anonymous Authentication. Additionally, in order to get authentication working you must pass {withCredentials:true} with your requests. This works fine for GET requests because the CORS spec does not require a pre-flight OPTIONS for those requests and the GET request header will have the required credentials token to be resolved properly.

The problem comes into play with the PUT requests. CORS requires a preflight OPTIONS request for PUTs but the OPTIONS request does not include credentials even if you set the withCredentials:true flag. This means you have to enable anonymous authentication which will break your integrated security. Its a bit of a catch 22. I was not able to find a way to enable anonymous login for OPTIONS only. I did find a couple of StackOverflow posts that claimed you could do it in IIS but I was not able to get that to work. I also found posts from MS that stated that IIS did not support this feature. This means that you have to write a custom handler for the OPTIONS request and generate the proper response to satisfy the browser. There are several posts on this topic but the best I found was Stu's response on this thread 401 response for CORS request in IIS with Windows Auth enabled

With a minor enhancement I was able to get this to work for me. I need support for several domains in my system config and I was still getting an error if I put multiple url's in the origin field. I used the Origin field of the Request.Headers to get the current origin and sent that value back in my constructed response header as shown in the code below:

EDIT: After playing with this for a while I realized the original solution had a flaw in it. My original code did not explicitly check on the origin to make sure it was allowed. As I said previously this was OK for GET operations because the GET would still fail (and the OPTIONS call is optional for GETs anyway) but for PUTs the client would pass the OPTIONS call and make the PUT request and get an error in the response. However, the server would still execute the PUT request since CORS is enforced client side. This would result in a data insertion or update when it should not occur. The solution to this is to explicitly check the detected origin against a CORS origin list maintained in the web.config and return a 403 status and a blank origin value if the requesting origin is not valid. The updated code is below

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        // Declare local variables to store the CORS settings loaded from the web.config
        String origins;
        String headers;
        String methods;

        // load the CORS settings from the webConfig file
        origins = ConfigurationManager.AppSettings["corsOrigin"];
        headers = ConfigurationManager.AppSettings["corsHeader"];
        methods = ConfigurationManager.AppSettings["corsMethod"];


        if (Request.HttpMethod == "OPTIONS")
        {
            // Get the Origin from the Request header. This should be present for any cross domain requests
            IEnumerable<string> headerValues = Request.Headers.GetValues("Origin");
            var id = headerValues.FirstOrDefault();

            String httpOrigin = null;
            // Assign the origin value to the httpOrigin string.
            foreach (var origin in headerValues)
            {
                httpOrigin = origin;
            }

            // Construct the Access Control headers using the derived origin value and the desired content.
            if (httpOrigin == null) httpOrigin = "*";

            // Check to see if the origin is included in the CORS origin list
            if (origins.IndexOf(httpOrigin) < 0)
            {
                // This has failed the CORS check so send a blank origin list and a 403 status (forbidden)
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "");
                HttpContext.Current.Response.StatusCode = 403;
            }
            else
            {
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", httpOrigin);
                HttpContext.Current.Response.StatusCode = 200;
            }

            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With,content-type, Content-Type, Accept, X-Token");
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Credentials", "true");


            var httpApplication = sender as HttpApplication;
            httpApplication.CompleteRequest();
        }

    }

This code was added to the Application_BeginRequest method in the global.asax.cs file. Additionally the signature of that method was modified to include the sender object as shown here: protected void Application_BeginRequest(object sender, EventArgs e) { This will now generate a response to the browser OPTIONS request which satisfies the CORS requirement and the subsequent GET will still get validated also so I don't believe there is any negative security impact.

The final piece is to construct your GETs and PUTs to both include the credentials information. Example requests are shown below: let url = environment.base_url + 'api/SubDoc/GetDocument?docID=' + docID;

this.httpClient.get(url,
  {
    withCredentials: true
  })
  .subscribe(
    response => {
      console.log(response);
      newData = response;
      this.currentDocument = newData;
    }
  );

and

body = { "GroupID": groupID, "ContactID": contact.ContactID, "IsActive": isActive }

let url = this.base_url + 'api/ContactGroup/Put';
this.httpClient.put(url, body,
  {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
    withCredentials: true
  })
  .subscribe(
    response=>{
      let temp:number;
      temp = parseInt(response.toString(),10);
      if(!isNaN(temp)){
        this.groupID = temp;          
      }
    }
  );

Hopefully, this will save someone else the days of pain and frustration I went through trying to get CORS working with Angular 6, ASP.NET and integrated security

BruceK
  • 150
  • 1
  • 10
  • Updated solution to properly check origin value before generating the HTTP response to the OPTIONS request. This fixed a security hole in the original solution. begin_request code has been updated above. – BruceK Nov 08 '18 at 13:59
0

Here's how I'm solving this exact example. I'm using Angular 5 with WebApi and Windows Authentication enabled. The CORS stuff is detailed here

The TL;DR; of the article is you need both Anonymous (because CORS headers don't carry authentication information) and windows authentication (so you can secure your application).

So in my webapi project I've got a CorsConfig class that sets up my CORS

public static class CorsConfig
{
    public static void Register(HttpConfiguration config)
    {
        var cors = new EnableCorsAttribute(ConfigurationManager.AppSettings["CorsClientUrl"], "*", "*")
        {
            SupportsCredentials = true
        };

        config.EnableCors(cors);
    }
}

register this with your HttpConfiguration

CorsConfig.Register(config);

In your web config under

<authorization>
  <allow verbs="OPTIONS" users="*" />
  <allow users="<your user list>" />
  <deny users="*" />
</authorization>

This will allow your OPTIONS check to pass through for all users. For all other verbs, I'm denying everyone and only allowing a specific list of users. You could change this to use roles.

On the Angular side I've created a interceptor that will put credentials on all http requests.

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


@Injectable()
export class WinAuthInterceptor implements HttpInterceptor {
  constructor() { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    req = req.clone({
      withCredentials: true
    });

    return next.handle(req);
  }
}

Register this in your app.module.ts in your provider list

  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: WinAuthInterceptor, multi: true }
  ]
Fran
  • 6,005
  • 1
  • 21
  • 34