Tip #767: Server-to-server authentication is here

HeadlessWoot, woot! At long last we can create passive clients – the ones that do not have someone sitting in front of them. Clients like web sites or services – and authenticate them without using username and password AND get the magic bearer token that is good to use in Web API.

The detailed walkthrough is available, describing in glorious details the process of creating a web site (MVC applicaton) that can talk to Dynamics CRM. Don’t be put off by references to the web sites – you don’t have to be building one to take advantage of the feature. In fact, substantially lesser efforts required to create an equivalent console application that can be deployed as a Web Job and run unattended as a service.

  1. Register your application on Azure AD. No need for multi-tenant, single-tenant will do. What I found is that if you are using https://portal.azure.com, it does not create the application instance locally requiring the logon and consent. Easy to say with the web site, not so much with a console application. If instance is not created you will not be able to create an application user (see below). There is probably another way to do it but using old portal does immediately create an instance.
    Local Azure app
  2. Add permissions to access CRM and generate your key (same instructions)
  3. Create an application user. This is where the good stuff happens, we’re allowing a specific application to access CRM without a consent from a user.
  4. Create console application in Visual Studio and add nuget packages
  5. You are good to go!
  6. The code looks like this:
static void Main(string[] args)
{
    string organizationUrl = "https://notarealone.crm.dynamics.com";
    string clientId = "6db9cae3-dead-beef-dead-7179d4958975";
    string appKey = "get you own";
    string aadInstance = "https://login.microsoftonline.com/";
    string tenantID = "your tenant id";

    ClientCredential clientcred = new ClientCredential(clientId, appKey);
    AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID);
    AuthenticationResult authenticationResult = authenticationContext.AcquireToken(organizationUrl, clientcred);
    var requestedToken = authenticationResult.AccessToken;
            
    using (var sdkService = new OrganizationWebProxyClient(GetServiceUrl(organizationUrl), false))
    {
        sdkService.HeaderToken = requestedToken;

        OrganizationRequest request = new OrganizationRequest()
        {
            RequestName = "WhoAmI"
        };

        WhoAmIResponse response = sdkService.Execute(new WhoAmIRequest()) as WhoAmIResponse;
        Console.WriteLine(response.UserId);
    }
}
static private Uri GetServiceUrl(string organizationUrl)
{
    return new Uri(organizationUrl + @"/xrmservices/2011/organization.svc/web?SdkClientVersion=8.2");
}

Last but not least, to quickly find your tenant id, select application in either Azure portal and ask for Endpoints, your tenant id will be splattered all over endpoint urls as a guid that follows the domain.

