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.

Share on FacebookTweet about this on TwitterShare on Google+

15 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.

Leave a Reply

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