CORS on WCF

⚠️ Important: Windows Communication Foundation (WCF) is in maintenance mode. Microsoft recommends:
  • ASP.NET Core Web API for REST APIs
  • gRPC for .NET for RPC-style communication
  • CoreWCF (community-supported) for migrating existing WCF services
This documentation is maintained for legacy WCF applications only.
⚠️ Security Warning: Using Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.

For WCF service you have to develop a custom behavior and include it in the endpoint configuration:

Implementation with Origin Validation

Step 1: Create Message Inspector with Origin Validation

public class CustomHeaderMessageInspector : IDispatchMessageInspector
{
    private readonly Dictionary<string, string> _requiredHeaders;
    private readonly string[] _allowedOrigins;

    public CustomHeaderMessageInspector(
        Dictionary<string, string> headers,
        string[] allowedOrigins)
    {
        _requiredHeaders = headers ?? new Dictionary<string, string>();
        _allowedOrigins = allowedOrigins ?? new string[0];
    }

    public object AfterReceiveRequest(
        ref System.ServiceModel.Channels.Message request,
        System.ServiceModel.IClientChannel channel,
        System.ServiceModel.InstanceContext instanceContext)
    {
        return null;
    }

    public void BeforeSendReply(
        ref System.ServiceModel.Channels.Message reply,
        object correlationState)
    {
        var httpHeader = reply.Properties["httpResponse"]
            as HttpResponseMessageProperty;

        if (httpHeader == null)
        {
            httpHeader = new HttpResponseMessageProperty();
            reply.Properties["httpResponse"] = httpHeader;
        }

        // Get the request to check origin
        var request = OperationContext.Current.RequestContext.RequestMessage;
        var requestProps = request.Properties["httpRequest"]
            as HttpRequestMessageProperty;

        var origin = requestProps?.Headers["Origin"];

        // Validate and set origin
        if (!string.IsNullOrEmpty(origin) && _allowedOrigins.Contains(origin))
        {
            httpHeader.Headers.Add("Access-Control-Allow-Origin", origin);
            httpHeader.Headers.Add("Vary", "Origin");
        }

        // Add other CORS headers
        foreach (var item in _requiredHeaders)
        {
            if (!httpHeader.Headers.AllKeys.Contains(item.Key))
            {
                httpHeader.Headers.Add(item.Key, item.Value);
            }
        }
    }
}
        

Step 2: Create Endpoint Behavior

public class EnableCrossOriginResourceSharingBehavior :
    BehaviorExtensionElement, IEndpointBehavior
{
    public void AddBindingParameters(
        ServiceEndpoint endpoint,
        System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
        // No implementation needed
    }

    public void ApplyClientBehavior(
        ServiceEndpoint endpoint,
        System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
    {
        // No implementation needed
    }

    public void ApplyDispatchBehavior(
        ServiceEndpoint endpoint,
        System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    {
        // Define allowed origins (NOT wildcard)
        var allowedOrigins = new[] {
            "https://example.com",
            "https://app.example.com"
        };

        var requiredHeaders = new Dictionary<string, string>();

        // CRITICAL FIX: Use Access-Control-Allow-Methods (not Request-Method)
        // Access-Control-Request-Method is a REQUEST header, not a RESPONSE header
        requiredHeaders.Add(
            "Access-Control-Allow-Methods",
            "POST,GET,PUT,DELETE,OPTIONS"
        );

        requiredHeaders.Add(
            "Access-Control-Allow-Headers",
            "X-Requested-With,Content-Type,Authorization"
        );

        // Optional: Allow credentials
        // requiredHeaders.Add(
        //     "Access-Control-Allow-Credentials",
        //     "true"
        // );

        // Preflight cache duration
        requiredHeaders.Add(
            "Access-Control-Max-Age",
            "86400"
        );

        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(
            new CustomHeaderMessageInspector(requiredHeaders, allowedOrigins)
        );
    }

    public void Validate(ServiceEndpoint endpoint)
    {
        // No validation needed
    }

    public override Type BehaviorType
    {
        get { return typeof(EnableCrossOriginResourceSharingBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new EnableCrossOriginResourceSharingBehavior();
    }
}
        

Step 3: Register Behavior in web.config

<system.serviceModel>
  <extensions>
    <behaviorExtensions>
      <add name="crossOriginResourceSharingBehavior"
           type="YourNamespace.EnableCrossOriginResourceSharingBehavior, YourAssembly, Version=1.0.0.0, Culture=neutral" />
    </behaviorExtensions>
  </extensions>

  <behaviors>
    <endpointBehaviors>
      <behavior name="corsEnabledBehavior">
        <webHttp />
        <crossOriginResourceSharingBehavior />
      </behavior>
    </endpointBehaviors>
  </behaviors>

  <services>
    <service name="YourNamespace.YourService">
      <endpoint address="api"
                binding="webHttpBinding"
                behaviorConfiguration="corsEnabledBehavior"
                contract="YourNamespace.IYourServiceContract" />
    </service>
  </services>
</system.serviceModel>
        

Step 4: Handle OPTIONS Preflight in Service Contract

[ServiceContract]
public interface IYourServiceContract
{
    [OperationContract]
    [WebInvoke(Method = "GET", UriTemplate = "data")]
    string GetData();

    [OperationContract]
    [WebInvoke(Method = "POST", UriTemplate = "data")]
    string PostData(DataContract data);

    // Handle preflight OPTIONS requests
    [OperationContract]
    [WebInvoke(Method = "OPTIONS", UriTemplate = "*")]
    void HandlePreflight();
}

public class YourService : IYourServiceContract
{
    public string GetData()
    {
        return "Data";
    }

    public string PostData(DataContract data)
    {
        return "Success";
    }

    public void HandlePreflight()
    {
        // CORS headers are added by the message inspector
        // Just return empty response with 204
        WebOperationContext.Current.OutgoingResponse.StatusCode =
            System.Net.HttpStatusCode.NoContent;
    }
}
        

Migration to Modern .NET

For new applications, consider migrating to ASP.NET Core Web API with much simpler CORS configuration:

// ASP.NET Core Web API - Much simpler!
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("https://example.com")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

var app = builder.Build();
app.UseCors();
        

Critical Bug Fixed: The original code used Access-Control-Request-Method which is a request header sent by the browser, not a response header. The correct response header is Access-Control-Allow-Methods. This bug would cause CORS to fail.

Note: For more information on migrating from WCF, see the official migration guide and the community-supported CoreWCF project.

Who’s behind this

Monsur Hossain and Michael Hausenblas

Contribute

The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.

Buy the book

Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain