Export (0) Print
Expand All
This topic has not yet been rated - Rate this topic

Server-to-Server Authentication using Certificates with Impersonation

banner art

[Applies to: Microsoft Dynamics CRM 4.0]

This authentication scenario is used when an ISV wants to provide a service, for example an online store front, to a customer and is required to make business data changes in Microsoft Dynamics CRM Online on the customer's behalf in order to provide that service. The ISV would typically implement a custom Web page that provides a user interface to the service and a custom Windows Live logon Web page. Optionally, the custom Web page could be embedded in an IFRAME within the Microsoft Dynamics CRM Online Web application.

Important    Use of certificates for Microsoft Dynamics CRM Online authentication is being phased out in the very near future in favor of a lightweight user name and password authentication method described in the Server-to-Server Authentication topic. All new code that authenticates with CRM Online should not use certificates for authentication. The Windows Live ID team will no longer be issuing new certificates.

The sequence of events that the user sees is as follows. If the user has not logged into Windows Live or Microsoft Dynamics CRM Online, the user is redirected to the ISV's custom logon page where the user can log on using their Windows Live credentials. After a successful logon, the user is redirected to the ISV's custom Web page where the service can be provided. Any Microsoft Dynamics CRM Online data changes that the custom Web page performs on the user's behalf is recorded in the database as being owned by the user. The ISV user who made the data change is also recorded.

The behind the scenes sequence of events for this scenario is shown in the following diagram and described in the text that follows the diagram.

Windows Live authentication

Important   The RPS (Relying Party Suite) SDK that is required to implement this authentication scenario is available only to those developers who are members of Microsoft's PartnerSource. For more information, see A HREF="http://go.microsoft.com/fwlink/?LinkID=189153&clcid;=0x409" TARGET="_blank"PartnerSource for Microsoft Dynamics.

Note    In order to use this new authentication model you must first obtain a certificate and then follow the documented procedure to download the Sign-in Assistant software from the Windows Live Web site and associate the certificate to your Windows Live ID. Step by step documentation is located in the file Walkthroughs\Authentication\CS\ServerToServer\Cert to WLID Association.doc in the SDK download.

The Microsoft Dynamics CRM Online authentication process for this scenario involves the following steps:

  • (1) Retrieve an RPS ticket that contains a PUID (Passport Unique ID) of the logon user. This step is only required if an RPS ticket does not exist in a cookie of the logon server.
  • (2) Retrieve a policy from the CrmDiscoveryService Web service using the RetrievePolicyRequest request.
  • (3) Retrieve a Windows Live ID (WLID) ticket for the ISV's service account from the Windows Live service using the WindowsLiveIdTicketAcquirer.RetrieveTicket method
  • (4) Retrieve detailed information about the specified organization from the CrmDiscoveryService Web service. Next, retrieve the logged-on user's Microsoft Dynamics CRM user ID using the RetrieveCrmUserIdByExternalIdRequest request. This request class is available in the WSDL obtained from the Microsoft Dynamics CRM Online Web service.
  • (5) Retrieve a (Crm) ticket from the CrmDiscoveryService Web service using the WLID ticket of the ISV's service account in the RetrieveCrmTicketRequest request. The ticket applies to a single organization and contains an organization-specific CrmService URL.
  • (6) Create an instance of the CrmAuthenticationToken class that has the CrmTicket and OrganizationName properties set to the correct values. Also set the CallerId property of the token to the logged-on user ID to impersonate the user.
  • (6) Create an instance of the CrmService class that has the Url property value and the CrmAuthenticationTokenValue property value set.
  • (6) Invoke CrmService Web service methods.

In order to perform step 3 shown earlier in this topic, the ISV must purchase a certificate from a certificate provider and set up a service account in Microsoft Dynamics CRM Online. Supported certificates are listed in the following table. The Microsoft Dynamics CRM team is investigating ways to offer more choices in certificate providers.

Supported Certificates
Entrust CA (2048)

Thumbprint: 80 1d 62 d0 7b 44 9d 5c 5c 03 5c 98 ea 61 fa 44 3c 2a 58 fe  / gB1i0HtEnVxcA1yY6mH6RDwqWP4

GoDaddy Root

Thumbprint:  27 96 ba e6 3f 18 01 e2 77 26 1b a0 d7 77 70 02 8f 20 ee e4  / J5a65j8YAeJ3Jhug13dwAo8g7uQ

NetworkSolution Root

Thumbprint:  04 83 ed 33 99 ac 36 08 05 87 22 ed bc 5e 46 00 e3 be f9 d7 / BIPtM5msNggFhyLtvF5GAOO++dc


