1
votes

I have created a Service with below Service contract

[ServiceContract]
public interface IService1
{
     [OperationContract]
     string GetData(int value);
}

With below Custom Authentication (not in App_Code)

public class CustomAuthenticator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        try
        {
            if (userName.Equals("user") && password.Equals("pass"))
            {
                //Good to go
            }
        }
        catch (Exception ex)
        {
            throw new FaultException("Authentication failed");
        }
    } 
}

My Config on Service. I used basicHttpBinding with Custom User Authentication (I don't want to use wsHttpBinding which makes it mandatory to have certificate)

<system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="myBasicBinding">
          <security mode="TransportWithMessageCredential">
            <message clientCredentialType="UserName"/>
            <transport clientCredentialType="None" proxyCredentialType="None"/>
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
    <services>
      <service name="TestService.Service1" behaviorConfiguration="customBehavior">
        <endpoint address="" binding="basicHttpBinding" bindingConfiguration="myBasicBinding" contract="TestService.IService1" />
        <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="customBehavior">
          <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
          <serviceMetadata httpGetEnabled="true" 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="false"/>
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="TestService.CustomAuthenticator,TestService"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <protocolMapping>
      <add binding="basicHttpsBinding" scheme="https" />
    </protocolMapping>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
<system.webServer>

Service is hosted on IIS 8.5 with Anonymous and Forms Authentications enabled. I took a Console application client, added service reference and below config

<system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="BasicHttpBinding_IService1">
          <security mode="TransportWithMessageCredential">
            <message clientCredentialType="UserName"/>
            <transport clientCredentialType="None" proxyCredentialType="None"/>
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
    <client>
      <endpoint address="https://techspider/Service/Service1.svc"
        binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1"
        contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" />
    </client>
  </system.serviceModel>

But, when I call a method on the service, it seems to be accepting any user name / password, which makes me think that it is not going to the custom authentication class I implemented.

If I don't supply any UserName, it is throwing me an error which indicates my setup is correct.

   Service1Client client = new Service1Client();
    client.GetData(1);

The username is not provided. Specify username in ClientCredentials.

The below code even after supplying wrong credentials, was able to fetch me the output.

Service1Client client = new Service1Client();
client.ClientCredentials.UserName.UserName = "aaaa";
client.ClientCredentials.UserName.Password = "dddd";
client.GetData(1);

Can anyone suggest if I'm missing any Config on Server? How do I ensure my CustomAuthenticator gets executed with every method call. I searched a number of questions online but none of them resolved.

1

1 Answers

1
votes

The feeling that each and every username password combination will be accepted is right. That's the case because you're not really validating the credentials.

if (userName.Equals("user") && password.Equals("pass"))
{
    //Good to go
}

If these demo-credentials are submitted, you're good to go ;-) But what if this doesn't apply? If any other username password combination is submitted, then nothing happens! And that's exactly the reason why your service is accepting arbitrary credentials. In the catch-block you're throwing an informative FaultException, but it won't be triggered as long as there doesn't occur an exception (which isn't the case in this simple example).

If you take a look at the docs, there's a very important remark and a corresponding code sample:

Override the Validate method to specify how the username and password is validated. If the username and password do not pass validation, then throw a SecurityTokenValidationException.

So instead of doing nothing if the submitted credentials don't match you should throw a SecurityTokenValidationException to reject the login-attempt.

public class CustomAuthenticator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        try
        {
            if (userName.Equals("user") && password.Equals("pass"))
            {
                //Good to go
            }
            else
            {
                // add this line
                throw new SecurityTokenException("Unknown Username or Password");
            }
        }
        catch (Exception ex)
        {
            throw new FaultException("Authentication failed");
        }
    } 
}