Developing Multi-Tenant Web Applications with Windows Azure AD
This document will explain how to use Windows Azure Active Directory to add to one .NET application’s single sign-on and directory access capabilities across multiple organizations.
Overview
In the first paper of the Windows Azure AD for developers series, you learned how to take advantage of your Windows Azure AD tenant for enabling your users to enjoy web sign-on with your line of business (LoB) applications, on-premises and in the cloud. Read it here: Adding Sign-On to Your Web Application Using Windows Azure AD
The second paper built on the first, showing you how to enable your LoB app to query directory info through the Graph API. Read it here: Using the Graph API to Query Windows Azure AD
The paper you are reading now will bring your knowledge of Windows Azure AD to the next level, by demonstrating how you can develop software-as-a-service (SaaS) applications designed to work with multiple tenants and automate new customers’ onboarding, all the while taking advantage of the Windows Azure AD features you learned about in the first two installments.
When it comes to identity and access, the common challenges a SaaS app developer must face are mostly about customers’ onboarding and access to the customer’s identity infrastructure. Every potential customer has a different web sign-on solution, making very difficult to standardize an onboarding process that is simple for the customer and easy for the app to manage; and every potential customer maintains identity and directory data in infrastructure that is inaccessible from cloud applications.
Windows Azure AD offers a simple solution to both challenges. In the first paper you learned how your administrator can use the Windows Azure Management Portal to register the applications you develop to have access to your directory tenant. In this walkthrough you will learn that the same applications can be configured to be also accessible by other Windows Azure AD tenants. Windows Azure AD offers a mechanism through which application themselves can ask to the admins of potential customers to grant access to their directory tenants. That is achieved by using the Windows Azure AD Management Portal to present the customer’s admin with a consent UI: the experience is not very different from the consent granting gestures that are now commonplace for all the most common social web applications.
This document will show you how to modify the entry in Windows Azure AD of the MVC 4 app you developed in the first two papers to make the app available to multiple Windows Azure AD tenants. Furthermore, it will walk you through the code changes you need to apply to the app for moving from a solution meant to connect to a single tenant, to a full multi-tenant solution which can onboard multiple customer’s organizations. The walkthrough instructions will be interleaved with explanations of higher level concepts that are necessary for you to understand how Windows Azure AD works beyond the narrow scope of the sample application described here.
To reiterate: the tasks here described are all incremental in respect to the solution developed in the first two papers: you should not start reading this paper until you read the first two and followed the instructions there.
Outline of Sections
This document is organized into the following sections:
- Prerequisites: This section lists all the requirements that must be met for you to complete the walkthrough.
- Solution Architecture: This section provides a high level introduction on one way in which a SaaS application can be structured to take advantage of Windows Azure AD.
- Promoting the Application Entry in Windows AD to be Externally Available: This section will revisit the LoB application’s entry in the Windows Azure portal, and will show you what to change to extend the availability of the existing app to directory tenants other than your own.
- Prepare the Application Project in VS to Handle Multiple Tenants: This section will show you how to modify the source code of your MVC 4 app, currently meant to perform Web sign on and Graph access with a single directory tenant, to handle a dynamic set of Windows Azure AD tenants. You will work through several sub-sections, which will tackle in turn different aspects of the app’s identity management that need to be generalized to the multitenant case.
- Add Sign-Up Capabilities to the Application: This section will show you how to modify the source code of your MVC 4 app, currently meant to perform Web sign on and Graph access with a single directory tenant, to handle a dynamic set of Windows Azure AD tenants. You will work through several sub-sections, which will tackle in turn different aspects of the app’s identity management that need to be generalized to the multitenant case.
- Optional: Create a Test Subscription: The Windows Azure subscription created in the first tutorial contains only one directory, your own: however, in order to test how your app operates through a multitenant workloads you need access to a Windows Azure AD tenant other than your own. This section will suggest a strategy to obtain one.
- Test the Application: This section will show how all the tasks performed in the walkthrough come together to create a coherent end-to-end experience for your SaaS application.
Prerequisites
This following prerequisites are required to complete this tutorial:
- An Internet connection
- An active Windows Azure subscription: You can get a 90 day free trial here: Windows Azure Free Trial
- Visual Studio 2012 Professional or Visual Studio 2012 Ultimate: You can download a free trial here: Visual Studio Free Trial
- Identity and Access Tools for Visual Studio 2012
- The sample application created by following the single sign-on and Graph API walkthroughs, which are located here:
Solution Architecture
Traditional line of business applications are geared to be accessed and maintained by users coming from the same organization that developed and deployed the app in the first place. Business applications offered as Software as a Service (SaaS), conversely, are developed and operated by an independent software vendor and meant to be consumed by third party organizations. It is common for those applications to be built as multi-tenant resources: an application shared by multiple customer organizations (tenants), where every tenant experiences the app as if it would be its only customer.
As mentioned in the overview, Windows Azure AD offers you a path through which you can take an existing LoB application and make it available for other Windows Azure AD tenant administrators (your prospect customers) to use in their organization. All the prospect customer’s admin has to do is navigating to a special URL and signing in their Windows Azure AD tenant: he or she will land on a page describing the app, its publisher and the access level it requires on your directory (SSO, SSO and read-only access, SSO and read-write). The customer’s admin can use the controls on the page to grant consent to the application to access his or her Windows Azure directory, in which case the Management Portal will automatically register the app in the customer’s Windows Azure AD tenant without requiring any further work.
The MVC 4 application created in the LoB walkthrough contains direct references to the Windows Azure AD tenant it was configured to integrate with: namely, you configured the app to redirect every unauthenticated request to the tenant-specific sign-on address, and to accept only tokens coming from that specific tenant. Most of the work you’ll do in this walkthrough consists of generalizing the app’s identity management logic, so that the app will be ready to handle web sign-on and Graph API calls with any of the registered customer tenants. Another important functional area will be adding the ability to direct prospect customers to the consent page described above, and process the results to dynamically grow the list of the Windows Azure AD tenants that are accepted as valid authorities. All this will be accomplished by leveraging the WIF’s extensibility model in few strategic areas.
The central component of the modified app’s architecture is the MultiTenantIssuerNameRegistry (MTINR from now own) and its tenants’ persistent store.
The MTINR maintains a list of all the Windows Azure AD tenants that should be considered valid authentication sources. The list is:
- Consulted at sign-on time. Only tokens issued by registered tenants are considered valid
- Updated at sign up time. When a prospective customer grants consent to the app for accessing its directory, the Management Portal redirects the user back to your app with the associated tenant ID. The walkthrough will show you how to programmatically process that message to add the new customer to the MTINR list.
MTINR also maintains the key that should be used for checking signatures on tokens coming from Windows Azure AD; this document will show you how to modify the automated key refresh logic to ensure that your app tracks any key roll events on the service and minimize downtime.
The addition of a sign-up step means that your app must now be able to serve UI also to unauthenticated users; you will learn how to change the WIF settings to apply the web sign-on requirements selectively on your controllers, as opposed to the blanket protection policy demonstrated in the LoB applications walkthrough.
Promoting the Application Entry in Windows AD to be Externally Available
As very first step, you are going to let Windows Azure AD know that you want to make your application available to other Windows Azure AD tenants. You will do so by changing the settings of the application in the Windows Azure Management Portal.
Navigate to the Windows Azure Management Portal and sign in with the same account you used to go through the LoB and Graph API walkthroughs. Go to the Active Directory tab, select your directory, click on the Integrated Apps header, locate the entry for the app you created in the LoB tutorial and click on it.
You will land on the same Quick Start page you were presented with at the end of the registration process; click on the Configure header.
Note |
---|
If during the last walkthroughs you checked the box “skip quick start the next time I visit”, you will reach the Configure screen directly. |
Halfway through the page, you’ll find a switch labeled External Access.
The default position of the switch is off: at creation time, applications are configured to be accessible only by users from the same Windows Azure AD tenant in which they have been created.
Turning External Access on means making possible for other tenants to provision the application in their own directories. Concretely, this means that Windows Azure AD will activate the customer consent URL that can be used by other tenant’s admins to grant to the application permission to access their own directories. When they consent to your application, that will trigger the provisioning in their own Windows Azure AD tenants of an entry describing the application, while will set app parameters (APP ID URI, APP URL, keys used to access the Graph API, etc.) in their page.
Important |
---|
The act of enabling external access for an application does not change any of the existing permissions the application already has on your own Windows Azure AD tenant. Furthermore: the act of disabling external access for an application will not affect the access levels already granted to it in yours or other’s tenants while it was available. In other words, this switch only affects the availability of the consent experience and associated provisioning mechanism for external customers; it does not affect the application entries it helped create. |
Click on the ON side of the switch. You will notice that the button changes into purple, and the command bar on the bottom of the screen now offers a Save button. Try to click on the Savebutton: it will not work, but it is important for you to familiarize yourself with the resulting error message.
The update operation failed. Click on the Details icon to troubleshoot the issue.
The message points out that the APP ID URI format is not valid for this operation.
Given that externally available applications will be accessible by a larger audience, possibly with no prior business ties with the organization offering the app, Windows Azure AD imposes some extra requirements to that kind of apps which make it easier to identify them.
Whereas LoB apps can have any URI as the APP ID URI value, with tenant-level uniqueness as the only constraint, applications need to comply with the following requirements to be set as externally available:
- Only URIs using https:// as the protocol scheme are acceptable
- The host portion of the URI must correspond to a verified domain associated to your Windows Azure AD tenant
If you have a custom domain that you want to use with Windows Azure AD, you can follow the instructions here to verify it. You can find the custom domains already verified under the Domainsheader, peer of the Integrated Apps header in the Windows Azure Management Portal pages for your tenant.
If you do not have or do not want to use a custom domain, you can take advantage of the default three-levels domain that gets assigned to every Windows Azure AD tenant, of the form<tenantname>.onmicrosoft.com. Given that it is a safe default option, that’s the strategy we will follow here.
Scroll down to the bottom of the page, to the single sign-on section. Edit the App ID URI by entering the value https://<tenantname>.onmicrosoft.com/ExpenseReport, substituting the<tenantname> string with the name of your Windows Azure AD tenant. Click Save again.
Provided that the APP ID URI format is the expected one, this time the update operation will succeed. The Management Portal will notify you with a message toaster at the bottom of the screen, however you will be able to tell also by a couple of key changes in the app settings as shown in the next figure.
The external access switch is now fully on the ON position. Furthermore, you will notice a new textbox labeled Url for granting access. That is the URL of the endpoint your prospect customers will use for granting directory access to your app; we will examine the URL structure in details later in the walkthrough, when we will integrate sign up functionality to the application code.
For what your Windows Azure AD tenant is concerned, at this point your application is ready to function in multi-tenant capacity: now you need to modify the application’s code accordingly.
Note |
---|
This tutorial still operated at the development stage of the app’s lifecycle. Once your app is ready for production, you’d come back to this screen to refine the app’s appearance in the consent page (by uploading a proper logo) and to change the app’s Reply URL to its production address. For the latter, you can use the same instructions provided in the section “Deploying the App in Windows Azure Web Sites” in the LoB app walkthrough: the actual deployment steps will vary according to your target environment, but the instructions for updating the app entry in the Management Portal can be applied as is. |
Prepare the Application Project in VS to Handle Multiple Tenants
Open in Visual Studio 2012 the MVC 4 project you created in the preceding walkthroughs.
As anticipated in the “Solution Architecture” section, you will need to apply several targeted changes to add multi-tenancy to the LoB application you created in the first two walkthroughs. We are going to apply those changes incrementally, per functional area; in line with the rest of the walkthroughs in the series, the main goal is to help you understanding what’s happening so that you can apply what you learn to your own applications.
Note |
---|
The changes proposed here also attempt to minimize the re-factoring of the code you created so far. This will lead to some redundancy (e.g. creation of new controllers, rather than consolidation of multiple features under a more generic one) that will make the tutorial easier to follow, however you should feel free to re-factor and normalize functionality as you see fit in your own projects. |
Adjust the Web Sign-on Protocol Coordinates in Web.config
As the very first task, we are going to update the WS-Federation coordinates in the Web.configto reflect the new settings. Locate the Web.config in the project explorer and open it.
Note |
---|
If you didn’t read the “WIF settings in details” advanced section in the LoB walkthrough, now it would be a good time to do so. Some of these tasks could be performed with the Identity and Access Tool, but we chose to explain how to do this by editing directly the Web.config because it illustrates more clearly the necessary changes. |
Scroll down to the <system.identityModel> section. You’ll notice that theidentityConfiguration/AudienceURI element still contains the old APP ID URI value: change it to the new value you entered in the Windows Azure Management Portal, as in the following.
<system.identityModel> <identityConfiguration> <audienceUris> <add value=”https://<tenantname>.onmicrosoft.com/ExpenseReporting” /> </audienceUris>
This will ensure that the app will accept as valid tokens scoped for the new APP ID URI.
Scroll further, to the <wsFederation> element insystem.identityModel.Services/federationConfiguration.
Here you’ll need to apply two changes:
- Modify the realm attribute value with the same APP ID URI you used for the<audienceUris> section above.
- Edit the issuer attribute value by replacing the tenantID GUID with the string
common
.
<federationConfiguration> <cookieHandler requireSsl="false" /> <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/common/wsfed" realm="https://<tenantname>.onmicrosoft.com/ExpenseReporting" requireHttps="false" /> </federationConfiguration>
The realm value corresponds of the value of the WS-Federation parameter wtrealm, which is included in sign-in messages to the authority (the identity provider) to indicate the intended recipient of the requested token; it has to match the value identifying the app in the target Windows Azure AD tenant.
The issuer value indicates the endpoint that should receive sign-on requests. Whereas in the first tutorial that corresponded to your Windows Azure tenant, now we cannot know in advance from which tenant (among all those who granted consent to the app) the next user will be coming from. Windows Azure AD offers a special endpoint, commonly referred to as the tenant-independent endpoint, which allows the app to defer the decision about which tenant should be used to the moment in which the user will enter his or her username. Given that the username includes domain information, at that point the tenant of choice will be uniquely determined and the authentication will flow as usual.
Note |
---|
It is important to remember that Windows Azure AD will issue a token to the user only if the recipient application (indicated by the realm parameter as described earlier) has been granted access within the user’s Windows Azure AD tenant. Using the tenant-independent endpoint does not weaken the access constraints established at the directory level; it just adds generality to the sign-on process. |
Customize the Authentication Process to Work with Multiple Tenants
Again in the Web.config file: scroll to the IssuerNameRegistry element.
You’ll recall from the first tutorial that this element is meant to record the parameters that define a valid token: the thumbprint of the X.509 certificate that should be used to validate the token’s signature, and the issuer value identifying the trusted Windows Azure AD tenant.
That information is still relevant in the multi-tenant case: the main difference is that, instead of having a single value, there is a list of acceptable issuers corresponding to the app’s customer tenants. The class used to implement the IssuerNameRegistry, ValidatingIssuerNameRegistry(VINR), can work with multiple entries in its <validIssuers> element. However a multi-tenant application might acquire new customers while it is running and serving existing ones, and editing the Web.config would have unintended consequences on the app’s availability.
To avoid that issue, in this tutorial we will store tenant’s information in one external file and will create a custom VINR implementation to use the external file as the source of validation coordinates.
Note |
---|
This tutorial implements the bare minimum to demonstrate the store’s functional role. In a real life application performance, availability and robustness of the store would be key aspects of the solution. |
Let’s start by creating the external store. Under the Content folder, create a new XML file (inSolution Explorer, right-click on Content, Add New Item, choose the Data category on the left, pick XML file) and call it tenants.xml.
Edit the file by pasting the following:
<?xml version="1.0" encoding="utf-8" ?> <authority> <tenants> <tenant id="95ca3807-2313-4cfe-93b3-20ef9f46ae88" /> </tenants> <keys> <key id="3A38FA984E8560F19AADC9F86FE9594BB6AD049B" /> </keys> </authority>
You can either leave the <tenants> and <keys> elements blank, or you can seed them with thethumbprint and the tenantID values from the current IssuerNameRegistry; after all, the application already has access rights to your Windows Azure AD tenant.
Now that we have the external repository, let’s create the custom VINR implementation using it to source validation.
Create a new folder AADUtils in the project’s root. Right-click on it, select Add..., then click New Item, select code on the categories on the left, click Class, then name the fileMultiTenantIssuerNameRegistry.
Add the following using directives:
using System.IdentityModel.Tokens; using System.Xml.Linq;
Make the new class an implementation of VINR; add a couple of static properties for trackingtenants.xml’s path and content, then add a static default constructor (all of our methods will be static) to initialize them at creation time.
namespace ExpenseReport.AADUtils { public class MultiTenantIssuerNameRegistry: ValidatingIssuerNameRegistry { private static XDocument doc; private static string filePath; static MultiTenantIssuerNameRegistry() { filePath = HttpContext.Current.Server.MapPath("~/Content/tenants.xml"); doc = XDocument.Load(filePath); } } }
Add methods for probing the repository for the presence of tenantIDs or keys:
public static bool ContainsTenant(string tenantId) { return doc.Descendants("tenant").Where(x => x.Attribute("id").Value == tenantId).Any(); } public static bool ContainsKey(string thumbprint) { return doc.Descendants("key").Where(x => x.Attribute("id").Value == thumbprint).Any(); }
Note |
---|
Both methods follow the same principle, based on LINQ’s concise syntax: they select all the elements of the target type, “tenant” or “key”, and verify if there is one matching the input value; if there is, the call to Any() on the result set will return true. |
Finally, you can add the override of the validation logic like in the following:
protected override bool IsThumbprintValid(string thumbprint, string issuer) { string issuerID = issuer.TrimEnd('/').Split('/').Last(); if (ContainsTenant(issuerID)) { if (ContainsKey(thumbprint)) return true; } return false; }
The method is automatically called by WIF upon receiving a token; its logic is very straightforward, it verifies that the issuing tenant is present in the list and that the token was signed with the registered certificate.
You will need to add more methods to the MultiTenantIssuerNameRegistry, but for what concerns the validation the class has now all it needs.
To ensure that it will be activated at the right stage of the authentication pipeline, you need to add it in the Web.config in place of the default IssuerNameRegistry.
Open the Web.config file, locate the <issuerNameRegistry> element and substitute it with the following:
<issuerNameRegistry type="ExpenseReport.AADUtils.MultiTenantIssuerNameRegistry, ExpenseReport" />
Adjust the Automatic Signing Key Refresh Logic
The first walkthrough introduced in the app some logic that updates the issuer coordinates (issuer and thumbprint of the X.509 certificate used to verify the token’s signature) every time the application starts up. You can find all the details in the section “Adding Automatic Metadata Refresh”.
Now that we keep tenant and key information in a file other than Web.config, we are no longer bound to performing the update in the Application_Start() event: however it is a good moment in the app’s lifecycle, hence for this tutorial we’ll keep it there.
However we do need to address a couple of details:
- The ValidatingIssuerNameRegistry.WriteToConfig() method works with the original config entry, and expects it in the Web.config: it will not work with our custom MTINR.
- The list of the accepted tenants does no longer come from a single tenant’s metadata
Both are pretty straightforward to solve. You just need to add another static method to MTINR, which just updates the keys in tenants.xml. Here there’s the code of the method, to be added to the class MultiTenantIssuerNameRegistry in the corresponding fileMultiTenantIssuerNameRegistry.cs.
public static void RefreshKeys(string metadataAddress) { IssuingAuthority ia = ValidatingIssuerNameRegistry.GetIssuingAuthority(metadataAddress); bool newKeys = false; foreach (string thumbp in ia.Thumbprints) if (!ContainsKey(thumbp)) { newKeys = true; break; } if (newKeys) { XElement keysRoot = (XElement)(from tt in doc.Descendants("keys") select tt).First(); keysRoot.RemoveNodes(); foreach (string thumbp in ia.Thumbprints) { XElement node = new XElement("key", new XAttribute("id", thumbp)); keysRoot.Add(node); } doc.Save(filePath); } }
The method can be broken down in three tasks:
- The call to GetIssuingAuthority(), a static method offered byValidatingIssuerNameRegistry, extracts from a metadata document the keys advertised by the service as what should be used to verify incoming tokens
- The first foreach block checks if the keys found in the metadata document are already present in tenants.xml.
- If no new information was found in the metadata, the method exists without changes; otherwise, the keys in the current tenants.xml are cleaned up and substituted by the new one(s) from the metadata document.
Note |
---|
The same warnings about HTTPS endpoint validation given in the section “Adding Automatic Metadata Refresh” from the first walkthrough apply here as well. |
That’s all the logic you need to implement automatic keys refresh: the next step is modifying theGlobal.asax to call RefreshKeys instead of the Web.config-based routine. Locate theGlobal.asax, and modify RefreshValidationSettings as follows:
protected void RefreshValidationSettings() { string metadataAddress = ConfigurationManager.AppSettings["ida:FederationMetadataLocation"]; AADUtils.MultiTenantIssuerNameRegistry.RefreshKeys(metadataAddress); }
Note |
---|
You might have noticed that the method still relies on the metadata address of your Windows Azure AD tenant – that is to say, of the tenant in which the application as first developed, as captured by the first run of the Identity and Access tool – instead of moving to a tenant-independent endpoint. In fact, the two are roughly equivalent: the application’s entry in Windows Azure AD is tied to your tenant, hence the corresponding metadata endpoint is guaranteed to work. On the other hand, moving to the tenant-independent endpoint would make the code easier to reuse as a starting point for other applications. You can choose either approach, the tutorial opted for the one requiring minimal code changes. |
Moving From Blanket Protection to Selective Authentication Requirements
In a classic LoB application all the users come from the same authority, hence the authentication flow can, at any time, unambiguously decide where to send unauthenticated users. Moreover, often users already have an active session with the authority (think users already signed in through their workstation prompts in their local AD domain) hence the authentication phase can be performed transparently, with no perceived interruption between typing the address of the app in the browser and gaining access to its UI. This is the default behavior of web apps configured to use sign-on protocols such as WS-Federation.
In a multitenant application users can, by very definition, come from different tenants: users are called to participate in the process of establishing what authority should be involved in the authentication, in what is known in literature as home realm discovery (HRD). In Windows Azure AD that phase is handled by the tenant-independent endpoint, which will take care of guiding the user through the necessary experience: no code changes are required for that. That said, as shown below there are other aspects beyond HRD which might require some specific work.
Multitenant application often include experiences that are available to anonymous users: common examples are landing pages, onboarding features, news, public support forums, entry points for free trials and many others. The blanket protection approach is not suitable for supporting such features: as such, this section will show you how to change the WIF settings to apply more fine-grained authentication requirements, on a controller by controller basis.
Change Your Web.config File
- Open the Web.config, and locate the <authorization> element in <system.web> right under the comment block labeled “Commented by Identity and Access VS Package”.
Note Do not confuse that element with the homonymous ones in the <location> blocks. - The current setting,
<deny users="?" />
, tells to ASP.NET that only authenticated users should be allowed to request resources from this application. We no longer want to drive this constraint from the Web.config: comment out the entire authorization element.
<!--<authorization> <deny users="?" /> </authorization>-->
- Locate the <authentication> element, comment it out and paste the following element in the Web.config, right below the comment block you just created.
<!--<authentication mode="None" />--> <authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
- Scroll down to the <system.webServer> element, and locate the <modules> list. You’ll see that there is a <remove> directive for the FormsAuthentication module: comment that out as shown below.
<modules> <!--<remove name="FormsAuthentication" />-->
- Finally, scroll down to the very end of Web.config. In the <wsFederation> element, which you already modified earlier in this tutorial, set passiveRedirectEnable to false.
<wsFederation passiveRedirectEnabled="false" issuer="https://login.windows.net/common/wsfed" realm="https://<your-tenant-name>.onmicrosoft.com/ExpenseReport" requireHttps="false" />
Note By default, the WIF modules will examine every 401 return codes from the app and, if there is an authority correctly configured, it would transform them into 302 redirects carrying sign-in messages to the trusted authority. This last setting tells WIF to ignore outgoing 401s, so that they can be redirected according to the forms authentication settings instead (in this case, to our custom controller).
Create the Account Controller
- The app is now configured to redirect to ~/Account/LogOn at authentication time. Let’s create a controller that will handle that specific route.
Right-click on the Controllers folder, click Add, click Add Controller, then name itAccountController. - Add the following using directive:
using System.IdentityModel.Services;
- Delete the default implementation of the controller, and substitute it with the following:
public void LogOn() { RedirectResult result; if (!Request.IsAuthenticated) { SignInRequestMessage sirm = FederatedAuthentication.WSFederationAuthenticationModule.CreateSignInRequest("", HttpContext.Request.RawUrl, false); result = Redirect(sirm.RequestUrl.ToString()); } else { result = Redirect("~/"); } result.ExecuteResult(this.ControllerContext); }
In a nutshell: the controller executes nearly the same logic that was implemented by the WIF module, that is to say:
- If the request is not authenticated:
- It generates a WS-Federation sign-in message, according to the config settings (the content of the <wsFederation> element)
- It redirects the user’s browser accordingly
- It generates a WS-Federation sign-in message, according to the config settings (the content of the <wsFederation> element)
- If the request is authenticated:
- It redirects to the home page.
- It redirects to the home page.
The advantage is that you have now full control on when that happens: as you will see later in the tutorial, this will allow you to add an explicit sign-on gesture in the application’s UI while maintaining the advantage of automatic redirects triggered by the authentication policies of the requested resource.
Note |
---|
This walkthrough picks the minimalist approach, in which the authentication experience is fully handled by Windows Azure AD; however that does not necessarily mean you have to do the same in your solutions. You have now complete control on what happens when the authentication flow is triggered: if you want to show a View to prompt (or even just help) the user for more info, you can easily do so by modifying the LogOn action. |
There is one last thing to do to achieve full federated authentication-forms authentication integration.
When the LogOn action is called in response to a request to a protected resource, the Forms authentication module includes the URL of the originating resource in the ReturnUrl query parameter; however, WIF ignores it. As things are now, once authenticated the user would land on the application’s home page, instead of the requested resource. Performing the extra internal redirect to the resource is easy, it’s just a matter of adding some logic to handle the End_Requestevent in the Global.asax.
Adding a Redirect to the Requested Resource
-
Open the Global.asax and add the following directive:
using System.Security.Claims;
-
Then, add the following method:
protected void Application_EndRequest(object sender, EventArgs e) { string wsFamRedirectLocation = HttpContext.Current.Response.RedirectLocation; if (wsFamRedirectLocation != null && wsFamRedirectLocation.Contains("ReturnUrl") && ClaimsPrincipal.Current.Identity.IsAuthenticated) { HttpContext.Current.Response.RedirectLocation = HttpUtility.ParseQueryString( wsFamRedirectLocation.Split('?')[1])["ReturnUrl"]; } }
The method runs at the end of the HTTP request processing pipeline. It inspects the HTTP header Location, which is used in case of redirection: if it is non-empty, the user is authenticated (hence the WS-Federation protocol flow already took place in full) and it contains ReturnUrl (indication that it is storing Forms authentication return info) then the browser is redirected to the ReturnUrllocation.
Secure the Controllers
Now that you turned off the blanket authentication protection, every single controller is responsible to specify its authentication requirements. The easiest way of doing so is decorating every Action with the [Authorize] attribute, as you would do with any other ASP.NET authentication scenario.
Warning |
---|
Although the tutorial covers MVC style applications, it is perfectly possible to pursue the same approach in Web Forms (for example, by leveraging the <location> and <authorization>elements in the Web.config). |
Below you can see the modification applied to the About() action of the Home controller:
[Authorize] public ActionResult About() { ViewBag.Message = "Your app description page."; return View(); }
Do the same for every action in the Home controller, with the exception of Index(). In this tutorial Index is reserved for the unauthenticated entry point for the application’s experience.
In the first walkthrough you modified Index to extract user info from claims: now that unauthenticated users will be able to access the action, that code will fail given that the absence of an authenticated user implies that there are no claims available. Comment it out and substitute the ViewBag.Message assignment to whatever message you see fit.
Public ActionResult Index() { //ClaimsPrincipal cp = ClaimsPrincipal.Current; //string fullname = // string.Format(“{0} {1}”, cp.FindFirst(ClaimTypes.GivenName).Value, // cp.FindFirst(ClaimTypes.Surname).Value); ViewBag.Message = "Welcome to the Expense Note App"; return View(); }
Note |
---|
The tutorial does not give instructions on how to do so, however if you want you can add to it (or to the associated View) some text welcoming the user and explaining how to use the application. That will be especially handy later, for the sign up parts. |
Change gestures for sign-in in the _Layout.cshtml file
The changes you have made so far will trigger the authentication flow as soon as the user clicks on a UI element requesting one action protected with [Authorize]. However, it is very common for applications not covered by blanket protection to offer an explicit UI gesture for signing in.
To do so, we will revisit the user greeting text on the top area of the screen, defined in_Layout.cshtml, which we worked on while explaining how to add sign out in the Adding Sign-In walkthrough. At the time we inlined everything in _Layout.cshtml, however in this tutorial we’ll need to add more functionality and that calls for some refactoring.
- Open _Layout.cshtml, locate the login section
<section id=”login”>
and modify it as follows:
<section id=”login”> @Html.Partial("_LoginPartial") @* @if (Request.IsAuthenticated) { <text> Hello, <span class=”username”>@User.Identity.Name</span>! @Html.ActionLink(“Signout”,”SignOut”, “SignOut”)</text> } else { <text> You are not authenticated </text> } *@ </section>
- Once that’s done, right-click on the folder Views/Shared, click Add, click View, name the new view _LoginPartial, choose Razor as the view engine, then click Add.
- Substitute _LoginPartial.cshtml’s content with the following code:
@if (Request.IsAuthenticated) { <text> Hello, <span class=”username”>@User.Identity.Name</span>! @Html.ActionLink("Signout","SignOut", "SignOut")</text> } else { <ul> <li>@Html.ActionLink("Sign in", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "signLink" })</li> </ul> }
That is the same logic inlined in the LoB tutorial, with the only difference being that the “you are not authenticated” text has been substituted with a clickable link which will activate the LogOnaction of the Account controller.
The Sign-Out Controller
No changes are necessary for adapting the sign out logic to the multi-tenant case. The controller was already configured to retrieve the issuer endpoint to use in the sign-out flow from theWeb.config; that means that it will now automatically pick up the change to the tenant-independent endpoint you applied at the beginning of the tutorial.
Customize the Graph Access Logic
The Graph API access logic is already designed to construct resource endpoints on the basis of thetenantID received in the claims describing the current user; as such, it will continue to work even when users will start coming from multiple tenants.
Note |
---|
One of the Windows Azure AD features which make this possible is the fact that ClientId and client secret do not change across tenants: when a tenant administrator gives consent for the app to access his or her Windows Azure AD tenant, the corresponding new application entry in his or her tenant will have the same values as everybody else. However note that this does not mean that one malicious administrator can retrieve the app’s secret and try to use it against another Windows Azure AD tenant which consented the same app. As you have learned in the Graph API walkthrough, secrets cannot be retrieved after creation. |
Add Sign-Up Capabilities to the Application
The last feature we need to add to the app to complete the transition to multi-tenancy is the ability of onboarding new customer organizations.
As explained through the document, applications marked as externally available in Windows Azure AD are associated to a specially crafted URL, which can be used by prospect customers to grant consent for the app to access their directory and have an entry for the app automatically provisioned to their tenants. In this section you will add a SignUp controller which can redirect users to the consent URL, and a UI gesture to trigger the action.
Note |
---|
Here we add the feature directly in the app, however once you understand how the mechanism works nothing prevents you from implementing the consent logic outside of the application, group it with consent links for other apps in your portfolio, and so on. |
- Add an empty SignUp controller, following the instructions for controller creations provided earlier in the walkthroughs.
- Add the following using directive:
using System.Configuration;
- Substitute the controller’s default implementation with the following two methods:
private string CreateConsentURL(string clientId, string requestedPermissions, string consentReturnURL, string context) { string consentUrl = string.Format("https://account.activedirectory.windowsazure.com/Consent.aspx?ClientId={0}", clientId); if(!String.IsNullOrEmpty(requestedPermissions)) consentUrl+= "&RequestedPermissions="+requestedPermissions; if(!String.IsNullOrEmpty(consentReturnURL)) consentUrl+= "&ConsentReturnURL="+HttpUtility.UrlEncode( consentReturnURL+(String.IsNullOrEmpty(context) ? String.Empty : "?"+context )); return consentUrl; } public void SignUp() { string request = System.Web.HttpContext.Current.Request.Url.ToString(); string returnurl = request.Substring(0, request.Length -6); string clientID = ConfigurationManager.AppSettings["ClientId"]; RedirectResult result = Redirect(CreateConsentURL(clientID, "DirectoryReaders", returnurl, string.Empty)); result.ExecuteResult(this.ControllerContext); }
The first method creates the URL that allows prospect customers to give to your app consent to access their directory tenants. Although the Windows Azure Management Portal provides you with the consent URL for your app, having the code that generates it from some basic parameters will give you extra flexibility. Let’s examine the parameters and their semantic:
- ClientId represents the identifier of your application. You added it in the config during the Graph API walkthrough. It is mandatory.
- RequestedPermissions expresses the level of directory access you want the prospect customer to grant to the application. The possible values reflect the access level choices you had in the Windows Azure Management Portal at app registration time, though here you are free to request different levels than the ones you picked in the UI for your own tenant. The values are:
- DirectoryReaders: use this value to request read access to the directory
- DirectoryWriters: use this value to request read/write access
- <Empty>: do not specify any value if you just want to have SSO capabilities
- DirectoryReaders: use this value to request read access to the directory
- ConsentReturnUrl indicates where to redirect the browser session once the user granted (or denied) permissions to the application. You’ll want to specify a route in your app where your sign up processing logic resides, more details later.
The method CreateConsentURL exposes all of the parameters above, plus an extra one. The context parameter is offered as a way for you to include extra info, which will travel to the consent page encoded within the ConsentReturnUrl and will come back once the redirect is honored. It is offered as a separate parameter to your convenience, so that you don’t have to mix the main return URL with context logic.
The context can come in useful when you want to track additional information about the request. For example, the redirect to consent will very often be performed as part of your own app-specific onboarding process and you’ll likely want to keep track of some context.
The second method is the actual action that will trigger the redirection. It invokesCreateConsentURL passing as parameters the ClientId from the config, the root of the current controller as return URL, DirectoryReaders as the requested access level and nothing as context. The method then proceeds to redirect to the newly crafted URL.
Now that you added the logic for triggering the redirect to the consent URL, you need to complete the flow by providing the code that will process the results. As established by the choice of ReturnURL below, this tutorial will place that code in the Index action of the SignUp controller.
- Add the using directive below.
using ExpenseReport.AADUtils;
- Add the following method to the SignUp controller.
public void Index() { if ((!string.IsNullOrEmpty(Request.QueryString["TenantId"]) && (!string.IsNullOrEmpty(Request.QueryString["Consent"])))) { if (Request.QueryString["Consent"].Equals("Granted", StringComparison.InvariantCultureIgnoreCase)) { MultiTenantIssuerNameRegistry.AddTenant(Request.QueryString["TenantId"]); } //redirect to SignIn RedirectResult result = Redirect("~/Account/LogOn"); result.ExecuteResult(this.ControllerContext); } }
Note The method code does not include error management.
Upon granting or denying access, Windows Azure AD honors the redirect to ReturnURL and appends a couple of extra parameters:
- Consent: this indicates the outcome of the operation. It can have values “Granted” or “Denied”
- TenantID: if the Consent parameter has value “Granted”, TenantID will be present and contain the identifier of the tenant that just consented to the application’s access requests
The purpose of this method, then, consists of capturing the ID of tenants who provisioned the app and keep track of them as valid authorities in the MTINR collection. Per the validation logic created earlier in the walkthrough, that will allow users coming from the new tenant to be accepted at sign-in time.
The code above examines the request URL, and if it finds that it contains a new tenant it saves it in the MTINR (using a method we will implement shortly).
Note |
---|
The implementation here does not add any restrictions to the requests arriving at this route, and if they are of the correct format the content of the TenantID parameter will end up in the MTINR store. That would not be the behavior you’d want to have in a real-life application, as it might be abused to flood your store with bogus entries or for gaining unauthorized access. As mentioned earlier, the consent experience would likely be part of your own onboarding process (e.g. gathering info about the prospect customer, getting payments, or any other onboarding steps your app requires). As such, you should ensure that you include that context in the consent URL creation and that you validate it when you process the response; that will ensure that fraudulent requests will be refused. |
After the tenant has been saved, the method concludes by triggering a sign on so that the user can start accessing the app right away.
Note |
---|
If your onboarding process requires further steps, you can choose to delay the sign on to a later time and serve a View instead. |
The Index action of the SignUp controller introduced a new MTINR method, AddTenant. Let’s add an implementation for it. Open MultiTenantIssuerNameRegistry.cs and add the following method to the MultiTenantIssuerNameRegistry class:
public static void AddTenant(string tenantId) { if (!ContainsTenant(tenantId)) { XElement node = new XElement("tenant", new XAttribute("id", tenantId)); XElement tenantsRoot = (XElement)(from tt in doc.Descendants("tenants") select tt).First(); tenantsRoot.Add(node); doc.Save(filePath); } }
The AddTenant method is very straightforward: if the tenantId in input does not already exist in the store (the consent operation is idempotent) it adds a new entry for it under the <tenants>node.
Finally, the application needs a UI gesture for triggering the sign up flow. This can be easily achieved by adding an entry in _LoginPartial.cshtml: open the file and change the <li> block for sign up as shown below.
@if (Request.IsAuthenticated) { <text> Hello, <span class="username">@User.Identity.Name</span>! @Html.ActionLink("Signout","SignOut", "SignOut")</text> } else { <ul> <li>@Html.ActionLink("Sign up", "SignUp", "SignUp", routeValues: null, htmlAttributes: new { id = "signupLink" })</li> <li>@Html.ActionLink("Sign in", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "signLink" })</li> </ul> }
Note |
---|
The current UI takes a very minimalist approach. In an actual application you might want to make the semantic of the sign up and sign-in controls: possible ways of achieving that include a more verbose link text, an explanation in the home page’s main body, and various others. |
Optional: Create a Test Subscription
In this section we present you with some options you can leverage for testing your new multitenant application.
To test a multitenant application in all its aspects, it is best to have available a second Windows Azure AD tenant that you can use for exercising all consent-related flows in the sample application. At this point in time, this means you need to have access to another Windows Azure subscription. You can obtain it by following a second time the sign-up and directory creation flow described at the beginning of the first walkthrough; alternatively, you can follow the instructions here for signing up for a trial subscription with a user from an existing Windows Azure AD tenant.
Given that the consent granting operation is idempotent, you could also use your own Windows Azure AD tenant for experimenting with sign up: that would spare you the task of creating a second subscription, however that would not allow you to see what happens when you try to sign in with an un-provisioned tenant, or experience the consent revocation flow (given that for your tenant the app is still a LoB application, explicitly provisioned hence with no express need for consent granting steps).
Yet another way of experimenting would be to use a Windows Azure AD tenant that is not necessarily related to a Windows Azure subscription, such as the directory tenant you get when you subscribe to services like Office365 or Intune.
Note |
---|
Only Windows Azure AD tenant administrators can grant consent to applications. No matter which testing method you choose, you need to have access to a tenant administrator user credentials. If you created your Windows Azure AD tenant using a Live ID-based Windows Azure subscription, you need to ensure that your tenant contains at least one tenant user that belongs to the Global Administrators role. You can easily create one by following the instructions in the first walkthrough, making sure that you choose “Global Administrator” in the user profile screen. |
Test the Application
You are now finally able to see the multitenant application in action. If you want to observe the app’s inner workings, feel free to add a breakpoint in any of the code fragments you added through the tutorial.
Running the Application
Note |
---|
If you are using a Windows Azure AD tenant other than your own, you might need to close all of the browser instances before starting; alternatively, you could instruct Visual Studio to start the debugging session in a web browser other than your default one. Those expedients might be necessary to ensure that there will be no interference between your existing web sessions and the debug one. |
In Visual Studio, press F5.
As expected, instead of being immediately redirected to Windows Azure AD for authentication you are presented with the home root of the application.
In the top bar you can locate the login section, correctly displaying the sign up and sign-in buttons given that we are currently not authenticated.
Let’s test the onboarding flow. Click on the sign up button.
You are immediately redirected to Windows Azure AD’s authentication prompt. Sign in using the administrator credentials of the Windows Azure AD tenant you decided to use for testing.
Note |
---|
If you got an error here, please double check that you are signing in with an administrator from a Windows Azure AD tenant. Currently Live ID users cannot be used for granting consent in the flow described here. |
As soon as you successfully authenticated, Windows Azure AD prompts you to grant or deny access to your directory tenant for the Expense Reporting application, for the access level specified in the SignUp controller.
Click Grant.
The page records the consent in the current user’s directory, then triggers a sign in; given that you already authenticated with Windows Azure AD at consent time, you don’t need to enter credentials anymore and you find yourself automatically signed in. The login section reflects the new state, showing your user’s name and the sign out button.
You can click anywhere in the app to confirm that you are in an authenticated session: for example, click on the Users tab: you’ll be able to see the list of users.
Let’s test the sign out. Click on the sign out button.
After a couple of redirects, you land on the SignOut view.
To confirm that you are truly signed out, click again on the Users tab: this time you will be prompted to sign in.
Enter your credentials: you’ll observe that after the authentication phase redirects, the browser correctly shows you the Users UI you requested in the first place. That shows that the logic added in the End_Request event worked as expected.
Revoking Access Rights
You will be able to perform the tasks shown in this section only if you provisioned the application in a Windows Azure AD tenant other than your own and associated to a Windows Azure subscription.
Navigate to the Windows Azure Management Portal and sign in as the administrator of the Windows Azure AD tenant that you used for provisioning the application.
Go to the Active Directory tab, pick your directory, choose Integrated Apps, locate the Expense Reporting app and click on it.
Whereas in the tenant in which the app was created you’d get the configuration UI that would allow you to change the app’s settings, here you a playing the part of a customer who purchased the application: you get a generic description page, summarizing the main application coordinates and the access levels granted.
Click on the Manage Access button on the bottom command bar.
This UI allows you to revoke access to the app, if you no longer want it to be able to access your directory in the granted terms.
Click Remove.
The app has been removed, and is no longer in the integrated apps list.
Go back to Visual Studio and press F5 again.
Click on Sign in, authenticate as the admin of the customer tenant and see what happens.
The user authenticates successfully with the directory, but the app that would be the recipient of the authentication token is no longer listed as one that has access to the user’s directory tenant: as a result, the directory does not issue the requested token and presents the user with an error page.