Walkthrough: Multi-tenant server-to-server authentication

Applies to Dynamics 365 for Customer Engagement apps version 9.x

This walkthrough will describe the steps to create a multi-tenant web application that can connect to a December 2016 Update for Dynamics 365 (online) tenant using the Visual Studio 2015 MVC web application template.

Requirements

  • Visual Studio 2015 with web developer tools installed

  • A December 2016 Update for Dynamics 365 (online) tenant associated with your Azure Active Directory (Azure AD) tenant.

  • A second December 2016 Update for Dynamics 365 (online) tenant associated with a different Azure AD tenant. This tenant represents a subscriber to your application. This can be a trial December 2016 Update for Dynamics 365 (online) subscription.

Goal of this walkthrough

When you complete this walkthrough you will have an MVC web application which will use the WhoAmIRequest Class to retrieve data about the user the application uses to connect to the Dynamics 365 for Customer Engagementapps tenant.

When you run the app successfully you will see a Sign in command in the top right corner.

The sign in command in the app

Click the Sign in command and you will be directed to Azure AD for your credentials.

After you sign in, you will see there is a WhoAmI command.

The WhoAmI command

Click WhoAmI, and you should see the following:

Results of a WhoAmI request

When you query your Customer Engagement tenant you will see that the results returned from the WhoAmI message refer to a specific application user account you have configured for the web application to use rather than the user account you are currently using.

Verify Azure AD tenant

Before you begin, connect to your Office 365 Admin Centerhttps://portal.office.com and in the Admin centers drop-down, verify that you see both Customer Engagement and Azure AD.

Admin Centers with Azure Active Directory and Dynamics 365 for Customer Engagement

If your Azure AD subscription is not associated with a Customer Engagement subscription, you will not be able to grant privileges for your application to access Customer Engagement data.

If you do not see this option, see Register your free Azure Active Directory subscription for information about how to register to get your Azure AD subscription.

If you already have an Azure subscription but it isn’t associated with your Office 365 account, see Associate your Office 365 account with Azure AD to create and manage apps.

Create an MVC web application

Using Visual Studio 2015, you can create a new MVC web application and register it with your Azure AD tenant.

  1. Open Visual Studio 2015.

  2. Make sure that the Microsoft account you are signed in as is the same one with access to the Azure AD tenant you want to use to register your application.

  3. Click New Project and select .NET Framework 4.6.1 and the ASP.NET Web Application template.

    Click OK, and in the New ASP.NET project dialog select MVC.

  4. Click the Change Authentication button, and in the dialog select Work And School Accounts.

  5. In the drop-down, select Cloud – Multiple Organizations.

    ASP.NET  MVC Change Authentication Dialog

  6. Click OK and complete initializing the project.

    Note

    Creating a Visual Studio project this way will register the application with your Azure AD tenant and add the following keys to the Web.Config appSettings:

    <add key="ida:ClientId" value="baee6b74-3c39-4c04-bfa5-4414f3dd1c26" />  
    <add key="ida:AADInstance" value="https://login.microsoftonline.com/" />  
    <add key="ida:ClientSecret" value="HyPjzuRCbIl/7VUJ2+vG/+Gia6t1+5y4dvtKAcyztL4=" />  
    
    

Register your application on Azure AD

If you have followed the steps in Create an MVC web application, you should find that the web application project you created in Visual Studio is already registered in your Azure AD applications. But there is one more step that you must perform within the Azure AD portal.

  1. Go to https://portal.azure.com and select Azure Active Directory.

  2. Click App registrations and look for the application you created using Visual Studio. In the General area, verify the properties:

    Application registration data in Azure Active Directory

  3. Verify that the Application ID property matches the ClientId value added in your Web.Config appSettings.

  4. The Home page URL value should match SSL URL property in your Visual Studio project and should direct to a localhost URL, i.e. https://localhost:44392/.

    Note

    You will need to change this later when you actually publish your application. But you need to have this set to the correct localhost value for debugging.

  5. In the API Access area, confirm that a Key value has been added. The Key value is not visible in the Azure portal after the application has been created, but this value was added to your Web.Config appSettings as the ClientSecret.

Note

When registering this application you do not need to grant your application rights to access Dynamics 365 for Customer Engagement (online) data as you usually do when creating a client application. This application is bound to a application user in the system.

Create an application user

