2

We are currently working with a team on two different RestAPIs. One is our main system and the other one is just an empty host with IdentityServer registered in it. We have successfully configured CORS and pre-flight OPTIONS handling in the main app, but now we are struggling with OPTIONS in IdentityServer (CORS is enabled and working). Unfortunately, we have some specific headers that are needed in token request and browser is sending pre-flight OPTIONS request. Every time we do that, we get:

The requested resource does not support HTTP method 'OPTIONS'.

In our RestAPI application pipeline we just had to create DelegatingHandler:

public class OptionsHttpMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (request.Method == HttpMethod.Options)
        {
            var apiExplorer = GlobalConfiguration.Configuration.Services.GetApiExplorer();

            var controllerRequested = request.GetRouteData().Values["controller"] as string;
            var supportedMethods = apiExplorer.ApiDescriptions
                .Where(d =>
                    {
                        var controller = d.ActionDescriptor.ControllerDescriptor.ControllerName;
                        return string.Equals(
                            controller, controllerRequested, StringComparison.OrdinalIgnoreCase);
                    })
                .Select(d => d.HttpMethod.Method)
                .Distinct();

            if (!supportedMethods.Any())
            {
                return Task.Factory.StartNew(
                    () => request.CreateResponse(HttpStatusCode.NotFound));
            }

            return Task.Factory.StartNew(() =>
            {
                var response = new HttpResponseMessage(HttpStatusCode.OK);

                return response;
            });
        }

        return base.SendAsync(request, cancellationToken);
    }
}

And register it in Global.asax:

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

I have no idea how to register it in IdentityServer pipeline. Here's the simplified implementation of Owin Startup class:

[assembly: OwinStartup(typeof(MyNamespace.IdentityServer.Host.Startup))]
namespace MyNamespace.IdentityServer.Host
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var identityServerServiceFactory = new IdentityServerServiceFactory()
                .UseInMemoryScopes(Scopes.Get())
                .UseInMemoryClients(Clients.Get());

            identityServerServiceFactory.UserService = new Registration<IUserService>(resolver => UserServiceFactory.Create());

            app.Map("/identity", idsrvApp =>
            {
                idsrvApp.UseIdentityServer(new IdentityServerOptions
                {
                    SiteName = "MyNamespace.IdentityServer",
                    SigningCertificate = this.LoadCertificate(),
                    Factory = identityServerServiceFactory
                });
            });
        }
    }
}

Tried so far:

  1. Setting some ISS handlers in <handlers> section in web.config

  2. I don't remember where I found this solution, but someone suggested to do the following:

    • register host API before IdentityServer
    • create a controller with routing set to address we want to hijack OPTIONS requests (in my case /identity/connect/token)
    • add [HttpOptions] tagged action, which returns 200, OK

    … but unfortunately, as I expected, my new controller hijacked all the traffic to the IdentityServer, not only OPTIONS requests.

Update - forgot to mention about third option

  1. I've come up with a solution that works (I mean almost because I haven't finished yet). It's simple, but I would rather avoid such an approach, because it adds additional work and, later on, code to maintain. The idea is to simply wrap IdentityServer's token endpoint in our own endpoint, so that AJAX calls only our RestAPI. And, as I stated before, our host is already set up for CORS and pre-flight OPTIONS
Sebastian Budka
  • 396
  • 1
  • 8
  • 19
  • Unrelated side note: use `Task.FromResult` (or mark method with async) instead of `Task.Factory.StartNew` in your `OptionsHttpMessageHandler`, `StartNew` is completely unnecesary here (and wastes time and resources, even if a little bit). – Evk Nov 01 '17 at 08:52
  • To be honest, it was just copy-pasteD from an external website, we just verified it works. But thanks for the advice, I will fix that. – Sebastian Budka Nov 01 '17 at 09:07

1 Answers1

0

I have managed to overcome this issue. I hope this is only temporary solution because it's neither elegant nor secure (I think it might open a way for some kind of attacks, but I am the beginner in security).

I am still waiting for better solution, and I'm willing to change accepted answer.

The solution was proposed by Chtiwi Malek in the other SO question, which I also provided in my question (my fault, I didn't check all of the answers).

All we have to do is to handle OPTIONS at the beginning of every request

protected void Application_BeginRequest()
{
    var res = HttpContext.Current.Response;
    var req = HttpContext.Current.Request;
    res.AppendHeader("Access-Control-Allow-Origin", "*");
    res.AppendHeader("Access-Control-Allow-Credentials", "true");
    res.AppendHeader("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version, X-File-Name");
    res.AppendHeader("Access-Control-Allow-Methods", "POST,GET,PUT,PATCH,DELETE,OPTIONS");

    if (req.HttpMethod == "OPTIONS")
    {
        res.StatusCode = 200;
        res.End();
    }
}

(I've copied AppendHeaders here from Chtiwi Malek's answer, but these is handled in my web.config)

We also have to enable OPTIONS requests for our application in web.config

<handlers>
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
Sebastian Budka
  • 396
  • 1
  • 8
  • 19