Data8 / DataverseClient

WS-Trust compatible version of the Microsoft Dataverse client for .NET Core
MIT License
33 stars 16 forks source link

Authenticate of ADAuthClient method is failing #10

Closed vmcbaptista closed 2 years ago

vmcbaptista commented 2 years ago

Hi. In the environment I'm using to test this client, Authenticate is crashing. By looking into the code and the requests that were happening, I believe that this is occurring because the negotiation flow for issuing the token is not right.

WS-Trust specification says the following:

  1. A request is initiated with a that identifies the details of the request (and may contain initial negotiation/challenge information)
  2. A response is returned with a that contains additional negotiation/challenge information. Optionally, this may return token information in the form of a (if the exchange is two legs long).
  3. If the exchange is not complete, the requestor uses a that contains additional negotiation/challenge information.
  4. The process repeats at step 2 until the negotiation/challenge is complete (a token is returned or a Fault occurs). In the case where token information is returned in the final leg, it is returned in the form of a . https://docs.oasis-open.org/ws-sx/ws-trust/200512/ws-trust-1.3-os.html#_Toc162064978

This means that wst:RequestSecurityToken shall be sent only once, and wst:RequestSecurityTokenResponse shall be sent in a loop until we get the token.

So looking to the https://github.com/Data8/DataverseClient/blob/51e04e97ba74fd5e1fc242afe3fe3679039f5334/Data8.PowerPlatform.Dataverse.Client/ADAuthClient.cs#L113

I can see that the while loop can make wst:RequestSecurityToken to be sent more than once. This was what happened with me.

A possible way to fix this might be something like:

var rst = new RequestSecurityToken(token);
            var resp = rst.Execute(_url, auth);

            // Keep exchanging tokens until we get a full RSTR
            var finalResponse = resp as RequestSecurityTokenResponseCollection;
            while (finalResponse == null)
            {
                if (!(resp is RequestSecurityTokenResponse r)) throw new Exception("Did not get RequestSecurityTokenResponse as expected.");
                _context.Init(r.BinaryExchange.Token, out token);
                resp = new RequestSecurityTokenResponse(r.Context, token).Execute(_url, auth);
                finalResponse = resp as RequestSecurityTokenResponseCollection;
            }

What do you think about this?

MarkMpn commented 2 years ago

I agree with your diagnosis, but I've never seen this happen with any of the example instances I've used for testing. In my testing I've either seen a single leg (RST -> RSTRC) or two legs (RST -> RSTR -> RSTR -> RTRC). Both these are handled by the current code. I'm happy to make a change along the lines you suggest, but I'd like to see it failing in action to prove the change works. Do you have any details about how your instance is configured that might make it require an additional round trip, and/or a Fiddler log of a successful connection attempt from the old SDK?

vmcbaptista commented 2 years ago

I agree with your diagnosis, but I've never seen this happen with any of the example instances I've used for testing. In my testing I've either seen a single leg (RST -> RSTRC) or two legs (RST -> RSTR -> RSTR -> RTRC). Both these are handled by the current code. I'm happy to make a change along the lines you suggest, but I'd like to see it failing in action to prove the change works. Do you have any details about how your instance is configured that might make it require an additional round trip, and/or a Fiddler log of a successful connection attempt from the old SDK?

Here you have 2 fiddler logs, the one named old sdk shows what happens when executing the constructor of CrmServiceClient. The other file shows what happens when invoking the OnPremiseClient followed by an Execute(new WhoAmIRequest())

fiddler.zip

I hope that it helps

MarkMpn commented 2 years ago

Thanks - still not sure how I can get my test servers to use that behaviour, but I can see it happening for you. Would you like to submit your proposed change as a PR?

vmcbaptista commented 2 years ago

Yes, I can make a PR, although the implementation I've proposed does not look into state returned by context.init. I actually do not know which condition shall I add to prevent infinite loop in case we never get a RTRC

MarkMpn commented 2 years ago

OK, I've just opened #14 which I believe should fix it, but I'm still setting up the tests. As I can't get my server to produce this extra leg of the authentication process, can you try running this modified code against your instance to see if that authenticates correctly?

vmcbaptista commented 2 years ago

I can confirm that your PR works with my instance.

BTW I've noticed that there is another thing that might break in some environments. The Identity under EndpointReference contains only Upn. So, in instances configured with Spn instead of Upn, it will break. I've also seen situations where Identity was not present inside EndpointReference (this is most probably some misconfiguration), nevertheless, I think the code here: https://github.com/Data8/DataverseClient/blob/623986dc51e1446f7f52b0100998aeb7e2e34458/Data8.PowerPlatform.Dataverse.Client/OnPremiseClient.cs#L122 could be improved to support Spn and EndpointReference without identity.

I think that the scenario without identity can be resolved to null, as the description for serverPrinc of ClientContext of SSPI says, "The principle name of the server to connect to, or null for any"

MarkMpn commented 2 years ago

So far I've been unable to replicate a situation where the WSDL doesn't include a UPN. Do you have any instructions you can share for me to set this test up?

vmcbaptista commented 2 years ago

I have no clue about how this was configured, but one of the Dynamics I have in my company has the following WSDL. Hope it helps

<wsdl:definitions
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
    xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex"
    xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy"
    xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract"
    xmlns:i0="http://schemas.microsoft.com/xrm/2011/Contracts/Services"
    xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
    xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:tns="http://schemas.microsoft.com/xrm/2011/Contracts"
    xmlns:wsa10="http://www.w3.org/2005/08/addressing"
    xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" name="OrganizationService" targetNamespace="http://schemas.microsoft.com/xrm/2011/Contracts">
    <wsdl:import namespace="http://schemas.microsoft.com/xrm/2011/Contracts/Services" location="https://w19-crm19-1.contoso50.com/Contoso50/XRMServices/2011/Organization.svc?wsdl=wsdl0"/>
    <wsdl:types/>
    <wsdl:service name="OrganizationService">
        <wsdl:port name="CustomBinding_IOrganizationService" binding="i0:CustomBinding_IOrganizationService">
            <soap12:address location="http://w19-crm19-1.contoso50.com/Contoso50/XRMServices/2011/Organization.svc"/>
            <wsa10:EndpointReference>
                <wsa10:Address>http://w19-crm19-1.contoso50.com/Contoso50/XRMServices/2011/Organization.svc</wsa10:Address>
                <Identity
                    xmlns="http://schemas.xmlsoap.org/ws/2006/02/addressingidentity">
                    <Spn>host/W19-CRM19-1.contoso50.com</Spn>
                </Identity>
            </wsa10:EndpointReference>
        </wsdl:port>
        <wsdl:port name="CustomBinding_IOrganizationService1" binding="i0:CustomBinding_IOrganizationService1">
            <soap12:address location="https://w19-crm19-1.contoso50.com/Contoso50/XRMServices/2011/Organization.svc"/>
            <wsa10:EndpointReference>
                <wsa10:Address>https://w19-crm19-1.contoso50.com/Contoso50/XRMServices/2011/Organization.svc</wsa10:Address>
            </wsa10:EndpointReference>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
MarkMpn commented 2 years ago

Can you check the latest code from #14 and see if that authenticates correctly against this instance please?

vmcbaptista commented 2 years ago

Yes #14 works fine 👍