CRM Oct-2011 SDK update and new authentication method.
With the roll out of the CRM 2011 on Office 365, a new authentication process was added to support it and allow for delegated authentication for CRM online. This authentication method also works for CRM Online (Live) and all forms of Premise base authentication.
Lets talk about History for just min.
There are several ways to do authentication from client applications that have existed since RTM, as a general rule, they are either done using the simple constructor on the proxy, or the IServiceConfiguration interface.
What I call simple constructor approach provides two constructors, one for the Discovery Service Proxy, and one for the Organization Service Proxy
For the Discovery Service Proxy, the simple constructor requires you to know a lot about the server connection up front, the same is true for the simple constructor for the Organization Service Proxy
You need to know what the URL of the service you want to connect too is, you need to know the user and device credentials, the home realm URI, and the right parameters to use with the right settings for the type of auth you want.
Seems simple, what's going on there behind the scenes though is pretty complicated and fairly time consuming. In short, for each type of of Service Proxy, the SDK is calling the WCF MEX service on CRM to get the WSDL to build the proxy for the type of server connection, then wiring up that proxy and returning it to the caller. Notice I didn’t say “Authenticate” there… Authentication happens the first time to you try to do something with the newly created service proxy.
From a time perspective, that’s a pretty heavy operation. One of the worst things that can happen here its to create, use, destroy , recreate , etc...
For performance, you really need to hold on to the constructed proxy and use it for the life of your connect. The other challenge here is that you don’t have visibility to what is taking up time in this operation… is it the WSDL create? or the Auth? or the proxy Generation ? or perhaps the call to CRM itself.
In the RTM Release, we were also provided the IServiceConfiguration interface.. which provided us with a more component based capability to set up the Proxy, Authenticate, and create the Discovery Service or Organization Service proxy. Using the IServiceConfiguration Method, via the ServiceConfigurationFactory.CreateConfiguration method, we can setup the Proxy, then manage authentication, then create the proxy we wish with the preconfigured IServiceConfiguration interface.
Now, the sticky part in the IServiceConfiguration interface is authentication. there are several different ways to do it, based on the authentication type you need and the CRM Server type you are connected too.
There are several examples in the CRM SDK that cover this process.
Microsoft.Xrm.Sdk.Client.IServiceManagement
The new IServiceManagment interface provides a us the same ability of the IServiceConfigurationmethod, with a much more straight forward and easy to use authentication process, Additionally it fully supports OSDP ( that’s CRM on Office 365 ) via the authentication process, without having to do anything “special”, and allows for a fairly easy centralization of proxy generation for the CRM connection.
This authentication data is wrapped up nicely in a new class called AuthenticationCredentials and a method on the IServiceManagement interface called Authenticate.
With the release of the service management interface, I moved much of my code over to use it in order to streamline the CRM connect process in my environment, however in testing I was challenged with Delegated Claims authentication.
Using Delegated Claims Auth and the IServiceManagement interface.
In an attempt to be clear:
I had an on premise CRM instance that was configured for claims… lets call it crm.contso.com. It is also configured to accept authentication for users in the Fabrikam.com domain.
Now, in the IServiceConfiguration interface, I would need to write code to deal with this using the cross domain auth process, using the HomeRealm URI to access the correct auth store. The code that supports that , of course, was unique and different then using the code to hit windows live, IFD or AD.
Using the IServiceManagement interface, I set up my code to look a like this:
Where userCredentials is myuser@fabrikam.com + the password, and homeRealm pointed at the ADFS Auth service for Fabrikam.com
1: AuthenticationCredentials authCred = new AuthenticationCredentials();
2: authCred.ClientCredentials = userCredentials;
3:
4: if (servicecfg.AuthenticationType == AuthenticationProviderType.LiveId )
5: authCred.SupportingCredentials = new AuthenticationCredentials() { ClientCredentials = deviceCredentials };
6:
7: // Claims
8: if (homeRealm != null)
9: authCred.HomeRealm = homeRealm;
10:
11: // Deal with an incorrect configuration here.
12: // Home Realm should not be used if the Service Identifier and the homeRealm are the same thing.
13: if (homeRealm != null && homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier)
14: authCred.AppliesTo = new Uri(servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier);
15:
16: // Run Authentication
17: // Failure will generate Exceptions
18: authCred = servicecfg.Authenticate(authCred);
As the Homerealm is provided , I set the AppliesTo property of the AuthenticationCredentials class to it. That will tell the authentication method to authenticate against Fabrikam’s ADFS system.
Next I get the Security Token and pass it to the Proxy Constructor that I want.. in this case the discovery server:
1: SecurityTokenResponse AuthKey = authCred.SecurityTokenResponse;
2: DiscoveryServiceProxy proxy = new DiscoveryServiceProxy((IServiceManagement<IDiscoveryService>)servicecfg, AuthKey);
This call generates a a MessageSecurityException exception. But why ?
Well, going back over the SDK docs and other sample code… I was unable to come with an answer, I had even tried to use the sample code provided in the SDK to do this. Still no luck.
Reach out internally I found out that there is one extra step that needs to be done to make delegated claims auth work.
In sum, what needed to be done was to check the IServiceManagement PolicyConfiguration.SecureTokenServiceIdentifier against homeRealm, if they were different, I needed to execute a second auth on the IServiceManagement endpoint using SecurityTokenResponse of the auth against Fabrikam.
once I had done that… it worked.. so it ended up looking like this:
1: AuthenticationCredentials authCred = new AuthenticationCredentials();
2: authCred.ClientCredentials = userCredentials;
3:
4: if (servicecfg.AuthenticationType == AuthenticationProviderType.LiveId )
5: authCred.SupportingCredentials = new AuthenticationCredentials() { ClientCredentials = deviceCredentials };
6:
7: // Claims
8: if (homeRealm != null)
9: authCred.HomeRealm = homeRealm;
10:
11: // Deal with an incorrect configuration here.
12: // Home Realm should not be used if the Service Identifier and the homeRealm are the same thing.
13: if (homeRealm != null && homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier)
14: authCred.AppliesTo = new Uri(servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier);
15:
16: // Run Authentication
17: // Failure will generate Exceptions
18: authCred = servicecfg.Authenticate(authCred);
19:
20:
21: // If is Federation and HomeRealm is not null, and HomeRealm is Not the same as the SecureTokeServiceIdentifier
22: // Run Seconday auth to auth the Token to the right source.
23: SecurityTokenResponse AuthKey = null;
24: if (servicecfg.AuthenticationType == AuthenticationProviderType.Federation &&
25: homeRealm != null &&
26: !string.IsNullOrWhiteSpace(homeRealm.ToString()) &&
27: (homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier))
28: {
29: // Relaying Auth to Resource Server
30: dtAuthQuery = DateTime.UtcNow;
31: // Auth token against the correct server.
32: AuthenticationCredentials authCred2 = servicecfg.Authenticate(new AuthenticationCredentials()
33: {
34: SecurityTokenResponse = authCred.SecurityTokenResponse
35: });
36: logEntry.Log(string.Format("{0} - Authenticated via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtAuthQuery.Subtract(DateTime.UtcNow).Duration().ToString()), TraceEventType.Verbose);
37: AuthKey = authCred2.SecurityTokenResponse;
38: }
39: else
40: {
41: logEntry.Log(string.Format("{0} - Authenticated via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtAuthQuery.Subtract(DateTime.UtcNow).Duration().ToString()), TraceEventType.Verbose);
42: AuthKey = authCred.SecurityTokenResponse;
43: }
44: DiscoveryServiceProxy proxy = new DiscoveryServiceProxy((IServiceManagement<IDiscoveryService>)servicecfg, AuthKey);
That allowed me to auth across claims realms, create the proxy I was interested.
Helper method to build the Discovery Service and Organization Service Proxies Supporting all current forms of CRM 2011 auth.
I have wrapped up the code I used to do this into a helper method supporting both the Organization and Discovery server proxy's is here, it will also appear in the update to the UII Solution Starters Agent desktop starter code once I release the update to that. ( soon ) .. as with all code .. use at your own risk..
/// <summary>
/// Creates and authenticates the Service Proxy for either the discovery server or organization service for CRM
/// </summary>
/// <typeparam name="T">Service Management Type</typeparam>
/// <param name="servicecfg">Pre-created Service Configuration object</param>
/// <param name="ServiceUri">URL to connect too</param>
/// <param name="homeRealm">HomeRealm URI</param>
/// <param name="userCredentials">User Credentials object</param>
/// <param name="deviceCredentials">Device Credentials object</param>
/// <param name="LogString">Log Preface string. </param>
/// <returns></returns>
private static object CreateAndAuthenticateProxy<T>(IServiceManagement<T> servicecfg,
Uri ServiceUri,
Uri homeRealm,
ClientCredentials userCredentials,
ClientCredentials deviceCredentials,
string LogString)
{
LoginTracer logEntry = new LoginTracer();
DateTime dtConnectTimeCheck = DateTime.UtcNow;
object OutObject = null;
if (servicecfg == null)
{
logEntry.Log(string.Format("{0} - attempting to connect to CRM server @ {1}", LogString, ServiceUri.ToString()), TraceEventType.Verbose);
// Create the Service configuration for that URL
servicecfg = ServiceConfigurationFactory.CreateManagement<T>(ServiceUri);
if (servicecfg == null)
return null;
logEntry.Log(string.Format("{0} - created CRM server proxy configuration for {1} - duration: {2}", LogString, ServiceUri.ToString(), dtConnectTimeCheck.Subtract(DateTime.UtcNow).Duration().ToString()), TraceEventType.Verbose);
}
else
{
logEntry.Log(string.Format("{0} - will use user provided {1} to connect to CRM ", LogString, typeof(T).ToString()), TraceEventType.Verbose);
}
// Auth
logEntry.Log(string.Format("{0} - proxy requiring authentication type : {1} ", LogString, servicecfg.AuthenticationType), TraceEventType.Verbose);
// Determine the type of authentication required.
if (servicecfg.AuthenticationType != AuthenticationProviderType.ActiveDirectory)
{
// Connect via anything other then AD.
// Setup for Auth Check Perf.
DateTime dtAuthQuery = DateTime.UtcNow;
AuthenticationCredentials authCred = new AuthenticationCredentials();
authCred.ClientCredentials = userCredentials;
if (servicecfg.AuthenticationType == AuthenticationProviderType.LiveId)
authCred.SupportingCredentials = new AuthenticationCredentials() { ClientCredentials = deviceCredentials };
// Claims
if (homeRealm != null)
authCred.HomeRealm = homeRealm;
// Deal with an incorrect configuration here.
// Home Realm should not be used if the Service Identifier and the homeRealm are identical.
if (homeRealm != null && homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier)
authCred.AppliesTo = new Uri(servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier);
// Run Authentication
// Failure will generate an exception.
authCred = servicecfg.Authenticate(authCred);
// If is Federation and HomeRealm is not null, and HomeRealm is Not the same as the SecureTokeServiceIdentifier
// Run Seconday auth to auth the Token to the right source.
SecurityTokenResponse AuthKey = null;
if (servicecfg.AuthenticationType == AuthenticationProviderType.Federation &&
homeRealm != null &&
!string.IsNullOrWhiteSpace(homeRealm.ToString()) &&
(homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier))
{
logEntry.Log(string.Format("{0} - Initial Authenticated via {1} {3} . Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtAuthQuery.Subtract(DateTime.UtcNow).Duration().ToString(), homeRealm.ToString()), TraceEventType.Verbose);
logEntry.Log(string.Format("{0} - Relaying Auth to Resource Server: From {1} to {2}", LogString, homeRealm.ToString(), servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier), TraceEventType.Verbose);
dtAuthQuery = DateTime.UtcNow;
// Auth token against the correct server.
AuthenticationCredentials authCred2 = servicecfg.Authenticate(new AuthenticationCredentials()
{
SecurityTokenResponse = authCred.SecurityTokenResponse
});
logEntry.Log(string.Format("{0} - Authenticated via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtAuthQuery.Subtract(DateTime.UtcNow).Duration().ToString()), TraceEventType.Verbose);
AuthKey = authCred2.SecurityTokenResponse;
}
else
{
logEntry.Log(string.Format("{0} - Authenticated via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtAuthQuery.Subtract(DateTime.UtcNow).Duration().ToString()), TraceEventType.Verbose);
AuthKey = authCred.SecurityTokenResponse;
}
if (typeof(T) == typeof(IDiscoveryService))
OutObject = new DiscoveryServiceProxy((IServiceManagement<IDiscoveryService>)servicecfg, AuthKey);
if (typeof(T) == typeof(IOrganizationService))
OutObject = new OrganizationServiceProxy((IServiceManagement<IOrganizationService>)servicecfg, AuthKey);
}
else
{
// Create the proxy here... we except out here if Auth Fails.
if (typeof(T) == typeof(IDiscoveryService))
OutObject = new DiscoveryServiceProxy((IServiceManagement<IDiscoveryService>)servicecfg, userCredentials);
if (typeof(T) == typeof(IOrganizationService))
OutObject = new OrganizationServiceProxy((IServiceManagement<IOrganizationService>)servicecfg, userCredentials);
}
logEntry.Log(string.Format("{0} - service proxy created - total create duration: {1}", LogString, dtConnectTimeCheck.Subtract(DateTime.UtcNow).Duration().ToString()), TraceEventType.Verbose);
return OutObject;
}
To use it to create a connection to a discovery server :
1: object objectRlst = CreateAndAuthenticateProxy<IDiscoveryService>(null, discoveryServiceUri, homeRealmUri, clientCredentials, deviceCredentials, "DiscoverOrganizations");
2: if (objectRlst != null && objectRlst is DiscoveryServiceProxy)
3: discoveryServiceProxy = (DiscoveryServiceProxy)objectRlst;
To use it to create a connection to Live’s Organization service, You call it as such:
1: object oProx = null;
2: oProx = CreateAndAuthenticateProxy<IOrganizationService>(null, oOrgSvc, null, userClientCred, DeviceCredentials, "ConnectAndInitCrmOrgService");
3: if (oProx != null && oProx is OrganizationServiceProxy)
4: prox = (OrganizationServiceProxy)oProx;
Hope this helps someone else along the way.
Comments
Anonymous
March 28, 2012
Matt great knowlege sharing - Thanks!Anonymous
March 01, 2013
Two issues. I'm not clear on how you distinguish between what authentication method that is to be used. Also, you use servicecfg but I seem to miss how you obtain it.Anonymous
March 08, 2013
In these lines of code: 1: object oProx = null; 2: oProx = CreateAndAuthenticateProxy<IOrganizationService>(null, oOrgSvc, null, userClientCred, DeviceCredentials, "ConnectAndInitCrmOrgService"); 3: if (oProx != null && oProx is OrganizationServiceProxy) 4: prox = (OrganizationServiceProxy)oProx; I pass in a null for the serviceconfig ( the first param ), As I didn’t pass in a value, the CreateandAuthenticateProxy method creates it based on the URI that is defiend by the oOrgSvc uri. The method will then recognize what auth to use based on the servicecfg Auth type. Later versions of this method will reorganize the userClientCred if necessary ( say you gave it a windows credential and it needed a user credential ) based on the auth type if necessary. Iv just not gotten back around to posting the update for it. Mattb.