Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dreaded CORS issue with WebAPI and token

I swear this has happened so many times to me that I actually hate CORS. I have just split my application in two so that one handles just the API side of things and the other handles the client side stuff. I have done this before, so I knew that I needed to make sure CORS was enabled and allowed all, so I set this up in WebApiConfig.cs

public static void Register(HttpConfiguration config)
{

    // Enable CORS
    config.EnableCors(new EnableCorsAttribute("*", "*", "*"));

    // Web API configuration and services
    var formatters = config.Formatters;
    var jsonFormatter = formatters.JsonFormatter;
    var serializerSettings = jsonFormatter.SerializerSettings;

    // Remove XML formatting
    formatters.Remove(config.Formatters.XmlFormatter);
    jsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));

    // Configure our JSON output
    serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    serializerSettings.Formatting = Formatting.Indented;
    serializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    serializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None;

    // Configure the API route
    config.MapHttpAttributeRoutes();
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );
}

As you can see, my first line Enables the CORS, so it should work. If I open my client application and query the API, it does indeed work (without the EnableCors I get the expected CORS error. The problem is my /token is still getting a CORS error. Now I am aware that /token endpoint is not part of the WebAPI, so I created my own OAuthProvider (which I must point out is being used in other places just fine) and that looks like this:

public class OAuthProvider<TUser> : OAuthAuthorizationServerProvider
    where TUser : class, IUser
{
    private readonly string publicClientId;
    private readonly UserService<TUser> userService;

    public OAuthProvider(string publicClientId, UserService<TUser> userService)
    {
        if (publicClientId == null)
            throw new ArgumentNullException("publicClientId");

        if (userService == null)
            throw new ArgumentNullException("userService");

        this.publicClientId = publicClientId; 
        this.userService = userService;
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

        var user = await this.userService.FindByUserNameAsync(context.UserName, context.Password);

        if (user == null)
        {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return;
        }

        var oAuthIdentity = this.userService.CreateIdentity(user, context.Options.AuthenticationType);
        var cookiesIdentity = this.userService.CreateIdentity(user, CookieAuthenticationDefaults.AuthenticationType);
        var properties = CreateProperties(user.UserName);
        var ticket = new AuthenticationTicket(oAuthIdentity, properties);

        context.Validated(ticket);
        context.Request.Context.Authentication.SignIn(cookiesIdentity);
    }

    public override Task TokenEndpoint(OAuthTokenEndpointContext context)
    {
        foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
            context.AdditionalResponseParameters.Add(property.Key, property.Value);

        return Task.FromResult<object>(null);
    }

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        // Resource owner password credentials does not provide a client ID.
        if (context.ClientId == null)
        {
            context.Validated();
        }

        return Task.FromResult<object>(null);
    }

    public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
    {
        if (context.ClientId == this.publicClientId)
        {
            var redirectUri = new Uri(context.RedirectUri);
            var expectedRootUri = new Uri(context.Request.Uri, redirectUri.PathAndQuery);

            if (expectedRootUri.AbsoluteUri == redirectUri.AbsoluteUri)
                context.Validated();
        }

        return Task.FromResult<object>(null);
    }

    public static AuthenticationProperties CreateProperties(string userName)
    {
        IDictionary<string, string> data = new Dictionary<string, string>
        {
            { "userName", userName }
        };

        return new AuthenticationProperties(data);
    }
}

As you can see, In the GrantResourceOwnerCredentials method I enable CORS access to everything again. This should work for all requests to /token but it doesn't. When I try to login from my client application I get a CORS error. Chrome shows this:

XMLHttpRequest cannot load http://localhost:62605/token. 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:50098' is therefore not allowed access. The response had HTTP status code 400.

and Firefox shows this:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:62605/token. (Reason: CORS header 'Access-Control-Allow-Origin' missing). Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:62605/token. (Reason: CORS request failed).

For testing purposes, I decided to use fiddler to see if I could see anything else that might give me a clue as to what is happening. When I try to login, FIddler shows a response code as 400 and if I look at the raw response I can see the error:

{"error":"unsupported_grant_type"}

which is strange, because the data I am sending has not changed and was working fine before the split. I decided to use the Composer on fiddler and replicated what I expect the POST request to look like. When I Execute it, it works fine and I get a response code of 200.

Does anyone have any idea why this might be happening?

Update 1

Just for reference, the request from my client app looks like this:

OPTIONS http://localhost:62605/token HTTP/1.1
Host: localhost:62605
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: POST
Origin: http://localhost:50098
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36
Access-Control-Request-Headers: accept, authorization, content-type
Accept: */*
Referer: http://localhost:50098/account/signin
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8

from the composer, it looks like this:

POST http://localhost:62605/token HTTP/1.1
User-Agent: Fiddler
Content-Type: 'application/x-www-form-urlencoded'
Host: localhost:62605
Content-Length: 67

grant_type=password&userName=foo&password=bar
like image 417
r3plica Avatar asked Oct 21 '15 12:10

r3plica


People also ask

How do I fix the CORS issue in Web API?

First, we need to enable CORS in WebAPI, then we call the service from other application AJAX request. In order to enable CORS, we need to install the JSONP package from NuGet (see Figure3). After adding Jsonp package, we need to add the following code-snippet in App_Start\WebApiConfig. cs file.

How do I get rid of CORS error in API?

Cross-Origin Resource Sharing (CORS) errors occur when a server doesn't return the HTTP headers required by the CORS standard. To resolve a CORS error from an API Gateway REST API or HTTP API, you must reconfigure the API to meet the CORS standard.

What is the CORS issue in Web API?

Cross-origin resource sharing (CORS) is a browser security feature that restricts cross-origin HTTP requests that are initiated from scripts running in the browser. If your REST API's resources receive non-simple cross-origin HTTP requests, you need to enable CORS support.


3 Answers

Inside of

 public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)

Get rid of this:

 context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

Currently you are doing the CORS thing twice. Once with .EnableCors and also again by writing the header in your token endpoint.

For what it's worth, in my OWIN startup class I have this at the very top:

app.UseCors(CorsOptions.AllowAll);

I also do NOT have it in my WebAPI register method, as I'm letting the OWIN startup handle it.

like image 71
Bill Sambrone Avatar answered Oct 21 '22 07:10

Bill Sambrone


Since OAuthAuthorizationServer runs as an Owin middleware you must use the appropriate package Microsoft.Owin.Cors to enable CORS that works with any middleware in the pipeline. Keep in mind that WebApi & Mvc are just middleware themselves in regards to the owin pipeline.

So remove config.EnableCors(new EnableCorsAttribute("*", "*", "*")); from your WebApiConfig and add the following to your startup class. Note app.UseCors it must precede the app.UseOAuthAuthorizationServer

app.UseCors(CorsOptions.AllowAll)
like image 25
cleftheris Avatar answered Oct 21 '22 05:10

cleftheris


@r3plica

I had this problem, and it is like Bill said.

Put the line "app.UseCors" at the very top in Configuration method() (before ConfigureOAuth(app) is enough)

Example:

public void Configuration(IAppBuilder app)
    {
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

        HttpConfiguration config = new HttpConfiguration();

        ConfigureWebApi(config);
        ConfigureOAuth(app);

        app.UseWebApi(config);
    }
like image 3
W. Buzatto Avatar answered Oct 21 '22 06:10

W. Buzatto