Add AD Azure Authentication to your exisiting Web App

By | February 7, 2018

You already have an existing Web Application and now you had moved to Azure, before since your Web App is just an Intranet App you natively used Active Directory to give Authorization to different pages on you App but what happens if you migrate to Azure will it work? Well no but good thing there is Azure AD that you can still use all you need is to do some adjustments to your application then it will be Ad Azure Authenticated.  Take note this guide before continuing to this guide make sure you already had synchronized your Active Directory to Azure, if not just follow this step by step guide before continuing.


Now If you have AD Syncing then its all good all you need is to follow the steps below and make your Web Application use AD Azure for Authentication without too much coding.  Just follow the simple steps below.

Step 1: Register you Web Application on Azure Active Directory

Login to your Azure then go to Azure Active Directory -> App Registrations -> Then create a New application registration


Give it a name, choose Web app / API, then assign a Sign-On URL, this is just simply the front page / Main Page URL of your Web Application.  Then click Create.

 

Now that its created take note of the Application ID below as we will use that later, that will be the Client Id


Go to the Application Settings -> Keys -> Then Create a new Key, this will be used by your Web Application.  Save the key so you can view what was generated.  Copy the Key and this will be your Client Secret

Still on the Registered App Settings go to Reply URL’s and add in all Reply URL’s in there, this is where where Azure AD issues token responses.


Now hover over you login name and photo on top right corner from there you will see the Tenant Id, take a note of this as well

Now the Azure Active Directory app registration is complete.


Step 2: Add Necessary Packages to your Solution 

To make it easy you can manually add the following lines in your packages.config file of you Web Project

  <package id="EntityFrameworkversion="6.1.3targetFramework="net461" />
  <package id="Microsoft.Azure.ActiveDirectory.GraphClientversion="2.0.2targetFramework="net461" />
  <package id="Microsoft.Data.Edmversion="5.6.3targetFramework="net461" />
  <package id="Microsoft.Data.ODataversion="5.6.3targetFramework="net461" />
  <package id="Microsoft.Data.Services.Clientversion="5.6.3targetFramework="net461" />
  <package id="Microsoft.IdentityModel.Clients.ActiveDirectoryversion="2.14.201151115targetFramework="net461" />
  <package id="Microsoft.IdentityModel.Protocol.Extensionsversion="1.0.0targetFramework="net461" />
  <package id="Microsoft.Owinversion="3.0.1targetFramework="net461" />
  <package id="Microsoft.Owin.Host.SystemWebversion="3.0.1targetFramework="net461" />
  <package id="Microsoft.Owin.Securityversion="3.0.1targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Cookiesversion="3.0.1targetFramework="net461" />
  <package id="Microsoft.Owin.Security.OpenIdConnectversion="3.0.1targetFramework="net461" />
  <package id="Owinversion="1.0targetFramework="net461" />
  <package id="System.IdentityModel.Tokens.Jwtversion="4.0.0targetFramework="net461" />
  <package id="System.Spatialversion="5.6.3targetFramework="net461" />

Take note only add the ones you are not using.  Most of the references here are security related apart from the Entity Framework which is needed for UserTokenCaches table on a SQL Server Database.

Save the file then right click on the references folder and choose Manage NuGet Packages, restore the Packages if it asked you to.  This will download the necessary DLL’s from Nuget, all you need to do is add them as a reference where necessary.

I will have to reiterate that, you will still need to manually add the references and here are the important (highlighted in blue) ones needed for this to work

Now that you had added the necessary references lets do some coding.

Step 3: Modify your project by adding/editing the Controllers, Views, Model, Classes and Config where necessary

Start by adding the following on your web.config to set the correct references

Under <configuration> section

  <configSections>
    <section name="entityFrameworktype="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089requirePermission="false" />
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
  </configSections>
<entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClienttype="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <connectionStrings>
    <add name="DefaultConnectionconnectionString="Data Source=sample.database.windows.net;Initial Catalog=SampleAzureDatabase;Persist Security Info=True;User ID=sample-admin;Password=sample-passwordproviderName="System.Data.SqlClient" />
  </connectionStrings>

Take note the connection string section is where you application will create a Table for User Token Caches, you can use an existing DB or just assign a new one, the codes below will create the necessary tables needed.

Under <appSettings> section will be the settings to your Azure Active Directory Instance and Application Registration Id’s and Keys

    <add key="ida:ClientIdvalue="{{This is the Client Id you saved earlier}}" />
    <add key="ida:AADInstancevalue="https://login.microsoftonline.com/" />
    <add key="ida:ClientSecretvalue="{{This is the Client Secret you saved earlier}}" />
    <add key="ida:Domainvalue="yoursampleurl.com" />
    <add key="ida:TenantIdvalue="{{This is the Tennant Id you saved earlier}}" />
    <add key="ida:PostLogoutRedirectUrivalue="http://yourapp.yoursampleurl.com" />