The service account represents a virtual Microsoft Dynamics CRM Online (ISV) user who performs business data changes to the Microsoft Dynamics CRM Online database on the logged-on user's behalf through Microsoft Dynamics CRM SDK Web service calls. As with any other Microsoft Dynamics CRM user account, the service account must be added to each desired organization where business data is to be changed.

Example

The following code sample shows how to authenticate with Microsoft Dynamics CRM Online and call a CrmService method on behalf of the logged-on user through impersonation. The code is written to follow the previous sequence diagram. Notice that this code is incomplete in that the certificate validation, RPS initialization, and ASPX logon form code are missing. That sample code is provided later in this topic.

The complete code sample can be found in the  Server\FullSample\ServerToServerImpersonate folder of the SDK download package .

[C#]
public void InvokeServiceMethod(int retryCount)
{
    try
    {
        if (retryCount == MAX_RETRIES)
        {
            // The maximum retry count has been reached.
            throw new Exception("An error occurred while attempting to authenticate.");
        }
        else
        {
            // STEP 2: Retrieve a policy from the Discovery Web service.
            CrmDiscoveryService discoveryService = new CrmDiscoveryService();
            discoveryService.Url = String.Format(
                "https://{0}/MSCRMServices/2007/{1}/CrmDiscoveryService.asmx", _hostname, "Passport");

            RetrievePolicyRequest policyRequest = new RetrievePolicyRequest();
            RetrievePolicyResponse policyResponse =
               (RetrievePolicyResponse)discoveryService.Execute(policyRequest);

            // STEP 3: Retrieve a ticket from the Windows Live ID service.
            // using the specified service account's WLID.
            string wlidTicket = null;
            wlidTicket = WindowsLiveIdTicketAcquirer.RetrieveTicket(RetrieveCertificate(_certSubjectCN), 
                _serviceAcctWLID, _environment, _passportDomain, policyResponse.Policy);

            // STEP 4: Obtain the logged-on user's Microsoft Dynamics CRM ID.
            // Retrieve a list of organizations that the logged on user is a member of.
            RetrieveOrganizationsRequest orgRequest = new RetrieveOrganizationsRequest();
            orgRequest.PassportTicket = wlidTicket;
            RetrieveOrganizationsResponse orgResponse =
                (RetrieveOrganizationsResponse)discoveryService.Execute(orgRequest);
            // Locate the target organization name using the organization friendly name.
            String orgUniqueName = String.Empty;
            OrganizationDetail orgInfo = null;
            foreach (OrganizationDetail orgDetail in orgResponse.OrganizationDetails)
            {
                if (orgDetail.FriendlyName.Equals(_orgFriendlyName))
                {
                    orgInfo = orgDetail;
                    orgUniqueName = orgInfo.OrganizationName;
                    break;
                }
            }
            RetrieveCrmUserIdByExternalIdRequest ruidReq = new RetrieveCrmUserIdByExternalIdRequest();
            ruidReq.ExternalId = _strPUID;
            ruidReq.PassportTicket = wlidTicket;
            ruidReq.OrganizationName = orgUniqueName;
            RetrieveCrmUserIdByExternalIdResponse ruidResp = 
                (RetrieveCrmUserIdByExternalIdResponse)discoveryService.Execute(ruidReq);

            // STEP 5: Retrieve a ticket from the Discovery Web service.
            RetrieveCrmTicketRequest crmTicketRequest = new RetrieveCrmTicketRequest();
            crmTicketRequest.OrganizationName = orgUniqueName;
            crmTicketRequest.PassportTicket = wlidTicket;
            RetrieveCrmTicketResponse crmTicketResponse =
               (RetrieveCrmTicketResponse)discoveryService.Execute(crmTicketRequest);

            // STEP 6: Create and configure an instance of the CrmService Web service.
            CrmAuthenticationToken token = new CrmAuthenticationToken();
            token.AuthenticationType = AuthenticationType.Passport;
            token.CrmTicket = crmTicketResponse.CrmTicket;
            token.OrganizationName = crmTicketResponse.OrganizationDetail.OrganizationName;
            // Set the CallerId with the Microsoft Dynamics CRM ID from Step 4 for impersonation.
            token.CallerId = ruidResp.CrmUserId;

            CrmService crmService = new CrmService();
            crmService.Url = crmTicketResponse.OrganizationDetail.CrmServiceUrl;
            crmService.CrmAuthenticationTokenValue = token;

            // Invoke the desired CrmService Web service methods.
            WhoAmIRequest whoRequest = new WhoAmIRequest();
            WhoAmIResponse whoResponse = (WhoAmIResponse)crmService.Execute(whoRequest);

            // Set the columns for retrieving the required data of the systemuser.
            ColumnSet cols = new ColumnSet();
            cols.Attributes = new string[] { "fullname" };
            
            // Retrieve the impersonating user (that is, the Service Account user) information using the 
            // WhoAmI service message. The Service Account user impersonates the logged-in user. 
            systemuser impersonationUser = (systemuser)crmService.Retrieve(EntityName.systemuser.ToString(),
                whoResponse.UserId, cols);

            // Retrieve the impersonated user (that is, the logged-on user) information using the Microsoft Dynamics CRM ID
            // from Step 4.
            systemuser impersonatedUser = (systemuser)crmService.Retrieve(EntityName.systemuser.ToString(),
                ruidResp.CrmUserId, cols);

            ImpersonationMessage.Text = 
                "<br />Impersonation Message: <br /><i>Authentication was successful.</i>";
            ImpersonationMessage.Text += 
                "<br /><i>Service Account user '" + impersonationUser.fullname.ToString();
            ImpersonationMessage.Text += 
                "' impersonates logged-in user '" + impersonatedUser.fullname.ToString() + "'.</i>";
        }
    }
    catch (SoapException ex)
    {
        // Handle the exception thrown from an expired ticket condition.
        if (GetErrorCode(ex.Detail) == EXPIRED_AUTH_TICKET)
        {
            // This will retry the CrmService Web service call.
            InvokeServiceMethod(retryCount++);
        }

        // If this was some other SOAP exception, rethrow this exception.
        throw ex;
    }
    catch (Exception ex)
    {
        // Handle the MAX_RETRY exception here.
        // This sample will just rethrow the exception.
        throw ex;
    }
}
 
private static string GetErrorCode(XmlNode errorInfo)
{
    XmlNode code = errorInfo.SelectSingleNode("//code");

    if (code != null)
        return code.InnerText;
    else
        return "";
}

protected static X509Certificate2 RetrieveCertificate(string subjectCN)
{
    X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

    store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

    // It is highly recommended to set the third parameter of the Find method to true for valid certificates.
    X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindBySubjectName, subjectCN, true);

    if (null != store)
    {
        store.Close();
    }

    if (null == certs || certs.Count < 1)
    {
        throw new Exception("Cannot find specified certificate.");
    }

    ValidateCert(certs[0]);

    // Return the first match.
    return certs[0];
}

