简体   繁体   中英

WCF: authenticate user before executing some service operation

My problem is the following.

A client interacts with my WCF service via a web interface (HTTP). Some service operations require the client to authenticate by providing username and password. Let's suppose for simplicity that these info are passed via query string parameters (or in the Authorization header as in HTTP Basic Auth).

Eg, a service operation may be called via http://myhost.com/myservice/myop?user=xxx&password=yyy

As multiple service operations require such sort of authentication, I would like to factor the authentication code out of the single operations.

By looking around, I read about service behaviors and came up with the following code:

public class MyAuthBehaviorAttribute : Attribute, IServiceBehavior, IDispatchMessageInspector {


    /********************/
    /* IServiceBehavior */
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
        System.ServiceModel.ServiceHostBase serviceHostBase) {
        //  It’s called right after the runtime was initialized
            foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers) {
                foreach (EndpointDispatcher epDisp in chDisp.Endpoints) {
                    epDisp.DispatchRuntime.MessageInspectors.Add(new MyAuthBehaviorAttribute());
                }
            }
    }

    /*...*/

    /*****************************/
    /* IDispatchMessageInspector */
    public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, 
        System.ServiceModel.IClientChannel channel, 
        System.ServiceModel.InstanceContext instanceContext) {
            object correlationState = null;
            var prop = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];


            var parts = HttpUtility.ParseQueryString(prop.QueryString);
            string user = parts["user"];
            string password = parts["password"];
            if (AuthenticateUser(user,password)) {
                // ???????????????????????????
            }
            else {
                throw new Exception("...");
            }

            return correlationState;
    }

    /*...*/

}

Then, the service is annotated through

[MyAuthBehavior]
public class Service : IContract
{
    // implementation of the IContract interface
}

Now, I manage to execute my behavior before ANY service operation. However, I have the following issues:

  • How can I pass the result of authentication to the service operations ?
  • How can I restrict the authentication to just a few service operations ?

Regarding the last point, I looked at IOperationBehavior, but in that case I can just attach IParameterInspectors and not IDispatchMessageInspectors. It would be undesirable because I may need to look at the message headers, for example in case I decide to consider the Authorization HTTP header when supporting HTTP Basic Authentication.

As a related question, I would also ask what you think about my approach, and if there are better (non-overcomplicated) approaches.

I would suggest isolating all of your methods that don't require authentication into their own service. For example:

IPublicService.cs and PublicService.svc

and the ones that require authentication:

IPrivateService.cs and PrivateService.svc

For authentication for PrivateService.svc, I'd suggest using MessageCredential using Username for that binding:

  <wsHttpBinding>
    <binding name="wsHttpEndpointBinding" closeTimeout="00:30:00" openTimeout="00:30:00" receiveTimeout="00:30:00" sendTimeout="00:30:00" maxReceivedMessageSize="500000000">
      <readerQuotas maxDepth="500000000" maxStringContentLength="500000000" maxArrayLength="500000000" maxBytesPerRead="500000000" maxNameTableCharCount="500000000" />
      <security mode="MessageCredential">
        <message clientCredentialType="UserName" />
      </security>
    </binding>
  </wsHttpBinding>

and add a custom username validator class:

public class CustomUserNameValidator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (username!="test" && password!="test")
            {
            throw new FaultException("Unknown username or incorrect password.");
            }
            return;
        }
    }

And register your class in web.config:

<behaviors>
  <serviceBehaviors>
    <behavior>
      <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
      <serviceMetadata httpsGetEnabled="true" />
      <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceCredentials>
        <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="MyProgram.CustomUserNameValidator,MyProgram" />
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>

After some research, here is my current solution.

Issue 1: execute (part of) the service behavior only for certain service operations

First of all, I mark my service operations with a custom attribute:

public class RequiresAuthAttribute : Attribute { }

public partial class MyService { 

    [RequiresAuth]
    WebGet(UriTemplate = "...")]
    public Tresult MyServiceOperation(...){ ... }

Then I retrieve this information to decide if the behavior has to be executed or not

    public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, 
        System.ServiceModel.IClientChannel channel, 
        System.ServiceModel.InstanceContext instanceContext) {
        if(AuthenticationNeeded()){ ... }
    }

    public bool AuthenticationNeeded() {
        // 1) Get the current operation's description
        OperationDescription od = GetOperationDescription(OperationContext.Current);

        // 2) Check if the service operation is annotated with the [RequiresAuth] attribute
        Type contractType = od.DeclaringContract.ContractType;
        object[] attr = contractType.GetMethod(od.Name).GetCustomAttributes(typeof(RequiresAuthAttribute), false);

        if (attr == null || attr.Length == 0) return false;
        return true;
    }

    // See http://www.aspnet4you.com/wcf/index.php/2013/01/30/message-interception-auditing-and-logging-at-wcf-pipeline/ 
    private OperationDescription GetOperationDescription(OperationContext operationContext) {
        OperationDescription od = null;
        string bindingName = operationContext.EndpointDispatcher.ChannelDispatcher.BindingName;
        string methodName;
        if (bindingName.Contains("WebHttpBinding")) {
            //REST request
            methodName = (string)operationContext.IncomingMessageProperties["HttpOperationName"];
        }
        else {
            //SOAP request
            string action = operationContext.IncomingMessageHeaders.Action;
            methodName = operationContext.EndpointDispatcher.DispatchRuntime.Operations.FirstOrDefault(o => o.Action == action).Name;
        }

        EndpointAddress epa = operationContext.EndpointDispatcher.EndpointAddress;
        ServiceDescription hostDesc = operationContext.Host.Description;
        ServiceEndpoint ep = hostDesc.Endpoints.Find(epa.Uri);

        if (ep != null) {
            od = ep.Contract.Operations.Find(methodName);
        }

        return od;
    }

Issue 2: pass information to the service operation

The service behavior will do something as

OperationContext.Current.IncomingMessageProperties.Add("myInfo", myInfo);

while the service operation will do something as

object myInfo = null;
OperationContext.Current.IncomingMessageProperties.TryGetValue("myInfo", out myInfo);

Alternatively, it is also possible to set a value for the service operation's parameters via

WebOperationContext.Current.IncomingRequest.UriTemplateMatch.BoundVariables["MYPARAM"] = myParam;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM