0

I'm creating an AngularJS (Typescript) SPA with a WebAPI2 backend, requiring authentication and authorization from the API. The API is hosted on a different server, so I'm using CORS, mainly following the guidance found at http://www.codeproject.com/Articles/742532/Using-Web-API-Individual-User-Account-plus-CORS-En as I'm a newcomer in this field.

All works fine, I can register and login, and then make requests to restricted-access controller actions (here the dummy "values" controller from the default VS WebAPI 2 template) by passing the received access token, in a client-side service with this relevant code:

private buildHeaders() {
    if (this.settings.token) {
        return { "Authorization": "Bearer " + this.settings.token };
    }
    return undefined;
}

public getValues(): ng.IPromise<string[]> {
    var deferred = this.$q.defer();
    this.$http({
        url: this.config.rootUrl + "api/values",
        method: "GET",
        headers: this.buildHeaders(),
    }).success((data: string[]) => {
        deferred.resolve(data);
    }).error((data: any, status: any) => {
        deferred.reject(status.toString() + " " +
            data.Message + ": " +
            data.ExceptionMessage);
    });
    return deferred.promise;
}

Now, I'd like to retrieve the user's roles once logged in so that the AngularJS app can behave accordingly. Thus I added this method in my account API (which at the class level has attributes [Authorize], [RoutePrefix("api/Account")], [EnableCors(origins: "*", headers: "*", methods: "*")] (* are for testing purposes):

[Route("UserRoles")]
public string[] GetUserRoles()
{
    return UserManager.GetRoles(User.Identity.GetUserId()).ToArray();
}

I then added this code to my login controller:

private loadUserRoles() {
    this.accountService.getUserRoles()
        .then((data: string[]) => {
            // store roles in an app-settings service
            this.settings.roles = data;
        }, (reason) => {
            this.settings.roles = [];
        });
}

public login() {
    if ((!this.$scope.name) || (!this.$scope.password)) return;

    this.accountService.loginUser(this.$scope.name,
            this.$scope.password)
        .then((data: ILoginResponseModel) => {
            this.settings.token = data.access_token;
            // LOAD ROLES HERE
            this.loadUserRoles();
        }, (reason) => {
            this.settings.token = null;
            this.settings.roles = [];
        });
}

where the account controller's method is:

public getUserRoles() : ng.IPromise<string[]> {
    var deferred = this.$q.defer();
    this.$http({
        url: this.config.rootUrl + "api/account/userroles",
        method: "GET",
        headers: this.buildHeaders()
    }).success((data: string[]) => {
        deferred.resolve(data);
    }).error((data: any, status: any) => {
        deferred.reject(status.toString() + ": " +
            data.error + ": " +
            data.error_description);
    });
    return deferred.promise;            
}

Anyway this triggers an OPTIONS preflight request, which in turn causes a 500 error. If I inspect the response, I can see that the GetOwinContext method gets a null request. Here is the beginning of the error stack trace:

{"message":"An error has occurred.","exceptionMessage":"Value cannot be null.\r\nParameter name: request","exceptionType":"System.ArgumentNullException","stackTrace":" at System.Net.Http.OwinHttpRequestMessageExtensions.GetOwinContext(HttpRequestMessage request)\r\n at Accounts.Web.Controllers.AccountController.get_UserManager() ...}

Yet, the code I'm using for GETting the roles is no different from that I use for GETting the dummy "values" from the WebAPI test controller. I can't exactly see the reason why a preflight should be required here, but in any case I'm getting this nasty exception in OWIN code.

My request header is (the API being at port 49592):

OPTIONS /api/account/userroles HTTP/1.1 Host: localhost:49592 Connection: keep-alive Access-Control-Request-Method: GET Origin: http://localhost:64036 User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 Access-Control-Request-Headers: accept, authorization Accept: */* Referer: http://localhost:64036/ Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8,it;q=-5.4

Could anyone explain?

Naftis
  • 4,393
  • 7
  • 63
  • 91
  • Are you sure User.Identity.GetUserId() has a value? – Jon Jun 26 '14 at 17:04
  • I cannot set a breakpoint there, the exception is thrown outside my code and before it, by the OWIN stuff which comes before in the pipeline. I too supposed this, but then I saw that the breakpoint is never hit, and the stack trace points to that OWIN method where parameter "request" is null. – Naftis Jun 26 '14 at 17:14
  • I must add that I tried the following: (a) as suggested in http://stackoverflow.com/questions/13624386/handling-cors-preflight-requests-to-asp-net-mvc-actions, I added Application_BeginRequest as specified in the post, but this triggers another exception: System.Web.HttpException: Server cannot set status after HTTP headers have been sent; (b) as suggested in the docs, I decorated the controller action with [AcceptVerbs(new[] { "GET"})] without OPTION to avoid MVC mess with the OPTIONS request, but this does not seem to have any effect. Any other idea? – Naftis Jun 28 '14 at 20:24
  • Have you ensure that in the web.config you have set the allowed verbs to include option? – Jon Jun 28 '14 at 20:39
  • Thanks, my web.config is essentially untouched from the webapi standard template. The only reference to OPTIONS I can find is under system.webServer/handlers: . I tried removing it, but nothing changed. – Naftis Jun 28 '14 at 21:06
  • Maybe this helps: I created a full repro solution here: http://1drv.ms/W0VvYx . At least it should be useful for starters as it's a complete fake skeleton with a DAL project, a web API host and a web client app. See the readme for repro steps and explanation. Any CORS guru out there would be welcome, I could not find any other web resource about null exception caused by a mere CORS preflight except for the one I already quoted, which was not a solution for me. Maybe it's something stupid, yet I'd not expect the CORS infrastructure to throw unexpectedly this way just because of a preflight. – Naftis Jul 10 '14 at 16:49

1 Answers1

0

I think I found some sort of working solution even if it looks somewhat dirty, but at least it works. I'm posting it here so other can eventually take advantage of it, but I'm open to suggestions. (Sorry for the bad formatting, but I tried several times and the editor does not allow me to correctly mark the code).

Essentially, the solution was suggested by the answer to this post: Handling CORS Preflight requests to ASP.NET MVC actions, but I changed the code which did not work for me (WebAPI 2 and .NET 4.5.1). Here is it:

  1. in Global.asax, method Application_Start, add BeginRequest += Application_BeginRequest;.

  2. add the override, which simply responds to OPTIONS requests by allowing everything (this is OK in my testing environment):

    protected void Application_BeginRequest(object sender, EventArgs e) { if ((Request.Headers.AllKeys.Contains("Origin")) && (Request.HttpMethod == "OPTIONS")) { Response.StatusCode = 200; Response.Headers.Add("Access-Control-Allow-Origin", "*"); Response.Headers.Add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");

      string sRequestedHeaders = String.Join(", ",
          Request.Headers.GetValues("Access-Control-Request-Headers") ?? new string[0]);
      if (!String.IsNullOrEmpty(sRequestedHeaders))
          Response.Headers.Add("Access-Control-Allow-Headers", sRequestedHeaders);
    
      Response.End();
    

    } }

  3. the attribute decorating the accounts controller method is just the RouteAttribute:

    [Route("UserRoles")] public string[] GetUserRoles() { string id = User.Identity.GetUserId(); Debug.Assert(id != null); string[] aRoles = UserManager.GetRoles(id).ToArray(); return aRoles; }

This way the OPTIONS request gets a proper response and the successive GET succeeds.

ADDITION

I must also add that the EnableCors attribute is not enough as we must not only handle the OPTIONS verb, but also ensure that any CORS request gets the Access-Control-Allow-Origin header. Otherwise, you might observe an apparently correct response (code 200 etc) but see the $http call failing. In my case I add to global.asax this line:

GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsAllowOriginHandler());

My CorsAllowOriginHandler is a DelegatingHandler which simply ensures that this header with value * is present in each response where the request included a Origin header:

public sealed class CorsAllowOriginHandler : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync
        (HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        // all CORS-related headers must contain the Access-Control-Allow-Origin header,
        // or the request will fail. The value may echo the Origin request header, 
        // or just be `*`.
        if ((request.Headers.Any(h => h.Key == "Origin")) &&
            (response.Headers.All(h => h.Key != "Access-Control-Allow-Origin")))
        {
            response.Headers.Add("Access-Control-Allow-Origin", "*");
        }
        return response;
    }
}
Community
  • 1
  • 1
Naftis
  • 4,393
  • 7
  • 63
  • 91