CallumVass
CallumVass

Reputation: 11456

WCF Authentication not working

I've implemented my custom userNamePasswordValidationMode in my WCF app like so:

<system.serviceModel>
    <bindings>
        <wsHttpBinding>
            <binding>
                <security mode ="Message">
                    <message clientCredentialType="UserName"/>
                </security>
            </binding>
        </wsHttpBinding>
    </bindings>
    <behaviors>
        <serviceBehaviors>
            <behavior>
                <serviceMetadata httpGetEnabled="True"/>
                <serviceCredentials>
                    <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="MyProject.Validator.MyValidator, MyProject" />
                </serviceCredentials>
            </behavior>
        </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="true" />
</system.serviceModel>

This throws no errors, but when I reference my service in my client and set the username/password credentials, my methods are still called, even if I enter in the wrong password:

Testing.myAPIClient.client = new Testing.myAPIClient();
client.ClientCredentials.UserName.UserName = "test";
client.ClientCredentials.UserName.Password = "wrongpassword";

Console.WriteLine(client.StockQuery("123"));
Console.ReadLine();

The method StockQuery still gets called and the code in MyValidator doesn't even get called:

public class MyValidator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            using (var ax = new AXConnector())
            {
                if (!(bool)ax.CallStaticClassMethod("OnlineUsers", "validateLogon", userName, password))
                {
                    throw new UnauthorizedAccessException("Not Authorised");
                }
            }
        }
    }

Edit

Here is my app.config:

    <system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="BasicHttpBinding_IMyAPI" closeTimeout="00:01:00"
                openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
                maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
                useDefaultWebProxy="true">
                <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                    maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                <security mode="None">
                    <transport clientCredentialType="None" proxyCredentialType="None"
                        realm="" />
                    <message clientCredentialType="UserName" algorithmSuite="Default" />
                </security>
            </binding>
        </basicHttpBinding>
    </bindings>
    <client>
        <endpoint address="http://myServer:89/MyAPI.svc" binding="basicHttpBinding"
            bindingConfiguration="BasicHttpBinding_IMyAPI" contract="Testing.IMyAPI"
            name="BasicHttpBinding_IMyAPI" />
    </client>
</system.serviceModel>

Edit 2

Service Interface:

[ServiceContract]
public interface IMyAPI
{
    string UserName { [OperationContract] get; [OperationContract] set; }
    string Password { [OperationContract] get; [OperationContract] set; }

    [OperationContract]
    bool StockQuery(string partNo);

    [OperationContract]
    decimal PriceQuery(string partNo, string accNo);
}

Service Class:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required),
ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Single)]
public class MyAPI : IMyAPI
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public MyAPI()
    {
        this.CheckSecurity();
    }

    private void CheckSecurity()
    {
        if (this.UserName != "test" && this.Password != "123")
        {
            throw new UnauthorizedAccessException("Not Authorised");
        }
    }

    // StockQuery and PriceQuery methods...
}

Upvotes: 3

Views: 10323

Answers (2)

codeMonkey
codeMonkey

Reputation: 4855

We ran into the problem where everything worked fine for years, but now we're moving to an environment where everything is SSL-offloaded behind a load balancer, so we're no longer able to rely on transport. I was running into all the same problems as described by OP.

We're still using regular-degular basicHttpBinding. Got it working this way:

Created a class that would parse the header XML:

[DataContract(Namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", Name = "Security")]
public partial class SecurityHeaderType
{
    [XmlElement(Namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd")]
    [DataMember]
    public UsernameToken UsernameToken { get; set; }
}

public class UsernameToken : IXmlSerializable
{
    public string Username { get; set; }
    public string Password { get; set; }

    public XmlSchema GetSchema()
    {
        throw new NotImplementedException();
    }

    public void ReadXml(XmlReader reader)
    {
        Dictionary<string, string> secDictionary;
        string xml = reader.ReadOuterXml();

        using (var s = GenerateStreamFromString(xml))
        {
            secDictionary =
                XElement.Load(s).Elements()
                    .ToDictionary(e => e.Name.LocalName, e => e.Value);
        }

        Username = secDictionary["Username"];
        Password = secDictionary["Password"];

    }

    public Stream GenerateStreamFromString(string s)
    {
        var stream = new MemoryStream();
        var writer = new StreamWriter(stream);
        writer.Write(s);
        writer.Flush();
        stream.Position = 0;
        return stream;
    }

    public void WriteXml(XmlWriter writer)
    {
        throw new NotImplementedException();
    }
}

I followed @john-isaiah-carmona 's example for the Username/Password fields, then write to them like so:

public void CheckSecurity()
{
    if (OperationContext.Current.IncomingMessageHeaders.FindHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd") != -1)
    {
        var securityHeader = OperationContext.Current.IncomingMessageHeaders.GetHeader<SecurityHeaderType>("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        Username = securityHeader.UsernameToken.Username;
        Password = securityHeader.UsernameToken.Password;
    }
    else
        throw new FaultException("Unknown username or incorrect password.");
}

Upvotes: 0

John Isaiah Carmona
John Isaiah Carmona

Reputation: 5366

Your client Security.Mode is set to "None" which it should be "Message".

<security mode ="Message">
    <message clientCredentialType="UserName"/>
</security>

Edit: You might need a certificate to use that. You can follow this walkthrough, but it is not recommended to use it in production.

[1]Another option is to implement your own security. Here's a basic example.

WCF Service

In your service, change it's ServiceBehavior's InstanceContextMode to PerSession and ConcurrencyMode to Single

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Single)]
public class SomeService : ISomeService
{ 
    // ...
}

Add a Username and Password property in your service.

public string UserName { [OperationContract] get; [OperationContract] set; }
public string Password { [OperationContract] get; [OperationContract] set; }

Add a private method for checking a security.

public void CheckSecurity()
{
    if ((this.UserName == null || this.Password == null) ||
        this.UserName == "username" && this.Password == "password"))
    {
        throw new FaultException("Unknown username or incorrect password.");
    }
}

Then call the CheckSecurity method in each of your service [2]class constructor method.

public void SomeServiceMethod()
{
    this.CheckSecurity();
    // some method code
}

Client Application

In your client application code, set the service username and password for every instance, or create a static class that will do this for you.

You might also try to use encryption in the username and password to add security.

[1] Taken from my answer in How to put a password on a WCF Service?
[2] You need to call the CheckSecuirty method in the beginning of each of your service method(s).

Upvotes: 1

Related Questions