Under <assemblyBindings> section

      <dependentAssembly>
        <assemblyIdentity name="System.SpatialpublicKeyToken="31bf3856ad364e35culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.6.4.0newVersion="5.6.4.0" />
      </dependentAssembly>

Now lets add the needed classes, lets start with the Models.

So under Models Folder add a ApplicationDbContext.cs class, this class will define your database connection so the UserTokenCache repository.  If you have a current connection you can just change the “DefaultConnection” to the connection string of your database.

using System;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
 
namespace SampleAzureAdAuthentication.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }
 
        public DbSet<UserTokenCache> UserTokenCacheList { getset; }
    }
 
    public class UserTokenCache
    {
        [Key]
        public int UserTokenCacheId { getset; }
        public string webUserUniqueId { getset; }
        public byte[] cacheBits { getset; }
        public DateTime LastWrite { getset; }
    }
}

In the same folder create a AdalTokenCache.cs class.  This class is where the User Token Cache is created and accessed

using System;
using System.Data.Entity;
using System.Linq;
using System.Web.Security;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
 
namespace SampleAzureAdAuthentication.Models
{
    public class ADALTokenCache : TokenCache
    {
        private ApplicationDbContext db = new ApplicationDbContext();
        private string userId;
        private UserTokenCache Cache;
 
        public ADALTokenCache(string signedInUserId)
        {
            // associate the cache to the current user of the web app
            userId = signedInUserId;
            this.AfterAccess = AfterAccessNotification;
            this.BeforeAccess = BeforeAccessNotification;
            this.BeforeWrite = BeforeWriteNotification;
            // look up the entry in the database
            Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            // place the entry in memory
            this.Deserialize((Cache == null? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache"));
        }
 
        // clean up the database
        public override void Clear()
        {
            base.Clear();
            var cacheEntry = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            db.UserTokenCacheList.Remove(cacheEntry);
            db.SaveChanges();
        }
 
        // Notification raised before ADAL accesses the cache.
        // This is your chance to update the in-memory copy from the DB, if the in-memory version is stale
        void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            if (Cache == null)
            {
                // first time access
                Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            }
            else
            {
                // retrieve last write from the DB
                var status = from e in db.UserTokenCacheList
                             where (e.webUserUniqueId == userId)
                             select new
                             {
                                 LastWrite = e.LastWrite
                             };
 
                // if the in-memory copy is older than the persistent copy
                if (status.First().LastWrite > Cache.LastWrite)
                {
                    // read from from storage, update in-memory copy
                    Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
                }
            }
            this.Deserialize((Cache == null? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache"));
        }
 
        // Notification raised after ADAL accessed the cache.
        // If the HasStateChanged flag is set, ADAL changed the content of the cache
        void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if state changed
            if (this.HasStateChanged)
            {
                if (Cache == null)
                {
                    Cache = new UserTokenCache
                    {
                        webUserUniqueId = userId
                    };
                }
 
                Cache.cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache");
                Cache.LastWrite = DateTime.Now;
 
                // update the DB and the lastwrite 
                db.Entry(Cache).State = Cache.UserTokenCacheId == 0 ? EntityState.Added : EntityState.Modified;
                db.SaveChanges();
                this.HasStateChanged = false;
            }
        }
 
        void BeforeWriteNotification(TokenCacheNotificationArgs args)
        {
            // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
        }
 
        public override void DeleteItem(TokenCacheItem item)
        {
            base.DeleteItem(item);
        }
    }
}

Now under the Controllers Folder create an AccountController.cs class.  This class will handle User Sign Ins, Sign Outs and Sign Out Callbacks

using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.Owin.Security;
 
namespace SampleAzureAdAuthentication.Controllers
{
    public class AccountController : Controller
    {
        public void SignIn()
        {
            // Send an OpenID Connect sign-in request.
            if (!Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },
                    OpenIdConnectAuthenticationDefaults.AuthenticationType);
            }
        }
 
        public void SignOut()
        {
            string callbackUrl = Url.Action("SignOutCallback""Account", routeValues: null, protocol: Request.Url.Scheme);
 
            HttpContext.GetOwinContext().Authentication.SignOut(
                new AuthenticationProperties { RedirectUri = callbackUrl },
                OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
        }
 
        public ActionResult SignOutCallback()
        {
            if (Request.IsAuthenticated)
            {
                // Redirect to home page if the user is authenticated.
                return RedirectToAction("Index""Home");
            }
 
            return View();
        }
    }
}

Create also a UserProfileController.cs Class, this class gives you a default class to access ActiveDirectory GraphClient and display some details about the user

using System;
using System.Configuration;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using System.Threading.Tasks;
using Microsoft.Azure.ActiveDirectory.GraphClient;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using SampleAzureAdAuthentication.Models;
 
namespace SampleAzureAdAuthentication.Controllers
{
    [Authorize]
    public class UserProfileController : Controller
    {
        private ApplicationDbContext db = new ApplicationDbContext();
        private string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
        private string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
        private string graphResourceID = "https://graph.windows.net";
 
