August 17, 2017

Quick Note: ADFS Authentication for Microsoft Dynamics 365

This is a quick note about a problem that took me several hours to figure out because there is few helpful information out there. So if you happen to have a Microsoft Dynamics 365 system and want to fetch an authentication token from ADFS, read on.

The Problem

Microsoft Dynamics 365 is a tool to manage customer relations (CRM), and we use it to track all communication with our B2B customers. The problem at hand was to feed it some information about customer from another system (written in Java), and thankfully, Dynamics has a webservice, so this should be easy.


Well, the webservice part is (fairly) easy, but authentication requires some deep knowledge about the mechanism used if you are not on dotnet (since all examples and libraries from Microsoft are only for C# or Visual Basic). 

So, how to call a dynamics 365 webservice URL from Java (or any language) with an authentication token from ADFS?

Fetching a Token from ADFS

Active Directory Federation Services (ADFS) is Microsofts single sign on solution, and it implements the WS-Trust protocol. There is an excellent blog post about how to fetch a token from ADFS by Leandro Boffi.

His example works, but sadly it returns not the kind of token we need for the dynamics 365 REST API (at least I could not get it to work).

The good news is that you only have to modify the sample soap request a bit to get a JWT token (instead of a SAML token):

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
        <a:To s:mustUnderstand="1">__ADFSURL__</a:To>
        <o:Security s:mustUnderstand="1"                    xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <o:UsernameToken>
                <o:Username>__USERNAME__</o:Username>
                <o:Password>__PASSWORD__</o:Password>
            </o:UsernameToken>
        </o:Security>
    </s:Header>
    <s:Body>
        <trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
            <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
                <a:EndpointReference>
                    <a:Address>__APPLICATION__</a:Address>
                </a:EndpointReference>
            </wsp:AppliesTo>
            <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
            <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
            <trust:TokenType>urn:ietf:params:oauth:token-type:jwt</trust:TokenType>
        </trust:RequestSecurityToken>
    </s:Body>
</s:Envelope>

Notice the change in <trust:TokenType>. If you send this soap request to

https://adfs.YOURCOMPANY.com/adfs/services/trust/13/usernamemixed 

you get the desired JWT token. Make sure you have set the correct content type header:

setHeader("Content-Type", "application/soap+xml; charset=utf-8")

The response should look like this:

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
    <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal</a:Action>
    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
        <u:Timestamp u:Id="_0">
            <u:Created>2017-08-16T07:00:16.458Z</u:Created>
            <u:Expires>2017-08-16T07:05:16.458Z</u:Expires>
        </u:Timestamp>
    </o:Security>
</s:Header>
<s:Body>
    <trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
        <trust:RequestSecurityTokenResponse>
            <trust:Lifetime>
                <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2017-08-16T07:00:16.443Z</wsu:Created>
                <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2017-08-16T15:00:16.443Z</wsu:Expires>
            </trust:Lifetime>
            <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
                <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
                    <wsa:Address>https://crm365.xxx.at/</wsa:Address>
                </wsa:EndpointReference>
            </wsp:AppliesTo>
            <trust:RequestedSecurityToken>
                <wsse:BinarySecurityToken wsu:Id="_5b817b78-8e77-40b6-be5a-c415382239a1-2A2FD48CB8F02AF6D6580E017B5D04BF"                                           ValueType="urn:ietf:params:oauth:token-type:jwt"                                           EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"                                           xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"                                          xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
                    xxxxx</wsse:BinarySecurityToken>
            </trust:RequestedSecurityToken>
            <trust:TokenType>urn:ietf:params:oauth:token-type:jwt</trust:TokenType>
            <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
            <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
        </trust:RequestSecurityTokenResponse>
    </trust:RequestSecurityTokenResponseCollection>
</s:Body>
</s:Envelope>

Extract the part between the <wsse:BinarySecurityToken> Tags. 
One final culprit: The token is Base64 encoded. Decode it to get the JWT token.

Use the JWT token with dynamics 365

Finally, we can use the token to make a call to e.g. https://crm365.xxx.at/XRMServices/2011/OrganizationData.svc/AccountSet(guid'XXX')

Just the following header with each request:

setHeader("Authorization", "Bearer " + token)


This should do it, hope someone finds it helpful. 

No comments:

Post a Comment