static void ValidateCert(X509Certificate2 cert)
{
    X509Chain chain = new X509Chain();

    // Check the entire chain for revocation.
    chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;

    // Check online or offline revocation lists.
    chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;

    // Set the time-out for the online revocation list.
    chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 30);

    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

    // Set or modify the time of verification.
    //chain.ChainPolicy.VerificationTime = new DateTime(1999, 1, 1);

    chain.Build(cert);

    if (chain.ChainStatus.Length != 0)
    {
        // Throw an exception here for an invalid certificate.  
    }            
}

The CrmDiscoveryService Web service is accessed through the global URL of the Microsoft Dynamics CRM Online server:

https://dev.crm.dynamics.com/MSCRMServices/2007/Passport/CrmDiscoveryService.asmx

If the (Crm) ticket expires during application execution, a new ticket must be obtained and assigned to the CrmTicket property of the CrmAuthenticationToken instance. If you try to access the CrmService Web methods with an expired ticket, a SOAP exception is thrown. The SoapException.Detail.Innertext property contains the error code value of "8004A101".

Notice that, in real-world scenarios, you would never authenticate and then immediately check for an expired ticket as this sample shows. Instead, you would authenticate and make additional Web service method calls. Part of your software design would be to catch SOAP exceptions from Microsoft Dynamics CRM Web service calls and check for an expired authentication ticket.

To access the Windows Live authentication service over the Internet and obtain a Windows Live ID ticket, the code sample uses the WindowsLiveIdTicketAcquirer class that is provided as source code in the Server\Helpers\CS\CrmOnlineAuth folder of the SDK download.

Obtaining the user's PUID from RPS

The RPS Ticket Request path in the diagram handles the following possible conditions:

  • An RPS ticket is stored in a cookie on the logon server and is not valid.
  • An RPS ticket does not exist.

If a valid RPS ticket is stored in a cookie on the logon server, the RPS Ticket Request step is not required.

All the previous scenarios are handled by the following sample code. If an RPS ticket must be obtained, the user is redirected to a sign-in Web form that can be customized by the ISV. Refer to the RPS SDK documentation for more information.

