Unable to set WWW-Authenticate

Topics: Web Api
Jan 26, 2012 at 7:53 PM
Edited Jan 26, 2012 at 7:55 PM

I'm trying to implement Basic authentication in a REST service.  This service is self hosted (not running in IIS).  I'm unable to set the "WWW-Authenticate" header.  Whenever I set the "WWW-Authenticate" header in the response I get this error:

System.ArgumentException occurred
  Message=The 'WWW-Authenticate' header cannot be modified directly.
Parameter name: name
  Source=System
  ParamName=name
  StackTrace:
       at System.Net.WebHeaderCollection.ThrowOnRestrictedHeader(String headerName)
       at System.Net.WebHeaderCollection.Add(String name, String value)
       at System.Net.HttpListenerResponse.AppendHeader(String name, String value)
       at System.ServiceModel.Channels.HttpOutput.ListenerResponseHttpOutput.PrepareHttpSend(Message message)
       at System.ServiceModel.Channels.HttpOutput.BeginSend(TimeSpan timeout, AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.HttpRequestContext.ReplyAsyncResult.SendResponse()
       at System.ServiceModel.Channels.HttpRequestContext.ReplyAsyncResult..ctor(HttpRequestContext context, Message message, TimeSpan timeout, AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.HttpRequestContext.OnBeginReply(Message message, TimeSpan timeout, AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.RequestContextBase.BeginReply(Message message, TimeSpan timeout, AsyncCallback callback, Object state)
       at Microsoft.ApplicationServer.Http.Channels.HttpMessageEncodingRequestContext.BeginReply(Message message, TimeSpan timeout, AsyncCallback callback, Object state)

Here is an exert of my code.

 

  public class AuthOperationHandler : HttpOperationHandler<HttpRequestMessage, HttpRequestMessage>
  {
    const string Scheme = "Basic";
    IUserValidator _UserValidation;
    public AuthOperationHandler(IUserValidator userValidation) : base("response") { _UserValidation = userValidation; }

    protected override HttpRequestMessage OnHandle(HttpRequestMessage input)
    {
      var credentials = ExtractCredentials(input);
      IPrincipal principal;
      if (credentials.Length < 2 || !_UserValidation.Validate(credentials[0], credentials[1], out principal))
      {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
        response.Headers.WwwAuthenticate.Add(new System.Net.Http.Headers.AuthenticationHeaderValue(Scheme, "realm=SomeRealm"));
        throw new HttpResponseException(response);
      }
     Thread.CurrentPrincipal = principle;
     return input;     
}

string[] ExtractCredentials(HttpRequestMessage request)
{
     // Extract Username & Password
   }
  }

This code is adapted from http://wcf.codeplex.com/discussions/259867.

Apparently this runs fine in IIS (I have not tested this) but I am self hosting.

Does anyone have any ideas?

Jan 26, 2012 at 9:52 PM

That is real odd... try adding a blank realm.

new AuthenticationHeaderValue("Basic", "realm=\"\"")

That seems to work for me in IIS, self hosted, and IIS express...

Jan 27, 2012 at 1:45 AM

I'm still unable to set this header even with an empty realm.

But I found another/better option. 

This is a working implementation of Basic Authentication.

See code below:

  public static class Program
  {
    static void Run(string[] args)
    {
	using(var service = StartService())
	  Console.ReadKey();
    }

    static HttpServiceHost StartService()
    {
      HttpConfiguration config = new HttpConfiguration
      {
        MessageHandlerFactory = () => new DelegatingHandler[] { new SetUserMessageHandler() },
        Security = (uri, s) =>
        {
          s.Mode = HttpBindingSecurityMode.TransportCredentialOnly;
          s.Transport.ClientCredentialType = System.ServiceModel.HttpClientCredentialType.Basic;
          s.Transport.Realm = "MyRealm";
        }
      };
      HttpServiceHost host = new HttpServiceHost(typeof(MyService), config, baseAddress);
      host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = System.ServiceModel.Security.UserNamePasswordValidationMode.Custom;
      host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new AuthenticationProvider();
      host.Open();
      return host;
    }
  }

  public class SetUserMessageHandler : DelegatingHandler
  {
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
      // Pull the current identity from the Thread.CurrentPrincipal, this was set in the AuthenticationProvider
      request.Properties.Add("User", Thread.CurrentPrincipal.Identity);
      return base.SendAsync(request, cancellationToken);
    }
  }

  public class AuthenticationProvider : UserNamePasswordValidator
  {
    public override void Validate(string userName, string password)
    {
      // Check username and password

      // If success then set the thread current principal
      Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(userName), new string[0]);
    }
  }