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

Practical Microsoft Azure Active Directory Blog Series

I finally had a chance to play with Microsoft Azure Active Directory in a recent project. I found the experience to be very interesting – Azure AD itself is an amazing, powerful product with a lot of potential. It certainly has a few rough edges here and there, but it’s pretty clear Microsoft are putting a lot of effort into it as it’s forming the cornerstone of how it authenticates all of it’s services including Office 365.

Azure AD gives you the ability to securely manage a set of users and also gives the added benefit of allowing two-factor authentication (2FA), single-sign-on across applications, multi-tenancy support and ability to allow external organisations to authenticate against your application.

This blog series will outline the minimum set of steps that you need to perform to quickly and easily add Azure AD authentication to an existing ASP.NET MVC 5 (or 3 or 4) site (or a new one if you select the No Authentication option when creating it) as well as configure things like API authentication, role authorisation, programmatic logins and deployments to different environments.

There are already tools and libraries out there for this – why are you writing this series?

Microsoft have made it fairly easy to integrate Azure AD authentication with your applications by providing NuGet packages with most of the code you need and also tooling support to configure your project in Visual Studio. This is combined with a slew of MSDN and technet posts covering most of it.

When it comes to trying to understand the code that is added to your solution however, things become a bit tricky as the documentation is hard to navigate through unless you want to spend a lot of time. Also, if you have Visual Studio 2013 rather than Visual Studio 2012 you can only add authentication to a new app as part of the File -> New Project workflow by choosing the Organizational Authentication option:

ASP.NET Organizational Authentication option
Visual Studio: File > New Project > ASP.NET > Change Authentication > Organizational Authentication

If you have an existing ASP.NET web application and you are using Visual Studio 2013 then you are out of luck.

Furthermore, the default code you get requires you to have Entity Framework and a database set up, despite the fact this is only really required if you are using multiple Azure AD tenants (unlikely unless you are creating a fairly hardcore multi-tenant application).

If you then want to add role-based authentication based on membership in Azure AD groups then there is no direction for this either.

For these reasons I’m developing a reference application that contains the simplest possible implementation of adding these features in an easy to follow commit-by-commit manner as a quick reference. I will also provide explanations of what all the code means in this blog series so you can understand how it all works if you want to.

You can see the source code of this application here and an example deployment here. The GitHub page outlines information such as example user logins and what infrastructure I set up in Azure.

What are you planning on covering?

This will be the rough structure of the posts I am planning in no particular order (I’ll update this list with links to the posts over time):

I’m notoriously bad at finishing blog series‘ that I start, so no promises on when I will complete this, but I have all of the code figured out in one way or another and the GitHub should at least contain commits with all of the above before I finish the accompanying posts so *fingers crossed*! Feel free to comment below if you want me to expedite a particular post.

More resources

I came across some great posts that have helped me so far so I thought I’d link to them here to provide further reading if you are interested in digging deeper:

Announcing repave.psm1

So after 18 months of not repaving my machine and occasionally (especially lately) having to deal with the machine filling up and slowing down I’m finally at the point where it’s time to repave. I wanted to do it ages ago, but I avoided it because of how painful it is to do.

This time around I’ve decided to bite the bullet and finally do something I’ve been meaning to do all along – create a script to make it much easier / quicker as well as form documentation about what programs / what setup I want for my machine.

I’ve been interested in Chocolatey and Boxstarter for ages to do this very thing. In this instance I didn’t bother using Boxstarter since I didn’t have any restarts in there, but I encourage people to look into it particularly if doing VM install scripts – it’s AMAZING.

I started writing this crazy PowerShell script to automate all the installs and settings I wanted and eventually I refactored it until it became like this. I think it’s really readable and maintainable and acts really well as documentation for myself.

While developing it I initially had a bunch of cinst calls, but the problem with that is each call incurs a 2s startup cost for some reason – this made developing it painful. In order to develop the script incrementally (I was doing it inside of a VM so I could trash it and start from the beginning again) I wanted three things:

  • Speed (if something is already installed I want it to skip it instantly, not wait for cinst to spin up for 2s)
  • Idempotency (I want to run and re-run the script again and again and again after making small changes to see their effect)
  • Fail fast (if something is wrong I want it to just fail and print an error so I can see what happened – I don’t want it to continue trying to install other things that might be dependent on the thing that failed)

I managed to achieve all of that and the other advantage I see in this approach is that it makes it really easy for me to reuse the script as an update mechanism if I decide to change things between re-paves. This is awesome and I think makes the script way more useful.

Long-story short: I’ve abstracted all of the main functionality into a PowerShell module and open-sourced it as repave.psm1 on GitHub. Check it out and feel free to fork it to create your own scripts and submit back a pull request with any fixes or additions.

It’s a bit rough around the edges since I’ve knocked it up in a hurry this weekend, but I did put in some initial documentation to describe all the functionality and there are two example scripts in there that use it.

Enjoy!