21 thoughts on “Tip #767: Server-to-server authentication is here

  1. Dibutil says:

    What about licensing of such a client?

    • Dibutil,

      that’s the best part – no license is required for the application users.

      HTH
      George

      • Mariusz says:

        Have You got some article from Microsoft that say so? I just want to be clear about that, because it would be awesome, and I don’t think so ;).

        • Mariusz,

          understand the skeptisizm. That’s why I put hyperlinks in the article. They usually point to something.

          As far as an official statement regarding application user is concerned, I expect something to appear in the next drop of licensing guide. As for now, I’m content with assuming that application user does not need a license. To start with, there is nowhere to define the license even if you wanted to.

          Cheers
          George

    • Tracy,

      most of the posts you mention are quite old (by internet standards). S2S authentication has become available for Dynamics 365 not so long ago (one of the stackoverflow posts does mention that) and you’d need an updated ADAL library to get it all going. What I posted has been tried and tested; but, of course, your mileage may vary.

      Also worth noting that some of the posts refer to username/password authentication while the code I’m referring to uses clientid/secret credentials. Could be the source of misbehaviour as well.

      HTH
      George

  2. PhuocLe says:

    I get the error, can you help me

    System.ServiceModel.Security.MessageSecurityException: ‘The HTTP request is unauthorized with client authentication scheme ‘Anonymous’. The authentication header received from the server was ‘Bearer authorization_uri=https://login.windows.net/12b5c856-99c8-4268-b99f-b5bfd02ae0f3/oauth2/authorize, resource_id=https://xxxxxx.crm.dynamics.com/’.’

    WebException: The remote server returned an error: (401) Unauthorized.

    • HI PhuocLe,

      this error usually is a result of incorrect credentials (either client id or secret in S2S scenario). If you could paste the code snippet and point to the line throwing that exception, we can tourbleshoot it further.

      Thanks
      George

  3. Rohit Iyer says:

    This is brilliant, great article, this worked for me like a charm!!!

  4. Tim says:

    Looking to utilize ADAL due to the application user (clientid/secret) capabilities and also want to use the CRM SDK Upsert function.
    However the Upsert function utilizes the CRM SDK OrganizationalService which does not seem to allow for clientid/secret. Is there an option for this?

    • Hi Tim,

      authentication has little to do with the service functionality. Authentication using ADAL gives you a bearer token that can be used in OrganizationWebProxyClient that implements IOrganizationService that gives you access to Execute method, e.g.

      using (var sdkService = new OrganizationWebProxyClient(GetServiceUrl(organizationUrl), false))
          {
              sdkService.HeaderToken = requestedToken;
       
              UpsertRequest request = new UpsertRequest()
              {
                  Target = whatever
              };
       
              UpsertResponse response = sdkService.Execute(request) as UpsertResponse;
      
              Console.WriteLine($"Did we just create something? {response.RecordCreated}");
          }
      
      • Tim says:

        Thank you, I realized after the fact that you had just called that out, but in any event it helped to reiterate. I am hoping to get some help, I have taken what has been offered and tried to put into practice, however I keep running into issues. The best solution I have seen so far is taking what you have provided but when I try to perform the action I receive the response of “Value cannot be null. Parameter name: value”, from what I can gather it may have to do with EnableProxyTypes, which I don’t see as available utilizing OrganizationWebProxyClient. I have attempted to simply everything below to illustrate this. Please excuse my ignorance. Many thanks to any assistance.

        using System;
        using Microsoft.Crm.Sdk.Messages;
        using Microsoft.Xrm.Sdk;
        using Microsoft.Xrm.Sdk.Client;
        using System.ServiceModel.Description;
        using Microsoft.IdentityModel.Clients.ActiveDirectory;
        using Microsoft.Xrm.Sdk.WebServiceClient;
        using Microsoft.Xrm.Sdk.Messages;
        using System.Data;

        namespace Integration
        {
        class IntegrationTest
        {
        private static string OrganizationUrl = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.organizationUrl; //”https://.crm.dynamics.com”
        private static string SoapOrgSvcUrl = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.soaporgsvcurl; //”https://.api.crm.dynamics.com/XRMServices/2011/Organization.svc”
        //private static string SoapDiscSvcUrl = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.soapdiscsvcurl; //”https://disco.crm.dynamics.com/XRMServices/2011/Discovery.svc”
        private static string RestInstanaceUrl = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.restInstanaceURL; //”https://.api.crm.dynamics.com/api/data/v8.2″
        //private static string RestDiscUrl = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.restDiscUrl; //”https://disco.crm.dynamics.com/api/discovery/v8.2″
        private static string AuthParamUrl = OrganizationUrl + “/api/data/”;
        private static string ServiceUrl = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.serviceurl; //”/xrmservices/2011/organization.svc/web?SdkClientVersion=8.2″
        private static string EntityName = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.entityname; //””;
        private static string Username = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.username; //”@.com”
        private static string Password = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.password; //””
        private static string ClientId = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.clientid; //”016E00BE-43B9-4845-AA76-B4E2AE20513E”
        private static string Secret = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.secret; //”Adfasd45dfasfas6Gdsaflksa=”;
        //private static string AppUser = Microsoft.Crm.Sdk.Samples.Properties.Settings.Default.appuser; //”@.com”;

        static IOrganizationService _service;

        private static void Main(string[] args)
        {
        // GOAL : Utilize REST with clientid,secret
        int Method = 2; //1 : REST(OrganizationWebProxyClient) , 2 : SOAP(OrganizationServiceProxy)
        Integrate(ClientId, Secret, AuthParamUrl, EntityName, OrganizationUrl, ServiceUrl, Username, Password, SoapOrgSvcUrl, Method);
        // 1 : Value cannot be null. Parameter name: value
        // 2 : ok
        }

        private static void Integrate(string clientid, string secret, string authparamurl, string entityname, string organizationurl, string serviceurl, string username, string password, string soaporgsvurl, int method)
        {
        try
        {
        //create service
        if (method == 1) // REST(OrganizationWebProxyClient)
        {
        string authResult = GetAuthToken(clientid, secret, authparamurl, organizationurl); //auth token
        var proxy = new OrganizationWebProxyClient(new Uri(organizationurl + serviceurl), false);
        proxy.HeaderToken = authResult;
        //enable early-bound entity types
        //**************???**************
        _service = (IOrganizationService)proxy;
        }
        else if (method == 2) // SOAP(OrganizationServiceProxy)
        {
        ClientCredentials credentials = new ClientCredentials();
        credentials.UserName.UserName = username;
        credentials.UserName.Password = password;
        OrganizationServiceProxy proxy = new OrganizationServiceProxy(new Uri(soaporgsvurl), null, credentials, null);
        //enable early-bound entity types
        proxy.EnableProxyTypes();
        _service = (IOrganizationService)proxy;
        }
        //get records to be processed
        DataSet ds = GetRecordsToProcess(entityname);
        //loop through each record to be processed
        foreach (DataRow row in ds.Tables[0].Rows)
        {
        //build entity
        Entity entity = BuildEntity(row, entityname);
        //perform action
        Upsert(entity);
        }
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        Console.ReadKey();
        }
        }

        private static string GetAuthToken(string clientid, string secret, string authparamurl, string organizationurl)
        {
        string authResult = “”; //token
        try
        {
        //ADAL S22 AUTH
        AuthenticationParameters ap = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(authparamurl)).Result;
        ClientCredential clientcred = new ClientCredential(clientid, secret);
        AuthenticationContext authContext = new AuthenticationContext(ap.Authority, false);
        authResult = authContext.AcquireTokenAsync(organizationurl, clientcred).Result.AccessToken;
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        Console.ReadKey();
        }
        return authResult;
        }

        private static DataSet GetRecordsToProcess(string entityname)
        {
        //records to be processed (ERP to CRM)
        DataSet dataset = new DataSet();
        try
        {
        DataTable dtData = new DataTable(entityname);
        dtData.Columns.Add(“new_KeyField1”);
        dtData.Columns[“new_KeyField1”].Unique = true;
        dtData.Columns[“new_KeyField1”].DataType = System.Type.GetType(“System.Int32”);
        dtData.Columns.Add(“new_KeyField2”);
        dtData.Columns[“new_KeyField2”].Unique = true;
        dtData.Columns[“new_KeyField2”].DataType = System.Type.GetType(“System.Int32”);
        dtData.Columns.Add(“new_sf_Description”);
        dtData.Rows.Add(1, 1, “1”);
        dataset.Tables.Add(dtData);
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        Console.ReadKey();
        }
        return dataset;
        }

        private static Entity BuildEntity(DataRow dr, string entityname)
        {
        Entity entity = new Entity();
        try
        {
        //get entity definition – early bound entity data model
        switch (entityname)
        {
        case “new_custom_entity”:
        new_custom_entity returnentity = new new_custom_entity { new_name = entityname };
        entity = returnentity;
        break;
        default:
        break;
        }
        //loop through each record “column”
        int itmindex = 0;
        foreach (var item in dr.ItemArray)
        {
        {
        //attach record data to entity
        //***need a way to capture the datatypes form the entity definition
        entity[dr.Table.Columns[itmindex].ColumnName.ToLower()] = item;
        //check if column is a key – add to entity key
        if (dr.Table.Columns[itmindex].Unique)
        {
        entity.KeyAttributes.Add(dr.Table.Columns[itmindex].ColumnName.ToLower(), item);
        }
        }
        itmindex++;
        }
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        Console.ReadKey();
        }

        return entity;
        }

        private static void ValidateConnection()
        {
        try
        {
        Guid userid = ((WhoAmIResponse)_service.Execute(new WhoAmIRequest())).UserId;
        if (userid != Guid.Empty)
        {
        Console.WriteLine(“Connection Established Successfully”);
        Console.ReadKey();
        }
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        Console.ReadKey();
        }
        }

        private static void Upsert(Entity entity)
        {
        //build request
        UpsertRequest request = new UpsertRequest()
        {
        Target = entity
        };
        ValidateConnection();
        //perform action
        UpsertResponse response = _service.Execute(request) as UpsertResponse;
        Console.WriteLine(response);
        Console.ReadKey();
        }
        }
        }

        • Tim says:

          After much searching and trying a solution has been found – Thanks Nathan!

          The fixing piece was set the second parameter of the OrganizationWebProxyClient from false to true.

          ‘var proxy = new OrganizationWebProxyClient(new Uri(organizationurl + serviceurl), false);’

          fixed to:

          ‘var proxy = new OrganizationWebProxyClient(new Uri(organizationurl + serviceurl), true);’

  5. mmiles says:

    Hi Guys, The Article is really helpful.

    Now I have problem with change token expire date. My Web Job is executing more than one hour. After this time when my app try call D365 method I get 401 Unauth because token has expired.

    Do you have any solution for above situation? I want to change default expire on date but I cannot find any hints

    • You should be able to use RefreshToken if it’s available. But general rule of thumb is to avoid caching and reusing your tokens and always use AcquireToken method. ADAL will cache and renew tokens for you as required.

  6. Florin says:

    Thanks a lot for this tip!

  7. Adam says:

    Any ideas why I would be getting this error when im trying to use the org service?

    System.ServiceModel.ProtocolException : Content Type text/xml; charset=utf-8 was not supported by service https://XXXXXXX.api.crm4.dynamics.com/XRMServices/2011/Organization.svc?SdkClientVersion=8.2. The client and service bindings may be mismatched.
    —-> System.Net.WebException : The remote server returned an error: (415) Cannot process the message because the content type ‘text/xml; charset=utf-8’ was not the expected type ‘application/soap+xml; charset=utf-8’..

    • Adam

      Because you’re sending something that is not expected by the server. Hard to tell without any details. Suggest that you post on public forums including more details like a snippet of your code.

      George

  8. Tounisiano says:

    First Thank you for this blog, i have migrated from .NET FrameWork to .NET Core
    and when run project i get error

    Could not load type System.ServiceModel.Description.MetadataConversionError’ from assembly ‘System.ServiceModel, beacuse ClientCredentials is not supported with .NET core, there is any way to combine ADAL with OrganizationServiceProxy ? there is my full code
    ““
    public static class CRMConnection
    {
    private static OrganizationServiceProxy sp = null;
    private static IWebProxy proxy = WebRequest.DefaultWebProxy;
    private static string URL;
    private static string Login;
    private static string MDP;
    private static string B64_Encoded_MDP;
    private static string Xml_Settings_Path = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.FullName + @”\VSMP_Settings_File.xml”;
    private static XmlDocument VELBOURN_Settings;

    public static OrganizationServiceProxy InitCRMConnection()
    {
    try
    {

    const SslProtocols _Tls12 = (SslProtocols)0x00000C00;

    const SecurityProtocolType Tls12 = (SecurityProtocolType)_Tls12;

    ServicePointManager.SecurityProtocol = Tls12;

    VELBOURN_Settings = new XmlDocument();
    VELBOURN_Settings.Load(Xml_Settings_Path);
    B64_Encoded_MDP = VELBOURN_Settings.DocumentElement.SelectSingleNode(“//CRMConnection_Settings//MDP”).InnerText;
    MDP = Base64Decode(B64_Encoded_MDP);
    URL = VELBOURN_Settings.DocumentElement.SelectSingleNode(“//CRMConnection_Settings//CRMLink”).InnerText;
    Login = VELBOURN_Settings.DocumentElement.SelectSingleNode(“//CRMConnection_Settings//Login”).InnerText;

    PropertyInfo _WebProxy = proxy.GetType().GetProperty(“WebProxy”, BindingFlags.NonPublic | BindingFlags.Instance);
    WebProxy wProxy = (WebProxy)_WebProxy.GetValue(proxy, null);
    wProxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;

    if (string.IsNullOrEmpty(URL) || string.IsNullOrEmpty(Login) || string.IsNullOrEmpty(MDP))
    throw new Exception(“connection failed”);

    Uri OrganizationUri = new Uri(Path.Combine(URL + “XRMServices/2011/Organization.svc”));
    ClientCredentials cc = new ClientCredentials();
    ClientCredentials cd = null;

    cc.UserName.UserName = Login;
    cc.UserName.Password = MDP;

    sp = new OrganizationServiceProxy(OrganizationUri, null, cc, cd);
    sp.ServiceConfiguration.CurrentServiceEndpoint.EndpointBehaviors.Add(new ProxyTypesBehavior());

    }
    catch (Exception ex)
    {
    LogHelper.Writer(“Erreur lors de la connexion au crm, Message de l’erreur : ” + Environment.NewLine + ex.ToString());
    }
    return sp;
    }
    private static string Base64Decode(string base64EncodedData)
    {
    var base64EncodedBytes = Convert.FromBase64String(base64EncodedData);
    return Encoding.UTF8.GetString(base64EncodedBytes);
    }
    }`
    ““
    Cordailly

    • Tounisiano,

      it’s not going to work. System.ServiceModel is not supported on .NET Core. WCF is not supported either and you will not be able to work with OrganizationServiceProxy.

      We all wish for the migration to .NET Core to be that easy but it’s not. What you should be doing is moving to Web API implementation + ADAL/MSAL for authentication. Or wait for the SDK libraries to be released on .NET Core

      HTH
      George

Leave a Reply

Your email address will not be published. Required fields are marked *