Authenticating an ASP.NET MVC 5 application with Microsoft Azure Active Directory

This post outlines how to easily add Azure AD authentication to an existing (or new) ASP.NET MVC 5 (or 3 or 4) application.

Practical Microsoft Azure Active Directory Blog Series

This post is part of the Practical Microsoft Azure Active Directory Blog Series.

Add Azure AD Authentication

These instructions will help you easily add authentication to your new or existing ASP.NET application, based on what the Visual Studio Identity and Access tools do. It’s a basic setup for a single tenant. Read the next post in the series to understand what’s going on and ways that it can be extended. The links show either a commit from the example project or to relevant documentation.

Note: Ignore the ...’s and replace the {SPECIFIED_VALUES} with the correct values.

  1. Create an Azure Active Directory tenant; note: AD tenants are not associated with your Azure Subscription, they are “floating” so add any live ids for people you want to administer it as Global Administrators
  2. Create an Application in your AD tenant with audience URL and realm being your website homepage (minus the slash at the end)
    • Record the name of your AD tenant e.g. {name}.onmicrosoft.com
    • Record the GUID of your AD tenant by looking at the FEDERATION METADATA DOCUMENT URL under View Endpoints
    • The image upload and Sign On URL are used for the Azure AD Applications Portal
  3. Create a user account in your tenant that you can use to log in with
  4. Install-Package Microsoft.Owin.Security.ActiveDirectory
  5. Install-Package System.IdentityModel.Tokens.ValidatingIssuerNameRegistry
  6. Add a reference to System.IdentityModel
  7. Add a reference to System.IdentityModel.Services
  8. Add a Startup.cs file (if it doesn’t already exist) and configure OWIN to use Azure Active Directory (edit for new version)
      using System.Configuration;
      using Microsoft.Owin.Security.ActiveDirectory;
      using Owin;
      namespace {YOUR_NAMESPACE}
      {
       public class Startup
       {
           public void Configuration(IAppBuilder app)
           {
               app.UseWindowsAzureActiveDirectoryBearerAuthentication(
                   new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                   {
                       TokenValidationParameters = new TokenValidationParameters
                       {
                           ValidAudience = ConfigurationManager.AppSettings["ida:AudienceUri"]
                       },
                       Tenant = ConfigurationManager.AppSettings["AzureADTenant"]
                   });
           }
       }
      }
    
  9. Add the correct configuration to your web.config file; change requireSsl and requireHttps to true if using a https:// site (absolutely required for production scenarios)
     <?xml version="1.0" encoding="utf-8"?>
     <configuration>
       <configSections>
         <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
         <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
       </configSections>
       ...
       <appSettings>
         ...
         <add key="ida:AudienceUri" value="{YOUR_WEBSITE_HOMEPAGE_WITHOUT_TRAILING_SLASH}" />
         <add key="ida:FederationMetadataLocation" value="https://login.windows.net/{YOUR_AD_TENANT_NAME}.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml" />
         <add key="AzureADTenant" value="{YOUR_AD_TENANT_NAME}.onmicrosoft.com" />
       </appSettings>
       ...
       <system.webServer>
         ...
         <modules>
           <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
           <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
         </modules>
       </system.webServer>
       ...
       <system.identityModel>
         <identityConfiguration>
           <issuerNameRegistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry">
             <authority name="https://sts.windows.net/{YOUR_AD_TENANT_GUID}/">
               <keys>
                 <add thumbprint="0000000000000000000000000000000000000000" />
               </keys>
               <validIssuers>
                 <add name="https://sts.windows.net/{YOUR_AD_TENANT_GUID}/" />
               </validIssuers>
             </authority>
           </issuerNameRegistry>
           <audienceUris>
             <add value="{YOUR_WEBSITE_HOMEPAGE_WITHOUT_TRAILING_SLASH}" />
           </audienceUris>
           <securityTokenHandlers>
             <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
             <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
           </securityTokenHandlers>
           <certificateValidation certificateValidationMode="None" />
         </identityConfiguration>
       </system.identityModel>
       <system.identityModel.services>
         <federationConfiguration>
           <cookieHandler requireSsl="false" />
           <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/{YOUR_AD_TENANT_NAME}.onmicrosoft.com/wsfed" realm="{YOUR_WEBSITE_HOMEPAGE_WITHOUT_TRAILING_SLASH}" requireHttps="false" />
         </federationConfiguration>
       </system.identityModel.services>
     </configuration>
    
  10. Configure AntiForgery to use the correct claim type to uniquely identify users
    // Global.asax.cs
              protected void Application_Start()
              {
                  ...
                  IdentityConfig.ConfigureIdentity();
              }
    
    // App_Start\IdentityConfig.cs
    using System.IdentityModel.Claims;
    using System.Web.Helpers;
    namespace {YOUR_NAMESPACE}
    {
        public static class IdentityConfig
        {
            public static void ConfigureIdentity()
            {
                AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
            }
        }
    }
    
  11. Configure the application to refresh issuer keys when they change
        public static void ConfigureIdentity()
        {
            ...
            RefreshIssuerKeys();
        }
        private static void RefreshIssuerKeys()
        {
            // http://msdn.microsoft.com/en-us/library/azure/dn641920.aspx
            var configPath = AppDomain.CurrentDomain.BaseDirectory + "\\" + "Web.config";
            var metadataAddress = ConfigurationManager.AppSettings["ida:FederationMetadataLocation"];
            ValidatingIssuerNameRegistry.WriteToConfig(metadataAddress, configPath);
        }
    
  12. Add LogoutController
    // Controllers\LogoutController.cs
    using System;
    using System.IdentityModel.Services;
    using System.Web.Mvc;
    namespace {YOUR_NAMESPACE}.Controllers
    {
        public class LogoutController : Controller
        {
            public ActionResult Index()
            {
                var config = FederatedAuthentication.FederationConfiguration.WsFederationConfiguration;
                var callbackUrl = Url.Action("Callback", "Logout", null, Request.Url.Scheme);
                var signoutMessage = new SignOutRequestMessage(new Uri(config.Issuer), callbackUrl);
                signoutMessage.SetParameter("wtrealm", config.Realm);
                FederatedAuthentication.SessionAuthenticationModule.SignOut();
                return new RedirectResult(signoutMessage.WriteQueryString());
            }
            [AllowAnonymous]
            public ActionResult Callback()
            {
                if (Request.IsAuthenticated)
                    return RedirectToAction("Index", "Home");
                return View();
            }
        }
    }
    
    <!-- Views\Logout\Callback.cshtml -->
    @{
        ViewBag.Title = "Logged out";
    }
    <h1>Logged out</h1>
    <p>You have successfully logged out of this site. @Html.ActionLink("Log back in", "Index", "Home").</p>
    
  13. Add logout link somewhere - @Html.ActionLink("Logout", "Index", "Logout")
  14. Add authentication to the app; do this as you normally would with [Authorize] to specific controller(s) or action(s) or globally by adding to GlobalFilters.Filters.Add(new AuthorizeAttribute());
  15. Load the site and navigate to one of the authenticated pages - it should redirect you to your Azure AD tenant login page whereupon you need to log in as one of the users you created and it should take you back to that page, logged in
  16. The usual User.Identity.Name and User.Identity.IsAuthenticated objects should be populated and if you want access to the claims to get the user’s name etc. then use something like ClaimsPrincipal.Current.FindFirst(ClaimTypes.GivenName).Value