Apache CXF client for claims-mode xRM (Microsoft D

2020-06-04 12:50发布

问题:

I'm trying to create an Apache CXF (2.7.5) client for the Microsoft Dynamics CRM 2011 ("xRM") web services (which I understood to be based on WCF 4) where CRM is in claims mode, so that the WSDL for this web service points to an STS (in my case AD FS 2.0).

My main question: Are there any tutorial, suggestions, blog posts to help me out (either describing how to send claims, or how to avoid them and instead use Windows authentication)?

Below is a description of what I've done until now.


I already had working code for the same web service, which works when CRM is in Windows authentication mode. That code is based on "CXF and MS CRM 2011" on Groovy Tom's Blog.

To support claims mode, I additionally needed to include org.apache.cxf:cxf-rt-ws-mex, so that the xRM WSDL can be parsed by CXF. Then I needed to make the CXF built-in STS client use SOAP 1.2:

client.getRequestContext().put("ws-security.sts.client-soap12-binding", "true");

to avoid an error 500 from AD FS 2.0. (Apparently AD FS 2.0 expects the /adfs/services/trust/mex endpoint to be called using SOAP 1.2, and CXF defaults to SOAP 1.1. I had to find this out from AD FS's WCF trace, which reported

System.ServiceModel.ProtocolException: Content Type text/xml; charset=UTF-8 was sent to a service expecting application/soap+xml; charset=utf-8. The client and service bindings may be mismatched.

when Apache CXF used SOAP 1.1.)

Then there was another problem: the WSDL returned by AD FS's /adfs/services/trust/mex endpoint seemed incomplete, in that it contains

<wsdl:types>
    <xsd:schema
        targetNamespace="http://schemas.microsoft.com/ws/2008/06/identity/securitytokenservice/Imports">
        <xsd:import namespace="http://schemas.microsoft.com/Message" />
        <xsd:import namespace="http://schemas.xmlsoap.org/ws/2005/02/trust" />
        <xsd:import namespace="http://docs.oasis-open.org/ws-sx/ws-trust/200512" />
    </xsd:schema>
</wsdl:types>

so none of the imports has a schemaLocation. This makes CXF complain that

org.apache.cxf.wsdl11.WSDLRuntimeException: Part request defined as element {http://docs.oasis-open.org/ws-sx/ws-trust/200512}RequestSecurityToken which is not in the schema.

I found out what caused this: the schemas containing RequestSecurityToken etc. are in the mex SOAP call result, but in separate <wsx:MetadataSection Dialect="http://www.w3.org/2001/XMLSchema"> sections, which the code in AbstractSTSClient completely ignores.

So I put in place my own WSDLFactory+WSDLReader (using property {{javax.wsdl.factory.WSDLFactory}}), which just inserts the schemas for the three namespaces into any WSDL it reads.

Now I'm blocked at the next point: the xRM WSDL (after formatting) contains an Address of http://www.w3.org/2005/08/addressing/anonymous (see below), which somehow causes CXF to look up that endpoint in AD FS's metadata. However, such an endpoint of course does not exist: it contains, e.g., https://...:.../adfs/services/trust/2005/usernamemixed.

<wsdl:definitions
    targetNamespace="http://schemas.microsoft.com/xrm/2011/Contracts/Services"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
    snipped="other xmlns attributes">
    <wsp:Policy wsu:Id="CustomBinding_IOrganizationService_policy">
        <wsp:ExactlyOne>
            <wsp:All>
                <!-- snip -->
                <sp:EndorsingSupportingTokens
                    xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">
                    <wsp:Policy>
                        <sp:IssuedToken
                            sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient">
                            <Issuer
                                xmlns="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">
                                <Address xmlns="http://www.w3.org/2005/08/addressing">
                                    http://www.w3.org/2005/08/addressing/anonymous
                                </Address>

So what can I do now?


More generally, my question is now: Am I on the right path for building a Java client for xRM-in-claims-mode? How have others gotten this to work? Or is there perhaps a way to avoid using claims, and instead use Windows authentication with xRM-in-claims-mode?

回答1:

We've finally gotten it to work, using not only "CXF and MS CRM 2011" on Groovy Tom's Blog which I mentioned in this question, but also "Using Apache CXF to connect to Microsoft Dynamics" by Jan-Hendrik Kuperus on the JH on Java blog (archive.org cached copy), and in addition correcting (?) the AD FS 2.0 WSDL.

Unfortunately I cannot directly post any code for licensing reasons, but here is an overview of what we did.


The key part of Jan-Hendrik Kuperus's solution is that we create our own STSClient, instead of letting CXF create one. That works around the issue of the ignored <wsx:MetadataSection Dialect="http://www.w3.org/2001/XMLSchema"> sections. It also works around the addressing issue from my question, that since has been fixed in CXF trunk. (We cannot switch to the latest CXF version, unfortunately: all of this was done with CXF 2.7.5.)

In that custom STS client, we point to a specific AD FS endpoint, make sure we use SOAP 1.2 (prevent HTTP error 500, see question), and switch off 'renewing':

STSClient stsClient = new STSClient(bus);
stsClient.setSoap12();
stsClient.setWsdlLocation(wsdlLocation.toExternalForm());
stsClient.setServiceQName(new QName("http://schemas.microsoft.com/ws/2008/06/identity/securitytokenservice", "SecurityTokenService"));
stsClient.setEndpointQName(new QName("http://schemas.microsoft.com/ws/2008/06/identity/securitytokenservice", "UserNameWSTrustBinding_IWSTrust13Async"));
stsClient.setSendRenewing(false);

(If 'renewing' is not switched off, then AD FS 2.0 returns a SOAP fault "ID3035: The request was not valid or is malformed." The AD FS trace says, "Microsoft.IdentityModel.SecurityTokenService.InvalidRequestException: MSIS3137: The RequestSecurityTokenElement contained an unsupported WS-Trust parameter: 'Renewing'.")

Now register stsClient in the request context under property SecurityConstants.STS_CLIENT ("ws-security.sts.client"), set request context property SecurityConstants.USERNAME, and in property SecurityConstants.CALLBACK_HANDLER register a CallbackHandler that handles the resulting WSPasswordCallback and sets the password, and you're in business. Except.

Except that at that point we found that CXF 2.7.5 chokes on AD FS's WSDL: java.lang.IllegalArgumentException: sp:KeyValueToken/wsp:Policy must have a value in KeyValueTokenBuilder#build(). It turns out that the WSDL contains a number of security policies with attribute wsp:Optional="true", and for each of these CXF expects a nested <wsp:Policy> element. So what we did is pre-process the AD FS WSDL, and just added empty <wsp:Policy/> elements in these places.

(We have no idea whether CXF 2.7.5 is too strict here, or whether AD FS 2.0's WSDL does not follow the standards.)


Also, we achieved to dynamically switch between a Windows-mode xRM and claims-mode xRM system, by looking at the <ms-xrm:AuthenticationPolicy> element in the xRM WSDL's security policy, and check whether <ms-xrm:Authentication> contains either ActiveDirectory or Federation. We did this by creating a custom policy extension extending XmlPrimitiveAssertion and registering the corresponding custom builder in bus.getExtension(AssertionBuilderRegistry.class). Then we create the custom STSClient in a custom 'out interceptor':

private static class XRMAuthSecurityModeInterceptor extends AbstractSoapInterceptor {
    public XRMAuthSecurityModeInterceptor() {
        super(Phase.PREPARE_SEND);
        addBefore("IssuedTokenOutInterceptor");
    }

    public void handleMessage(SoapMessage message) throws Fault {
        // if the custom assertion with security mode Federation is present, then create STSClient and...
            message.setContextualProperty(SecurityConstants.STS_CLIENT, stsClient);
    }
}

Finally, since we did not want to work with a downloaded version of AD FS's WSDL, we did the 'fixing' of that WSDL in the same 'out interceptor', by getting the SP12Constants.ISSUED_TOKEN assertion , getting its .getIssuerEpr().getMetadata().getAny(), and from that the {http://www.w3.org/2005/08/addressing}Address. The result is something like http://example.com:12345/adfs/services/trust/mex. We retrieve that URL, parse the XML, add the <wsp:Policy/> elements as described above, save the result to a file, and use that file's URL as the STSClient's wsdlLocation.


Hopefully this helps others; feel free to ask questions if you don't get this to work.



回答2:

I've merged a fix for the "anonymous" issue to CXF:

https://issues.apache.org/jira/browse/CXF-5807

When the Issuer address is "anonymous", you can specify the desired STS Endpoint name via the Metadata, or failing that it will fall back to just picking the first Port in the received WSDL.

Colm.