Using steps in Manually create a Dynamics 365 for Customer Engagement application user, create an application user with the Application Id value from your application registration which is also the same as the ClientId value in the Web.Config.

Add Assemblies

Add the following NuGet packages to your project

Package Version
Microsoft.CrmSdk.CoreAssemblies Latest version
Microsoft.IdentityModel.Clients.ActiveDirectory 2.22.302111727
Microsoft.IdentityModel.Tokens 5.0.0
Microsoft.Azure.ActiveDirectory.GraphClient 2.1.0

Note

Do not update the Microsoft.IdentityModel.Clients.ActiveDirectory assemblies to the latest version. Version 3.x of these assemblies changed an interface that the Microsoft.CrmSdk.CoreAssemblies depends on.

For information about managing NuGet packages, see NuGet Documentation: Managing NuGet Packages Using the UI

Apply code changes to the MVC template

The following code changes will provide basic functionality to use the Customer EngagementWhoAmI message and verify that the application user account identity is being used by the application.

Web.config

Add the following keys to the appSettings.

<add key="ida:OrganizationHostName" value="https://{0}.crm.dynamics.com" />   

The ida:OrganizationHostName string will have the subscriber’s Customer Engagement online organization name added at the placeholder so that the correct service will be accessed.

<add key="owin:appStartup" value="<your app namespace>.Startup" />  

The owin:appStartup string ensures that the OWIN middleware uses the Startup class in this project. Otherwise you will get the following error:

- No assembly found containing an OwinStartupAttribute.  
- No assembly found containing a Startup or [AssemblyName].Startup class.  

More information: ASP.NET: OWIN Startup Class Detection

Controllers/HomeController.cs

Add the AllowAnonymous decorator to the Index action. This allows access to the default page without authentication.

using System.Web.Mvc;  
  
namespace SampleApp.Controllers  
{  
    [Authorize]  
    public class HomeController : Controller  
    {  
        [AllowAnonymous]  
        public ActionResult Index()  
        {  
            return View();  
        }  
  
        public ActionResult About()  
        {  
            ViewBag.Message = "Your application description page.";  
  
            return View();  
        }  
  
        public ActionResult Contact()  
        {  
            ViewBag.Message = "Your contact page.";  
  
            return View();  
        }  
    }  
}  
  

Note

In your web application or service, it is not expected that you will allow anonymous access. Anonymous access is used here for simplicity. Controlling access to your application is out of scope for this walkthrough.

Views/Shared/_Layout.cshtml

In order to display the command link WhoAmI for authenticated users, you need to edit this file.

Locate the div element with the class navbar-collapse collapse and edit it to include the code below:

<div class="navbar-collapse collapse">  
    <ul class="nav navbar-nav">  
     <li>@Html.ActionLink("Home", "Index", "Home")</li>  
     <li>@Html.ActionLink("About", "About", "Home")</li>  
     <li>@Html.ActionLink("Contact", "Contact", "Home")</li>  
     @if (Request.IsAuthenticated)  
     {  
         <li>@Html.ActionLink("WhoAmI", "Index", "CrmSdk")</li>  
     }  
    </ul>  
  
    @Html.Partial("_LoginPartial")  
   </div>  
  

App_Start/Startup.Auth.cs

The following changes will invoke the consent framework when a new tenant logs into the application:

public partial class Startup  
 {  
  private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];  
  private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];  
  //Not used     
  //private string graphResourceID = "https://graph.windows.net";      
  private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];  
  private string authority = aadInstance + "common";  
  private ApplicationDbContext db = new ApplicationDbContext();  
  
  //Added  
  private string OrganizationHostName = ConfigurationManager.AppSettings["ida:OrganizationHostName"];  
  
  public void ConfigureAuth(IAppBuilder app)  
  {  
  
   app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);  
  
   app.UseCookieAuthentication(new CookieAuthenticationOptions { });  
  
   app.UseOpenIdConnectAuthentication(  
       new OpenIdConnectAuthenticationOptions  
       {  
        ClientId = clientId,  
        Authority = authority,  
        TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters  
        {  
         /*  
         instead of using the default validation   
         (validating against a single issuer value, as we do in line of business apps),   
         we inject our own multitenant validation logic  
         */  
         ValidateIssuer = false,  
        },  
        Notifications = new OpenIdConnectAuthenticationNotifications()  
        {  
         SecurityTokenValidated = (context) =>  
                  {  
                   return Task.FromResult(0);  
                  },  
         AuthorizationCodeReceived = (context) =>  
                  {  
                   var code = context.Code;  
  
                   ClientCredential credential = new ClientCredential(clientId, appKey);  
                   string tenantID = context  
                    .AuthenticationTicket  
                    .Identity  
                    .FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")  
                    .Value;  
  
                   /* Not used  
                  string signedInUserID = context  
                     .AuthenticationTicket  
                     .Identity  
                     .FindFirst(ClaimTypes.NameIdentifier)  
                     .Value;    
                     */  
  
                   //Added  
                   var resource = string.Format(OrganizationHostName, '*');  
                   //Added  
                   Uri returnUri = new Uri(  
                    HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)  
                    );  
  
                   /* Changed below  
                    AuthenticationContext authContext =   
                    new AuthenticationContext(  
                     aadInstance + tenantID,   
                     new ADALTokenCache(signedInUserID)  
                     );  
                    */  
                   //Changed version  
                   AuthenticationContext authContext =  
                   new AuthenticationContext(aadInstance + tenantID);  
  
                   /* Changed below  
                   AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(  
                       code,   
                       new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),   
                       credential,   
                       graphResourceID);  
                   */  
                   //Changed version  
                   AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(  
                       code,  
                       new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),  
                       credential,  
                       resource);  
  
                   return Task.FromResult(0);  
                  },  
         AuthenticationFailed = (context) =>  
                  {  
                   context.OwinContext.Response.Redirect("/Home/Error");  
                   context.HandleResponse(); // Suppress the exception  
                   return Task.FromResult(0);  
                  }  
        }  
       });  
  
  }  
 }  
  

Add Controllers/CrmSdkController

Add the following CrmSdkController.cs to the Controllers folder. This code will execute the WhoAmI message

  1. Right click the Controllers folder and select Add > Controller…

  2. In the Add Scaffold dialog, select MVC5 Controller – Empty

  3. Click Add

  4. Paste the following code substituting <Your app namespace> with the namespace of your app.

using Microsoft.IdentityModel.Clients.ActiveDirectory;   
using Microsoft.Xrm.Sdk;   
using Microsoft.Xrm.Sdk.WebServiceClient;   
using System; using System.Configuration;   
using System.Linq;   
using System.Security.Claims;   
using System.Web.Mvc;  
  
namespace <Your app namespace>  
{  
 [Authorize]  
 public class CrmSdkController : Controller  
    {  
  
  private string clientId =   
   ConfigurationManager.AppSettings["ida:ClientId"];  
  private string authority =   
   ConfigurationManager.AppSettings["ida:AADInstance"] + "common";  
  private string aadInstance =   
   ConfigurationManager.AppSettings["ida:AADInstance"];  
  private string OrganizationHostName =   
   ConfigurationManager.AppSettings["ida:OrganizationHostName"];  
  private string appKey =   
   ConfigurationManager.AppSettings["ida:ClientSecret"];  
  
  // GET: CrmSdk  
  public ActionResult Index()  
  {  
   string tenantID = ClaimsPrincipal  
    .Current  
    .FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")  
    .Value;  
   // Clean organization name from user logged  
   string organizationName = User.Identity.Name.Substring(  
    User.Identity.Name.IndexOf('@') + 1,   
    User.Identity.Name.IndexOf('.') - (User.Identity.Name.IndexOf('@') + 1)  
    );  
   //string crmResourceId = "https://[orgname].crm.microsoftonline.com";  
   var resource = string.Format(OrganizationHostName, organizationName);  
   // Request a token using application credentials  
   ClientCredential clientcred = new ClientCredential(clientId, appKey);  
   AuthenticationContext authenticationContext =   
    new AuthenticationContext(aadInstance + tenantID);  
   AuthenticationResult authenticationResult =   
    authenticationContext.AcquireToken(resource, clientcred);  
   var requestedToken = authenticationResult.AccessToken;  
   // Invoke SDK using using the requested token  
   using (var sdkService =  
    new OrganizationWebProxyClient(  
     GetServiceUrl(organizationName), false)  
     )  
   {  
    sdkService.HeaderToken = requestedToken;  
    OrganizationRequest request = new OrganizationRequest() {  
     RequestName = "WhoAmI"  
    };  
    OrganizationResponse response = sdkService.Execute(request);  
    return View((object)string.Join(",", response.Results.ToList()));  
   }  
  }  
  