[C#]
// STEP 1: Retrieve an RPS ticket that contains the logged-on user's PUID.
public void InitializeRPS()
{
    // Get the RPS object pre-initialized in the Global.asax file.
    RPS myRps = (RPS)Application["globalRPS"];
    string siteName = "default";

    // Create other base RPS objects.
    RPSHttpAuth httpAuth = new RPSHttpAuth(myRps);
    RPSPropBag authPropBag = new RPSPropBag(myRps);
    RPSDomainMap domainMap = new RPSDomainMap(myRps);
    
    RPSServerConfig mainConfig = new RPSServerConfig(myRps);
    RPSPropBag siteConfig = (RPSPropBag)mainConfig["Sites"];

    RPSPropBag mySiteConfig = (RPSPropBag)siteConfig[siteName];

    int siteID = Convert.ToInt32(mySiteConfig["SiteID"]);

    // Set the returnUrl and siteID in authPropBag.
    // Use SSL and remove the query string parameters from returnUrl.
    string returnUrl = "http://" + Request.ServerVariables["SERVER_NAME"] + Request.Path;
    authPropBag["ReturnURL"] = returnUrl;
    authPropBag["SiteID"] = siteID;

    // Create a ticket object and populate authPropBag with values from the Request object.
    RPSTicket ticket = httpAuth.Authenticate(siteName, Request, authPropBag);

    // Obtain the users 'authState' to determine if they are currently signed in.
    uint authState = (uint)authPropBag["RPSAuthState"];

    if (ticket == null)
    {
        // No RPSTicket found.  Check for AuthState=2 (Maybe) state.
        if (authState == 2)
        {
            // RPS Maybe state detected.  Indicates a ticket is present, but cannot be read.
            // Redirect to SilentAuth URL to obtain a fresh RPS Ticket.
            // Write RPS response headers to write the Maybe state cookie to prevent looping.
            string rpsHeaders = (string)authPropBag["RPSRespHeaders"];
            if (rpsHeaders != "")
            {
                httpAuth.WriteHeaders(Response, rpsHeaders);
            }

            string SilentAuthUrl = domainMap.ConstructURL("SilentAuth", siteName, null, authPropBag);
            Response.Redirect(SilentAuthUrl);
        }
        else
        {
            // The user is not signed in.  Show the Web page with the Sign in option.

            Message.Text = "A ticket was not detected.  Click the Sign in button below to sign in.";
            ShowTicketProperties.Text = "";
            bool showSignIn = true;
            bool isSecure = Request.IsSecureConnection;

            LogoButton.Text = httpAuth.LogoTag(showSignIn, isSecure, null, null, siteName, authPropBag);
        }
        _strPUID = "";
    }
    else
    {
        // An RPS ticket was found. Ensure that the ticket is valid.
        // The signature is valid and the policy criteria, such as the time window and use of SSL, is met.

        bool isValid = ticket.Validate(authPropBag);

        if (!isValid)
        {
            // The RPS ticket exists, but is not valid. Refresh the ticket by redirecting the user to 
            // the Auth URL.If appropriate login server cookies exist, a ticket refresh will be 
            // transparent to the user.
            Response.Redirect(domainMap.ConstructURL("Auth", siteName, null, authPropBag));
            _strPUID = "";
        }
        else
        {
            // The RPS ticket exists and is valid. Show the Web page with the Sign out button and the
            // user's NetID.
            bool showSignIn = false;
            bool isSecure = Request.IsSecureConnection;
            LogoButton.Text = httpAuth.LogoTag(showSignIn, isSecure, null, null, siteName, authPropBag);

            Message.Text = "You have a current ticket.  You are signed in.";

            ShowTicketProperties.Text += "<table width=90%>";
            ShowTicketProperties.Text += "<tr><td>Your NetID (PUID)</td><td>";
            ShowTicketProperties.Text += (string)ticket.Property["HexPUID"] + "</td></tr>";
            ShowTicketProperties.Text += "<tr><td>TicketType</td><td>" + ticket.TicketType;
            ShowTicketProperties.Text += 
                "&nbsp;&nbsp;<i>[1=WebSSO, 2=Compact, 3=AuthCookie, 4=SecAuthCookie]</i>" + "</td></tr>";
            ShowTicketProperties.Text += 
                "<tr><td>Ticket IssueInstant</td><td>" + ticket.Property["IssueInstant"];
            ShowTicketProperties.Text += 
                "&nbsp;&nbsp;<i>[IssueInstant = seconds since 1970]</i>" + "</td></tr>";
            ShowTicketProperties.Text += "</table>";

            // Get the RPS response headers from authPropBag and write to the response stream.
            string rpsHeaders = (string)authPropBag["RPSRespHeaders"];
            if (rpsHeaders != "")
            {
                httpAuth.WriteHeaders(Response, rpsHeaders);
            }

            _strPUID = (string)ticket.Property["HexPUID"];
        }
    }
}

In this code, the _strPUID variable is a private class member variable.

See Also

Concepts

Reference

Other Resources


© 2010 Microsoft Corporation. All rights reserved.


Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.