        // GET: UserProfile
        public async Task<ActionResult> Index()
        {
            string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
            string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
            try
            {
                Uri servicePointUri = new Uri(graphResourceID);
                Uri serviceRoot = new Uri(servicePointUri, tenantID);
                ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(serviceRoot,
                      async () => await GetTokenForApplication());
 
                // use the token for querying the graph to get the user details
 
                var result = await activeDirectoryClient.Users
                    .Where(u => u.ObjectId.Equals(userObjectID))
                    .ExecuteAsync();
                IUser user = result.CurrentPage.ToList().First();
 
                return View(user);
            }
            catch (AdalException)
            {
                // Return to error page.
                return View("Error");
            }
            // if the above failed, the user needs to explicitly re-authenticate for the app to obtain the required token
            catch (Exception)
            {
                return View("Relogin");
            }
        }
 
        public void RefreshSession()
        {
            HttpContext.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = "/UserProfile" },
                OpenIdConnectAuthenticationDefaults.AuthenticationType);
        }
 
        public async Task<string> GetTokenForApplication()
        {
            string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
            string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
 
            // get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
            ClientCredential clientcred = new ClientCredential(clientId, appKey);
            // initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
            AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID, new ADALTokenCache(signedInUserID));
            AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
            return authenticationResult.AccessToken;
        }
    }
}

Now with all Controllers in place lets add the Views so you can see the necessary forms on your Web Application

Under Views\Shared Folder create _LoginPartial.cshtml View, you can use this partial view to add on your Layout as a link and show the Sign Out and User Details link

@if (Request.IsAuthenticated)
{
    <text>
        <ul class="nav navbar-nav navbar-right">
            <li>
                @Html.ActionLink("Hello " + User.Identity.Name + "!""Index""UserProfile", routeValues: null, htmlAttributes: null)
            </li>
            <li>
                @Html.ActionLink("Sign out""SignOut""Account")
            </li>
        </ul>
    </text>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Sign in""SignIn""Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

Under Views\Account Folder Create a SignOutCallBack.cshtml View

@{
    ViewBag.Title = "Sign Out";
}
<h2>@ViewBag.Title.</h2>
<p class="text-success">You have successfully signed out.</p>

Under Views\UserProfile Folder create Index.cshtml, this is for showing user details linked from the Partial View above

@using Microsoft.Azure.ActiveDirectory.GraphClient
@model User
 
@{
    ViewBag.Title = "User Profile";
}
<h2>@ViewBag.Title.</h2>
 
<table class="table table-bordered table-striped">
    <tr>
        <td>Display Name</td>
        <td>@Model.DisplayName</td>
    </tr>
    <tr>
        <td>First Name</td>
        <td>@Model.GivenName</td>
    </tr>
    <tr>
        <td>Last Name</td>
        <td>@Model.Surname</td>
    </tr>
</table>

In this same folder add a Relogin.cshtml View, this is to handle re-logins

@using Microsoft.Azure.ActiveDirectory.GraphClient
 
@{
    ViewBag.Title = "User Profile";
}
<h2>@ViewBag.Title.</h2>
 
<p>
    The token for accessing the Graph API has expired.
    Click @Html.ActionLink("here""RefreshSession""UserProfile"nullnull) to sign-in and get a new access token.
</p>

Now lets trigger it when your Web Application fires, in order to do that you need two classes. One Under App_Start Folder Create Startup.Auth.cs class to request a token to call the Graph API.

using System;
using System.Configuration;
using System.IdentityModel.Claims;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Owin;
using SampleAzureAdAuthentication.Models;
 
namespace SampleAzureAdAuthentication
{
    public partial class Startup
    {
        private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
        private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
        private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
        private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
 
        public static readonly string Authority = aadInstance + tenantId;
 
        // This is the resource ID of the AAD Graph API.  We'll need this to request a token to call the Graph API.
        string graphResourceId = "https://graph.windows.net";
 
        public void ConfigureAuth(IAppBuilder app)
        {
            ApplicationDbContext db = new ApplicationDbContext();
 
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
 
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
 
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = Authority,
                    PostLogoutRedirectUri = postLogoutRedirectUri,
 
                    Notifications = new OpenIdConnectAuthenticationNotifications()
                    {
                        // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
                        AuthorizationCodeReceived = (context) =>
                        {
                            var code = context.Code;
                            ClientCredential credential = new ClientCredential(clientId, appKey);
                            string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
                            AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
                            AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                            code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
 
                            return Task.FromResult(0);
                        }
                    }
                });
        }
    }
}

Now lets create a Startup.cs Class just in the Root directory, this will call the ConfigureAuth method on the class above to execute the necessary steps for Authentication

using Owin;
 
namespace SampleAzureAdAuthentication
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

Now lets use it.
First add the PartialView on necessary pages like such

@Html.Partial("_LoginPartial")

and for all the MVC Controllers and/or WebApi Controller you will need to add the [Authorize] attribute so Secure Authentication kick in on the ones with the attribute implemented, ie

Now every time you visit the site you will be asked to log in using  your AAD Credentials

Recommended

Leave a Reply