Fix CORS Preflight OPTIONS Failures in ASP.NET Web API 2 OWIN
Resolve CORS preflight (OPTIONS) failures in ASP.NET Web API 2 on IIS with OWIN OAuth/JWT auth. Fix duplicate pipelines, middleware order, IIS handlers, and OPTIONS blocks for Angular frontend.
How can I fix CORS preflight (OPTIONS) failures in an ASP.NET Web API 2 application hosted on IIS that uses OWIN authentication (OAuth + JWT)?
Environment
- ASP.NET Web API 2 hosted on IIS
- OWIN authentication (OAuth + JWT)
- Angular frontend making CORS requests (preflight OPTIONS fails)
Symptoms
- Browser’s preflight (OPTIONS) requests are blocked or return 405
- CORS headers are not applied to the OWIN pipeline
- Duplicate Web API pipelines observed after wiring up OWIN and GlobalConfiguration
Relevant excerpts from my app
Startup (ConfigureJwtOAuth):
private static void ConfigureJwtOAuth(IAppBuilder app)
{
app.UseOAuthAuthorizationServer(new AppOAuthOptions());
app.UseJwtBearerAuthentication(new AppJwtOptions());
HttpConfiguration config = new HttpConfiguration();
app.UseWebApi(config);
}
WebApiConfig.Register (called from Global.asax.Application_Start via GlobalConfiguration.Configure):
public static void Register(HttpConfiguration config)
{
// ...
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
GlobalConfiguration.Configuration.MessageHandlers.Insert(0, new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));
config.MessageHandlers.Add(new BlockOptionsHandler());
var corsAttr = new EnableCorsAttribute(ConfigurationManager.AppSettings["WhitelistUrls"], "*", "*");
config.EnableCors(corsAttr);
// ...
}
Global.asax.Application_BeginRequest (blocks OPTIONS):
protected void Application_BeginRequest(object sender, EventArgs e)
{
if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
{
HttpContext.Current.Response.StatusCode = 405;
HttpContext.Current.Response.End();
}
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
this.stopWatch.Start();
}
web.config CORS-related headers:
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="Content-Type" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
</customHeaders>
</httpProtocol>
Questions / What I need
- Why are preflight OPTIONS requests failing in this setup? Which of the above items are the root causes (duplicate HttpConfiguration, Application_BeginRequest returning 405, BlockOptionsHandler, middleware ordering, web.config headers)?
- What exact changes should I make to Startup.cs, WebApiConfig.Register, Global.asax.cs and web.config so that:
- a single Web API pipeline is used (no duplicates),
- CORS headers are applied to preflight responses,
- authentication middleware (OAuth/JWT) does not block OPTIONS requests,
- message handlers and compression remain functional.
Please provide a recommended, minimal fix with code examples. For example, show the correct way to create and reuse a single HttpConfiguration in OWIN startup, the right ordering for app.UseCors vs. authentication middleware, and whether to remove or change the Application_BeginRequest OPTIONS handling and/or BlockOptionsHandler. An example startup snippet illustrating the recommended order would be very helpful.
Fixing CORS preflight failures in this setup requires a single Web API pipeline (one HttpConfiguration), removing any server code that forcibly blocks OPTIONS (Application_BeginRequest and BlockOptionsHandler), and handling CORS at the OWIN layer before OAuth/JWT so preflight responses include Access-Control-* headers. Also update IIS to allow OPTIONS through (remove the OPTIONSVerbHandler) and avoid configuring CORS in two places (don’t use both Web API CORS and OWIN CORS at the same time).
Contents
- Why cors preflight fails in IIS + OWIN (root causes)
- Minimal safe fix (summary)
- File-by-file exact changes with code examples
- Startup.cs (single HttpConfiguration + owin cors ordering)
- WebApiConfig.Register (message handlers, remove BlockOptionsHandler)
- Global.asax (remove OPTIONS blocking)
- web.config (allow OPTIONS, remove duplicate headers)
- Recommended Startup snippet (asp net cors + owin cors ordering)
- Verification: test preflight request cors
- Sources
- Conclusion
Why cors preflight fails in IIS + OWIN (root causes)
Short answer: multiple layers are fighting each other. The browser’s preflight OPTIONS request never gets a proper CORS response because one or more of these are true:
-
Duplicate HttpConfiguration / double pipelines: you register Web API in Global.asax (GlobalConfiguration.Configure) and again create a new HttpConfiguration in Startup and call app.UseWebApi(config). That yields two pipelines and inconsistent message handlers / CORS setup. Browsers hit whichever pipeline/IIS handler answers first.
(See practical notes about duplicate pipelines and OWIN registration in community threads such as the StackOverflow Q on OWIN token auth and CORS: https://stackoverflow.com/questions/36285253/enable-cors-for-web-api-2-and-owin-token-authentication.) -
Application_BeginRequest returning 405: your code explicitly short-circuits OPTIONS with a 405; that’s a direct block — the preflight never reaches OWIN or Web API.
-
BlockOptionsHandler: adding a message handler that blocks OPTIONS will likewise prevent preflight replies with the required Access-Control-Allow-* headers.
-
Middleware ordering: if CORS middleware runs after authentication (OAuth/JWT), the preflight gets challenged or rejected instead of getting a bare CORS response. CORS must be handled before auth middleware.
-
IIS-level interception and handler configuration: IIS can “hijack” OPTIONS or reject extensionless OPTIONS requests unless you remove the OPTIONSVerbHandler and allow the extensionless handler to accept OPTIONS (Microsoft documents this and shows a web.config fix: https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api). StackOverflow threads also show IIS blocking OPTIONS when handlers are misconfigured: https://stackoverflow.com/questions/22495240/iis-hijacks-cors-preflight-options-request/42277801#42277801.
-
Multiple places adding CORS headers (web.config customHeaders + Web API CORS + OWIN CORS) can create duplicate or conflicting Access-Control-Allow-Origin headers. That can make the CORS algorithm fail in some clients/servers (avoid duplicate sources of CORS headers).
Why does this matter? The browser sends an OPTIONS preflight without credentials (often) and expects a quick response listing allowed methods/headers/origin. If any layer blocks OPTIONS or an earlier layer returns 405, the real request never happens.
Minimal safe fix (summary)
- Use a single HttpConfiguration instance and register Web API once under OWIN (do WebApiConfig.Register(config) from Startup, not from Global.asax). That prevents duplicate pipelines.
- Remove the OPTIONS block in Global.asax.Application_BeginRequest and remove BlockOptionsHandler (stop explicitly rejecting OPTIONS).
- Handle CORS in OWIN (app.UseCors) and register it before authentication middleware (OAuth/JWT). Don’t enable both OWIN CORS and Web API CORS at the same time — pick one (prefer OWIN when using OWIN auth). See a short discussion: https://benfoster.io/blog/aspnet-webapi-cors.
- Let IIS forward OPTIONS to your app: remove the OPTIONSVerbHandler and ensure the extensionless handler accepts verb=“*”. See Microsoft’s guidance: https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api.
- Keep your compression/message handlers in the same HttpConfiguration instance (they’ll still run for real requests); they usually don’t need to run for OPTIONS.
File-by-file exact changes with code examples
Below are concrete edits. The goal: one HttpConfiguration instance, OWIN CORS before auth, no OPTIONS blocking, and IIS configured to allow OPTIONS.
Startup.cs (single HttpConfiguration + owin cors ordering)
Replace your current ConfigureJwtOAuth that creates a standalone HttpConfiguration with a startup pattern that creates one HttpConfiguration and calls your WebApi registration method on it. Register CORS first, then authentication, then Web API.
Example (minimal, working):
using System.Configuration;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Cors;
using System.Web.Http;
using Microsoft.Owin.Cors;
using Owin;
public class Startup
{
public void Configuration(IAppBuilder app)
{
// 1) single HttpConfiguration for the app
var config = new HttpConfiguration();
// 2) register your Web API routes, formatters, message handlers, etc.
WebApiConfig.Register(config); // make sure this method accepts HttpConfiguration
// 3) configure CORS at the OWIN level (use whitelist from config if you have one)
var whitelist = ConfigurationManager.AppSettings["WhitelistUrls"]; // e.g. "https://app.example.com"
if (!string.IsNullOrEmpty(whitelist))
{
var policy = new CorsPolicy
{
AllowAnyHeader = true,
AllowAnyMethod = true,
SupportsCredentials = true
};
foreach (var origin in whitelist.Split(',').Select(s => s.Trim()))
{
if (!string.IsNullOrEmpty(origin)) policy.Origins.Add(origin);
}
var corsOptions = new CorsOptions
{
PolicyProvider = new Microsoft.Owin.Cors.CorsPolicyProvider
{
PolicyResolver = context => Task.FromResult(policy)
}
};
app.UseCors(corsOptions);
}
else
{
// quick temporary fix while debugging
app.UseCors(CorsOptions.AllowAll);
}
// 4) Authentication middleware AFTER CORS so preflight is not challenged
app.UseOAuthAuthorizationServer(new AppOAuthOptions());
app.UseJwtBearerAuthentication(new AppJwtOptions());
// 5) finally plug Web API into OWIN pipeline
app.UseWebApi(config);
}
}
Notes:
- The critical ordering: app.UseCors(…) → app.UseOAuthAuthorizationServer(…) → app.UseJwtBearerAuthentication(…) → app.UseWebApi(config).
- If you install OWIN CORS via NuGet, you get Microsoft.Owin.Cors and can use CorsOptions.AllowAll for a quick test.
- Avoid calling config.EnableCors() (Web API CORS) if you’re using app.UseCors to prevent duplicate header sources.
WebApiConfig.Register (message handlers, remove BlockOptionsHandler)
Modify WebApiConfig.Register to take and use the passed HttpConfiguration instance rather than referencing GlobalConfiguration.Configuration directly. Remove BlockOptionsHandler and do compression/message handlers on the local config.
Before (problematic snippets):
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
GlobalConfiguration.Configuration.MessageHandlers.Insert(0, new ServerCompressionHandler(...));
config.MessageHandlers.Add(new BlockOptionsHandler());
var corsAttr = new EnableCorsAttribute(ConfigurationManager.AppSettings["WhitelistUrls"], "*", "*");
config.EnableCors(corsAttr);
After (recommended):
public static void Register(HttpConfiguration config)
{
// formatters, routing
var json = config.Formatters.JsonFormatter;
// Compression message handler (keeps compression)
config.MessageHandlers.Insert(0, new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));
// Remove the BlockOptionsHandler: it blocks preflight
// config.MessageHandlers.Add(new BlockOptionsHandler()); // DELETE this
// Do NOT call config.EnableCors() if you handle CORS in OWIN.
// If you must use Web API CORS instead of OWIN, then remove the app.UseCors(...) call
// and enable Web API CORS here with a single method; but don't mix both.
// routes...
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional });
}
Key points:
- Put handlers and formatters on the same local config instance.
- Do not add a handler that prevents OPTIONS.
Global.asax (remove OPTIONS blocking)
Remove the explicit OPTIONS short-circuit that returns 405. That code directly causes the symptom you described.
Remove or edit this block:
protected void Application_BeginRequest(object sender, EventArgs e)
{
if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
{
HttpContext.Current.Response.StatusCode = 405;
HttpContext.Current.Response.End();
}
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
this.stopWatch.Start();
}
Either delete the OPTIONS branch entirely, or if you must still short-circuit for some reason, respond with 200 and proper headers (but prefer letting OWIN CORS handle it):
// DELETE the OPTIONS block entirely; let OWIN CORS return the preflight
protected void Application_BeginRequest(object sender, EventArgs e)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
this.stopWatch.Start();
}
Also ensure Application_Start no longer registers Web API and Startup registers Web API. If you moved WebApiConfig.Register into Startup, remove GlobalConfiguration.Configure(WebApiConfig.Register) from Application_Start to avoid double-registration.
web.config (allow OPTIONS, remove duplicate headers)
-
Remove the Access-Control-* customHeaders that IIS adds globally (they can duplicate OWIN headers). Comment them out or delete them.
-
Ensure IIS/handlers will forward OPTIONS to your pipeline by removing the OPTIONSVerbHandler and ensuring the extensionless handler accepts verb=“*”. Example snippet (system.webServer section):
<system.webServer>
<handlers>
<!-- allow OPTIONS to reach the app for extensionless URLs -->
<remove name="OPTIONSVerbHandler" />
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*" verb="*"
type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
<httpProtocol>
<customHeaders>
<!-- REMOVE these if you handle CORS in OWIN -->
<!-- <add name="Access-Control-Allow-Origin" value="*" /> -->
<!-- <add name="Access-Control-Allow-Headers" value="Content-Type" /> -->
<!-- <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" /> -->
</customHeaders>
</httpProtocol>
</system.webServer>
See Microsoft’s guidance on enabling CORS and IIS handler changes: https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api.
Recommended Startup snippet (asp net cors + owin cors ordering)
Put this final, compact snippet into Startup.Configuration for a minimal, clear working order:
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
WebApiConfig.Register(config); // uses this same `config`
// OWIN-level CORS first (temporary AllowAll or a whitelist)
app.UseCors(CorsOptions.AllowAll); // replace with whitelist policy for production
// Authentication middleware next
app.UseOAuthAuthorizationServer(new AppOAuthOptions());
app.UseJwtBearerAuthentication(new AppJwtOptions());
// Finally, Web API
app.UseWebApi(config);
}
If you must restrict origins in production, replace CorsOptions.AllowAll with a small CorsPolicy/CorsOptions-based provider (see earlier example). The key is: CORS must be handled before auth middleware so preflight isn’t challenged.
Verification: test preflight request cors
Quick tests you can run from the server or another host:
- cURL preflight simulation:
curl -i -X OPTIONS "https://api.yoursite.com/api/values" \
-H "Origin: https://app.yoursite.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
Expected key items in the response:
- HTTP/1.1 200 OK (or 204)
- Access-Control-Allow-Origin: https://app.yoursite.com
- Access-Control-Allow-Methods: POST, GET, OPTIONS, …
- Access-Control-Allow-Headers: Content-Type, Authorization
- (Optional) Access-Control-Max-Age, Access-Control-Allow-Credentials
- Browser test: open DevTools → Network, trigger the request in Angular, inspect the OPTIONS entry. If you still see 405 or no CORS headers, re-check for:
- Any remaining OPTIONS block in Global.asax
- BlockOptionsHandler or other message handlers
- web.config custom headers or handler config still blocking OPTIONS
- Use the curl test to verify whether IIS or the app returns the response.
If you see duplicate Access-Control-Allow-Origin headers, remove one source (prefer OWIN).
Sources
- Enabling Cross-Origin Requests in ASP.NET Web API (Microsoft)
- Enable CORS for Web Api 2 and OWIN token authentication (Stack Overflow)
- IIS hijacks CORS Preflight OPTIONS request (Stack Overflow)
- CORS is not working in Web API with OWIN authentication (Stack Overflow)
- How to make CORS Authentication in WebAPI 2? (Stack Overflow)
- CORS Not working for OPTIONS (Stack Overflow)
- Enabling CORS in ASP.NET Web API 2 (Ben Foster blog)
- Web API OWIN CORS handling - Ozkary blog
- Supporting HTTP method ‘OPTIONS’ and CORS for Web API – KenLin blog
Conclusion
To stop cors preflight failures: consolidate to a single HttpConfiguration under OWIN, remove any code that actively blocks OPTIONS (Application_BeginRequest and BlockOptionsHandler), handle CORS at the OWIN layer before your OAuth/JWT middleware, and make the small IIS handler change so OPTIONS can reach your pipeline. Do that and your preflight responses will reliably contain the required Access-Control-* headers and the Angular frontend will be able to proceed.