Woot, 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.
- 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.
- Add permissions to access CRM and generate your key (same instructions)
- 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.
- Create console application in Visual Studio and add nuget packages
- You are good to go!
- 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.
What about licensing of such a client?
Dibutil,
that’s the best part – no license is required for the application users.
HTH
George
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
But what can you really do with CRM via an app, if the API does not allow access?
See the following threads:
https://community.dynamics.com/crm/f/117/t/193506
http://stackoverflow.com/questions/37454539/using-adal-c-sharp-as-confidential-user-daemon-server-server-to-server-401-u
http://stackoverflow.com/questions/37215742/401-unauthorized-authentication-using-rest-api-dynamics-crm-with-azure-ad/41901426#41901426
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
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
This is brilliant, great article, this worked for me like a charm!!!
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.
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();
}
}
}
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);’
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.
Thanks a lot for this tip!
You’re welcome – that’s what we’re here for!
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
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