We currently have a WCF SOAP API that allows the consumer to authenticate using a username and password (internally uses a UserNamePasswordValidator) For reference the username and password is passed in the SOAP Body as follows:
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" mustUnderstand="1">
<Timestamp Id="_0">
<Created>
2013-04-05T16:35:07.341Z</Created>
<Expires>2013-04-05T16:40:07.341Z</Expires>
</Timestamp>
<o:UsernameToken Id="uuid-ac5ffd20-8137-4524-8ea9-3f4f55c0274c-12">
<o:Username>someusername</o:Username>
<o:Password o:Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">somepassword
</o:Password>
</o:UsernameToken>
</o:Security>
We we like to additionally support a consumer to specify credentials in the HTTP Authorization header, as either Basic auth, or an OAuth Bearer token
We already have several ways of actually doing the authentication for non-SOAP APIs, but I am not familiar with how to tell WCF to use any class I might create for this. How can I accomplish this? The only other question I have seen that attempts to answer this is here, but the accepted answer uses SOAP headers, not HTTP headers, and the asker essentially gave up.
Obviously any solution needs to be backwards compatible - we need to continue to support consumers specifying credentials in the SOAP Security Header.
You should look into implementing a ServiceAuthorizationManager
for your WCF service to handle the HTTP Authorization header authorization.
Create a class that inherits from System.ServiceModel.ServiceAuthorizationManager
, and override one or more of the CheckAccess
functions to examine the incoming web request and decide whether to allow it in or reject it. Rough sketch:
public class MyServiceAuthorizationManager: System.ServiceModel.ServiceAuthorizationManager
{
public override bool CheckAccess(OperationContext operationContext, ref Message message)
{
var reqProp = message.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
var authHeader = reqProp.Headers[HttpRequestHeader.Authorization];
var authorized = // decide if this message is authorized...
if (!authorized)
{
var webContext = new WebOperationContext(operationContext);
webContext.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;
webContext.OutgoingResponse.Headers.Add(HttpResponseHeader.WwwAuthenticate, String.Format("Bearer realm=\"{0}\"", baseUri.AbsoluteUri));
}
return authorized;
}
}
Wire this into your WCF service where you create the service host:
restAPIServiceHost = new DataServiceHost(typeof(API.RestAPIService), restUris);
var saz = restAPIServiceHost.Description.Behaviors.Find<ServiceAuthorizationBehavior>();
if (saz == null)
{
saz = new ServiceAuthorizationBehavior();
restAPIServiceHost.Description.Behaviors.Add(saz);
}
saz.ServiceAuthorizationManager = new MyServiceAuthorizationManager();
restAPIServiceHost.Open();
This will inject an authorization check into every method exposed by the WCF service, without requiring any changes to the service methods themselves.
Your MyServiceAuthorizationManager implementation can also be installed into your WCF service using web.config magic, but I find direct code easier to understand and debug.
Note that it will be difficult to have multiple authorization check systems in force on the same service without them stomping on each other or leaving a gap in your security coverage. If you have a UserNamePasswordValidator
in force to handle the SOAP user credentials case, it will reject a message that contains only an HTTP Authorization header. Similarly, a ServiceAuthorizationManager
that only checks for HTTP Authorization header will fail a web request containing SOAP user credentials. You will most likely need to figure out how to check for both kinds of auth credential representations in the same auth check. For example, you could add code to the CheckAccess function above to look for, extract, and test the SOAP user credentials if an HTTP Authorization header is not present in the message.
When you have to accept multiple auth representations you'll need to decide on precedence, too. If an HTTP Authorization header is present, I suspect it should take precedence over anything contained in the SOAP message. If the HTTP Authorization header is present but invalid, full stop - reject the request as unauthorized. It doesn't matter what's in the SOAP stuff - an invalid HTTP Authorization header is always bad news. If there is no HTTP Authorization header at all, then you can poke around to see if there is a SOAP security element from which you can get SOAP user credentials and test them for validity.
One of the ways you can go is using MessageInspectors.
Something like this:
First - create message inspector - to be responsible to add header with your credentials
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel;
using System.Xml;
namespace your_namespace
{
/// <summary>
/// /************************************
/// *
/// * Creating Message inspector for
/// * updating all outgoing messages with Caller identifier header
/// * read http://msdn.microsoft.com/en-us/magazine/cc163302.aspx
/// * for more details
/// *
/// *********************/
/// </summary>
public class CredentialsMessageInspector : IDispatchMessageInspector,
IClientMessageInspector
{
public object AfterReceiveRequest(ref Message request,
IClientChannel channel,
InstanceContext instanceContext)
{
return null;
}
public void BeforeSendReply(ref Message reply, object
correlationState)
{
#if DEBUG
//// Leave empty
//MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
//Message message = buffer.CreateMessage();
////Assign a copy to the ref received
//reply = buffer.CreateMessage();
//StringWriter stringWriter = new StringWriter();
//XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter);
//message.WriteMessage(xmlTextWriter);
//xmlTextWriter.Flush();
//xmlTextWriter.Close();
//String messageContent = stringWriter.ToString();
#endif
}
public void AfterReceiveReply(ref Message reply, object
correlationState)
{
#if DEBUG
//// Leave empty
//MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
//Message message = buffer.CreateMessage();
////Assign a copy to the ref received
//reply = buffer.CreateMessage();
//StringWriter stringWriter = new StringWriter();
//XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter);
//message.WriteMessage(xmlTextWriter);
//xmlTextWriter.Flush();
//xmlTextWriter.Close();
//String messageContent = stringWriter.ToString();
#endif
}
public object BeforeSendRequest(ref Message request,
IClientChannel channel)
{
request = CredentialsHelper.AddCredentialsHeader(ref request);
return null;
}
#region IDispatchMessageInspector Members
#endregion
}
}
Second - add the code to add header
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.ServiceModel.Channels;
using System.ServiceModel;
namespace your_namespace
{
public class CredentialsHelper
{
// siple string is for example - you can use your data structure here
private static readonly string CredentialsHeaderName = "MyCredentials";
private static readonly string CredentialsHeaderNamespace = "urn:Urn_probably_like_your_namespance";
/// <summary>
/// Update message with credentials
/// </summary>
public static Message AddCredentialsHeader(ref Message request)
{
string user = "John";
string password = "Doe";
string cred = string.Format("{0},{1}", user, password);
// Add header
MessageHeader<string> header = new MessageHeader<string>(cred);
MessageHeader untyped = header.GetUntypedHeader(CredentialsHeaderName, CredentialsHeaderNamespace);
request = request.CreateBufferedCopy(int.MaxValue).CreateMessage();
request.Headers.Add(untyped);
return request;
}
/// <summary>
/// Get details of current credentials from client-side added incoming headers
///
/// Return empty credentials when empty credentials specified
/// or when exception was occurred
/// </summary>
public static string GetCredentials()
{
string credentialDetails = string.Empty;
try
{
credentialDetails = OperationContext.Current.IncomingMessageHeaders.
GetHeader<string>
(CredentialsHeaderName, CredentialsHeaderNamespace);
}
catch
{
// TODO: ...
}
return credentialDetails;
}
}
}
Third - get your credentials on the server side
public void MyServerSideMethod()
{
string credentials = CredentialsHelper.GetCredentials();
. . .
}
Hope this helps.
For Basic authentication I got WCF working by setting the security mode to Transport.
For example in the web.config:
<system.serviceModel>
<services>
<service behaviorConfiguration="DefaultServiceBehavior" name="MyService">
<endpoint address="basic" binding="basicHttpBinding" bindingConfiguration="BasicAuthenticationBinding" name="MyEndpoint" contract="MyContract" />
</service>
</services>
<bindings>
<basicHttpBinding>
<binding name="BasicAuthenticationBinding">
<security mode="Transport">
<transport clientCredentialType="Basic" />
</security>
</binding>
</basicHttpBinding>
</bindings>
</system.serviceModel>