Sunday, 29 January 2017

Setting Up Azure AD Manually

Hi, There


Today I am gonna show you how you can setup Azure Active Directory Services manually.


What is Azure AD?


This is a directory service in which only the user inside a directory can login to your website, other users can't access your website. This is simply can be said as login via Azure or Microsoft account.


Things you should know already?


1. C# (Because this tutorial is using C#, otherwise this can be done on other         languages too).
2. ASP.Net MVC Framework


Things you need:


1. Microsoft Visual Studio (Greater than 2013 recommended)
2. A Microsoft Azure ID
3. Working internet connection


I have divided this tutorial in 11 steps.

Be patient while doing this because this process will take time.


Step 1: Setting Up SSL URL

  • Open any desired ASP.Net MVC Web Application Project in Visual Studio.
  • Select Project in solution explorer.
  • Press F4 this will open basic project properties.




  • Set SSL Enabled to True.


  • Copy Auto-Generated SSL URL.


  • Right-Click on the project folder and select Properties.


  • Select Web => Project URL.
  • Paste the Copied SSL URL in this field.



Step 2: App Registration

  • Open Azure Portal.
  • Open Azure Active Directory => App Registrations => Add New

  • Enter any desired app name.
  • Paste the Copied SSL URL from Step 1.
  • Click Create.



Step 3: Getting Client Secret

  • Go to new app Settings => Keys.



  • Enter any Name and Select Duration and Do not enter anything inside value.
  • Click Save.


  • Copy the value appeared under Value at the moment otherwise it will get disappeared. This is called as Client Secret.




Step 4: Install NuGet Packages

Install the following NuGet packages from top to bottom:-

  • Microsoft.IdentityModel.Clients.ActiveDirectory (Only This Version: 2.24.xxxxxxxxx)
  • Microsoft.Owin.Security
  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Security.OpenIdConnect
  • Kentor.OwinCookieSaver
  • EntityFramework
  • Microsoft.Azure.ActiveDirectory.GraphClient
  • Microsoft.Owin.Host.SystemWeb


Step 5: Setting Up Configurations

  • Open web.config of you web app.
  • Navigate to appSettings tag.
  • Add the following keys and values:-
  • <add key="ida:PostLogoutRedirectUri" value="Copied SSL URL"/>
  • <add key="ida:ClientId" value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/>

  • <add key="ida:AADInstance" value="https://login.microsoftonline.com/"/>
  • <add key="ida:ClientSecret" value="Copied Client Secret from Step 3"/>
  • <add key="ida:Domain" value="yourcompany.onmicrosoft.com"/>

  • <add key="ida:TenantId" value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/>



Your updated web.config should have this structure:-




Step 6: Setting Up Database for Storing Authorization Tokens


  • Create a database for storing authorization tokens and cookies.
  • Copy the connection string for this database to web.config file.
  • Name the connection string as DefaultConnection.
  • Don't forget to mention providerName property in connectionstring as System.Data.SqlClient.



Step 7: Creating and Copying Necessary Files


Create the following files and copy the content provided (Note: Don't forget to change the namespaces according to your project and do not change anything instead of namespaces inside the files): 

  • In application root create file Startup.cs

using Microsoft.Owin;

using Owin;



[assembly: OwinStartupAttribute(typeof(yournamespace.Startup))]
namespace yournamespace

{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

  • In application root/App_Start folder create file Startup.Auth.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Claims;
using System.Linq;
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 yournamespace.Models;

namespace yournamespace
{
    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.UseKentorOwinCookieSaver();
            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);
                        }
                    }
                });
        }
    }
}

  • In application root/Models folder create file AdalTokenCache.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Security;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace yournamespace.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)
            {
                Cache = new UserTokenCache
                {
                    webUserUniqueId = userId,
                    cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache"),
                    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);
        }
    }
}

  • In application root/Models folder create file ApplicationDbContext.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace yournamespace.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }

        public DbSet<UserTokenCache> UserTokenCacheList { get; set; }
    }

    public class UserTokenCache
    {
        [Key]
        public int UserTokenCacheId { get; set; }
        public string webUserUniqueId { get; set; }
        public byte[] cacheBits { get; set; }
        public DateTime LastWrite { get; set; }
    }
}


Step 8:Authorization and Getting Credentials

  • Any method, action or class on which you want authorization decorate it with the [Authorize] attribute.
  • For getting credentials you need to again copy some code:-
[Authorize]
public class HomeController : Controller
{
int i = 0;
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";

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;
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(); i++;
IUser user = result.CurrentPage.ToList().First();


  • The IUser interface contains all the data that you need like User name of the current user, Name of the logged in user and other data. Remember it doesn't contain password.

Step 9: Logging Out


  • Copy the following code for logging out the user: 

[Authorize]
    public class LogoutController : Controller
    {
        // GET: Logout
        public void Index()
        {
Session.RemoveAll();
//return RedirectToAction("Index", "Home");
string callbackUrl = Url.Action("SignOutCallback", "Logout", 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 RedirectToAction("Index", "Account");
}
}


Step 10: Uploading App to Azure Portal or any other Cloud: Required Changes: 3


  • Open web.config => Replace the ida:PostLogoutRedirectUri value with your azurewebsites or cloud URL (Keep it as Https).
  • Open Azure Portal =>  Azure Active Directory => App Registrations => Your App => Manifest =>  Edit 


  • Change homepage url with your azurewebsites URL.


  • Change replyUrls with your azurewebsites URL.



Step 11: Removing Known Errors


  • Problem: Infinite Redirects
       Cause :Cookie expiration after logging in into microsoft account
          Redirection to http instead of https

       Remedy: add app.UseKentorOwinCookieSaver();
             Inside root/App_Start/Startup.Auth as shown in screenshot



  => In web.config ==> System.webServer Tag copy
<rewrite>
      <rules>
        <clear />
        <rule name="Redirect to https" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" redirectType="Permanent" appendQueryString="false" />
        </rule>
      </rules>
    </rewrite>


  • Don't forget to change the database connectionstring from local to global database otherwise it will produce an SQL Server instance not found error.
  • If you are getting RequiredNonce error the Clear UserTokenCaches table data created in your database .


You can also watch my video tutorial on YouTube here



I think I have covered everything that can be used while Setting up Azure AD.



If you have any doubts feel free to post in comment.

I will be more than happy to reply.

Thanks for reading the article. 
If you like the article please share.