  private Uri GetServiceUrl(string organizationName)  
  {  
   var organizationUrl = new Uri(  
    string.Format(OrganizationHostName, organizationName)  
    );  
   return new Uri(  
    organizationUrl +   
    @"/xrmservices/2011/organization.svc/web?SdkClientVersion=8.2"  
);  
  }  
 }  
}  
  

Views/CrmSdk

Add a new view named Index.

  1. Right click the CrmSdk folder and select Add > View…

  2. In the Add View dialog, set the following values:

    MVC Add View Dialog

  3. Click Add

  4. Replace the generated code with the following:

    @model string  
    @{  
     ViewBag.Title = "SDK Connect";  
    }  
    
    <h2>@ViewBag.Title.</h2>  
    
    <p>Connected and executed sdk command WhoAmI.</p>  
    
    <p>Value: @Model</p>  
    
    

Debug the app

When you press F5 to debug the application you may get error that the certificate accessing localhost using SSL is not trusted. The following are some links to resolve this issue with Visual Studioand IIS Express:

Note

For this step, you can simply use the Microsoft account associated with your Azure AD tenant and the Customer Engagement tenant that it is associated with. This isn’t actually demonstrating a multi-tenant scenario. We will do that in the next step. This step is just to verify that the code works before introducing the additional complexity of testing the actual multi-tenant functionality.

Refer to the steps described in Goal of this walkthrough to test the application.

At this point you can verify that the application user account was used. An easy way to check this is by using the Customer Engagement Web API. Type in the following URL into a separate tab or window, substituting the UserId value from the application.

[Organization URI]/api/data/v9.0/systemusers(<UserId value>)?$select=fullname  

The JSON response should look like the following. Notice that the fullname value will be to the application user you created in the Create an application user step, rather than the Customer Engagement user you used to sign into the application.

{  
    "@odata.context": "[Organization Uri]/api/data/v9.0/$metadata#systemusers(fullname)/$entity",  
    "@odata.etag": "W/\"603849\"",  
    "fullname": "S2S User",  
    "systemuserid": "31914b34-be8d-e611-80d8-00155d892ddc",  
    "ownerid": "31914b34-be8d-e611-80d8-00155d892ddc"  
}  
  

Configure test subscriber

Now that you have verified that the application works, it time to test connectivity to a different Customer Engagement tenant. Using a different Customer Engagement organization you will need to perform the following steps.

To give consent, perform the following steps while logged in as the Azure AD admin:

  1. While you are debugging your application, open a separate InPrivate or incognito window.

  2. In the address field of the window type the URL for your app, i.e. https://localhost:44392/

  3. Click the Sign in button and you will be prompted to grant consent.

    Azure Active Directory consent form

    After you grant consent you will return to the app, but you won’t be able to use it yet. If you click WhoAmI at this point you can expect the following exception:

System.ServiceModel.Security.MessageSecurityException  
HResult=-2146233087  
  Message=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/4baaeaaf-2771-4583-99eb-7c7e39aa1e74/oauth2/authorize, resource_id=https://<org name>.crm.dynamics.com/'.  
InnerException.Message =The remote server returned an error: (401) Unauthorized.  

By granting consent, the application from your Azure AD tenant will be added to the applications in the subscriber’s active directory tenant.

Create a custom security role in the subscriber tenant

The application user you will need to create must be associated with a custom security role which defines their privileges. For this manual testing step, you should first manually create a custom security role. More information: Create or edit a security role

Note

The application user cannot be associated with one of the default Dynamics 365 (online), version 8.2 security roles. You must create a custom security role to associate with the application user.

Create the subscriber application user

For the purposes of this walkthrough, we will manually create the application user to verify connectivity from a different tenant. When you deploy to actual subscribers, you will want to automate this. More information: Prepare a method to deploy the application user

You create the application user manually using the same values you used for your development organization in Create an application user. The exception is that you must have completed the step to grant consent first. When you save the user, the Application ID URI and Azure AD Object ID values will be set. You will not be able to save the user if you haven’t granted consent first.

Finally, associate the application user with the custom security role you added in the previous step.

Test the subscriber connection

Repeat the steps in Debug the app except use the credentials for a user from the other Customer Engagement tenant.

See also

Use Multi-Tenant Server-to-server authentication
Use Single-Tenant Server-to-server authentication
Build web applications using Server-to-Server (S2S) authentication
Connect to Dynamics 365 for Customer Engagement