Using OpenID to authenticate in MVC via Azure AD (Manual Steps)

Title says it all, we have some MVC apps using Azure AD via WSFed and want to convert using OpenID auth. While WSFED works well, we wanted to take a simple approach of using OpenID through Azure AD. These are the steps to either convert from WSFED or add OpenID in existing MVC Apps for Authentication.

I assume that you already have an application registered in Azure Active Directory for your website to use for authenticating AD users. If not, the first step is to create an Application in Azure Active Directory for your website to use to authenticate AD users. To do this:

    1. Sign in to the Azure Management Portal (http://azure.microsoft.com).
    2. Click on the Active Directory icon on the left menu, and then click on the desired directory.
    3. On the top menu, click Applications. If no apps have been added to your directory, this page will only show the Add an App link. Click on the link, or alternatively you can click on the Add button on the command bar.
    4. On the What do you want to do page, click on the link to Add an application my organization is developing.
  • On the Tell us about your application page, you must specify a name for your application as well as indicate the type of application you are registering with Azure AD. You can choose from a web application and/or web API (default) or native client application which represents an application that is installed on a device such as a phone or computer. For this guide, make sure to select Web Application and/or Web API
  1. Once finished, click the arrow icon on the bottom-right corner of the page.
  2. On the App properties page, provide the Sign-on URL (URL for your web application) and App ID URI (Unique URI for your application – Usually it’s a combination or your AD domain/application. Example: http://www.domain.com/mywebsite.somedomain.com) for your web application then click the checkbox in the bottom-right hand corner of the page.
  3. Your application has been added, and you will be taken to the Quick Start page for your application.
  4. Click on the “Configure” Tab. Generate a Key for your client access and write down the following information:
      1. CLIENT ID:
      2. KEY (You generate a Key by clicking on the Save Button on the configure tab)
      3. APP ID URI

     

  5. Federation Metadata Document (You can get this information by click on “VIEW ENDPOINTS” at the bottom section of the Configure tab)

 Capture

Enable SSL on your Dev Machines

With OpenID, you need to have your MVC app enabled with SSL. In your development environment, you can set this by going to the properties of the MVC app, select “Web” on the left navigation and type “https” on the project URL box:

SSL

Add OpenID and OWIN nuget packages to your MVC Application:

  • Microsoft.IdentityModel.Protocol.Extensions
  • System.IdentityModel.Tokens.Jwt
  • Microsoft.Owin.Security.OpenIdConnect
  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Host.SystemWeb
  • Active Directory Authentication Library

Create a class Startup.Auth.cs in the App_Start folder

Replace the code from below: Be sure to take the whole class definition!

Namespace references:


using Microsoft.IdentityModel.Clients.ActiveDirectory;

using Microsoft.Owin.Security;

using Microsoft.Owin.Security.Cookies;

using Microsoft.Owin.Security.OpenIdConnect;

using Owin;


public partial class Startup

   {

       //

       // The Client ID is used by the application to uniquely identify itself to Azure AD.

       // The App Key is a credential used to authenticate the application to Azure AD. Azure AD supports password and certificate credentials.

       // The Metadata Address is used by the application to retrieve the signing keys used by Azure AD.

       // The AAD Instance is the instance of Azure, for example public Azure or Azure China.

       // The Authority is the sign-in URL of the tenant.

       // The Post Logout Redirect Uri is the URL where the user will be redirected after they sign out.

       //

       private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];

       private static string appKey = ConfigurationManager.AppSettings["ida:AppKey"];

       private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];

       private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];

       private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];

       public static readonly string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

       // 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 = ConfigurationManager.AppSettings["ida:GraphUrl"];

       public void ConfigureAuth(IAppBuilder app)

       {

           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 userObjectID = context.AuthenticationTicket.Identity.FindFirst(

                                   "http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

                           AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));

                           AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(

                              code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);

                           AuthenticationHelper.token = result.AccessToken;

                           return Task.FromResult(0);

                       }

                   }

               });

       }

   }

Create Utility classes

In the project, create a new folder called Utils, create a class AuthenticationHelper.cs. Replace the code from below. Be sure to take the whole class definition!

References


using Microsoft.Azure.ActiveDirectory.GraphClient;


internal class AuthenticationHelper

   {

       public static string token;

       /// <summary>

       ///     Async task to acquire token for Application.

       /// </summary>

       /// <returns>Async Token for application.</returns>

       public static async Task<string> AcquireTokenAsync()

       {

           if (token == null || token.IsEmpty())

           {

               throw new Exception("Authorization Required.");

           }

           return token;

       }

       /// <summary>

       ///     Get Active Directory Client for Application.

       /// </summary>

       /// <returns>ActiveDirectoryClient for Application.</returns>

       public static ActiveDirectoryClient GetActiveDirectoryClient()

       {

           Uri baseServiceUri = new Uri(Constants.ResourceUrl);

           ActiveDirectoryClient activeDirectoryClient =

               new ActiveDirectoryClient(new Uri(baseServiceUri, Constants.TenantId),

                   async () => await AcquireTokenAsync());

           return activeDirectoryClient;

       }

   }

In the Utils folder, create a class Constants.cs. Replace the code from below. Be sure to take the whole class definition!


internal class Constants

   {

       public static string ResourceUrl = ConfigurationManager.AppSettings["ida:GraphUrl"];

       public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];

       public static string AppKey = ConfigurationManager.AppSettings["ida:AppKey"];

       public static string TenantId = ConfigurationManager.AppSettings["ida:TenantId"];

       public static string AuthString = ConfigurationManager.AppSettings["ida:Auth"] +

                                         ConfigurationManager.AppSettings["ida:Tenant"];

       public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];

   }

In the Utils folder, create three new classes called NaiveSessionCache.cs. Replace the code from below. Be sure to take the whole class definition!

References:


using Microsoft.IdentityModel.Clients.ActiveDirectory;


public class NaiveSessionCache : TokenCache

   {

       private static readonly object FileLock = new object();

       private readonly string CacheId = string.Empty;

       private string UserObjectId = string.Empty;

       public NaiveSessionCache(string userId)

       {

           UserObjectId = userId;

           CacheId = UserObjectId + "_TokenCache";

           AfterAccess = AfterAccessNotification;

           BeforeAccess = BeforeAccessNotification;

           Load();

       }

       public void Load()

       {

           lock (FileLock)

           {

               if (HttpContext.Current != null)

               {

                   Deserialize((byte[])HttpContext.Current.Session[CacheId]);

               }

           }

       }

        public void Persist()

       {

           lock (FileLock)

           {

               // reflect changes in the persistent store

               HttpContext.Current.Session[CacheId] = Serialize();

               // once the write operation took place, restore the HasStateChanged bit to false

               HasStateChanged = false;

           }

       }

       // Empties the persistent store.

       public override void Clear()

       {

           base.Clear();

           HttpContext.Current.Session.Remove(CacheId);

       }

       public override void DeleteItem(TokenCacheItem item)

       {

           base.DeleteItem(item);

           Persist();

       }

       // Triggered right before ADAL needs to access the cache.

       // Reload the cache from the persistent store in case it changed since the last access.

       private void BeforeAccessNotification(TokenCacheNotificationArgs args)

       {

           Load();

       }

       // Triggered right after ADAL accessed the cache.

       private void AfterAccessNotification(TokenCacheNotificationArgs args)

       {

           // if the access operation resulted in a cache update

           if (HasStateChanged)

           {

               Persist();

           }

       }

   }

Add OWIN Startup class 

Right-click on the project, select Add, select “OWIN Startup class”, and name the class “Startup”. If “OWIN Startup Class” doesn’t appear in the menu, instead select “Class”, and in the search box enter “OWIN”. “OWIN Startup class” will appear as a selection; select it, and name the class Startup.cs .

In Startup.cs , replace the code from below. Again, note the definition changes from public class Startup to public partial class Startup .


using System;

using System.Threading.Tasks;

using Microsoft.Owin;

using Owin;

[assembly: OwinStartup(typeof(MVCProject.Startup))]

namespace MVCProject

{

   public partial class Startup

   {

       public void Configuration(IAppBuilder app)

       {

           ConfigureAuth(app);

       }

   }

}

Create UserProfile model

In the Models folder add a new class called UserProfile.cs . Copy the implementation of UserProfile from below:


public class UserProfile

   {

       public string DisplayName { get; set; }

       public string GivenName { get; set; }

       public string Surname { get; set; }

   }

Create new UserProfileController

Add a new empty MVC5 controller UserProfileController to the project. Copy the implementation from below. Remember to include the [Authorize] attribute on the class definition.

References:


using System.Net.Http;

using System.Net.Http.Headers;

using System.Security.Claims;

using System.Threading.Tasks;

using System.Web;

using System.Web.Mvc;

using Microsoft.IdentityModel.Clients.ActiveDirectory;

using Microsoft.Owin.Security.OpenIdConnect;

using Newtonsoft.Json;


[Authorize]

   public class UserProfileController : Controller

   {

       private const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid";

       private static readonly string clientId = ConfigurationManager.AppSettings["ida:ClientId"];

       private static readonly string appKey = ConfigurationManager.AppSettings["ida:AppKey"];

       private readonly string graphResourceId = ConfigurationManager.AppSettings["ida:GraphUrl"];

       private readonly string graphUserUrl = "https://graph.windows.net/{0}/me?api-version=" +

                                              ConfigurationManager.AppSettings["ida:GraphApiVersion"];

       //

       // GET: /UserProfile/

       public async Task<ActionResult> Index()

       {

           //

           // Retrieve the user's name, tenantID, and access token since they are parameters used to query the Graph API.

           //

           UserProfile profile;

           string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value;

            AuthenticationResult result = null;

           try

           {

               // Get the access token from the cache

               string userObjectID =

                   ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")

                       .Value;

               AuthenticationContext authContext = new AuthenticationContext(Startup.Authority,

                   new NaiveSessionCache(userObjectID));

               ClientCredential credential = new ClientCredential(clientId, appKey);

               result = authContext.AcquireTokenSilent(graphResourceId, credential,

                   new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));

               // Call the Graph API manually and retrieve the user's profile.

               string requestUrl = String.Format(

                   CultureInfo.InvariantCulture,

                   graphUserUrl,

                   HttpUtility.UrlEncode(tenantId));

               HttpClient client = new HttpClient();

               HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);

               request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

               HttpResponseMessage response = await client.SendAsync(request);

               // Return the user's profile in the view.

               if (response.IsSuccessStatusCode)

               {

                   string responseString = await response.Content.ReadAsStringAsync();

                   profile = JsonConvert.DeserializeObject<UserProfile>(responseString);

               }

               else

               {

                   // If the call failed, then drop the current access token and show the user an error indicating they might need to sign-in again.

                   authContext.TokenCache.Clear();

                   profile = new UserProfile();

                   profile.DisplayName = " ";

                   profile.GivenName = " ";

                   profile.Surname = " ";

                   ViewBag.ErrorMessage = "UnexpectedError";

               }

           }

           catch (Exception e)

           {

               if (Request.QueryString["reauth"] == "True")

              {

                   //

                   // Send an OpenID Connect sign-in request to get a new set of tokens.

                   // If the user still has a valid session with Azure AD, they will not be prompted for their credentials.

                   // The OpenID Connect middleware will return to this controller after the sign-in response has been handled.

                   //

                   HttpContext.GetOwinContext()

                       .Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType);

               }

               //

               // The user needs to re-authorize. Show them a message to that effect.

               //

               profile = new UserProfile();

              profile.DisplayName = " ";

               profile.GivenName = " ";

               profile.Surname = " ";

               ViewBag.ErrorMessage = "AuthorizationRequired";

           }

           return View(profile);

       }

   }

Create new AccountController

Add a new empty MVC5 controller AccountController to the project. Copy the implementation from below.

References:


using System.Security.Claims;

using Microsoft.IdentityModel.Clients.ActiveDirectory;

using Microsoft.Owin.Security;

using Microsoft.Owin.Security.Cookies;

using Microsoft.Owin.Security.OpenIdConnect;

using QualityEngineeringSite.Utils;


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()

       {

           // Remove all cache entries for this user and send an OpenID Connect sign-out request.

           string userObjectID =

               ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

           AuthenticationContext authContext = new AuthenticationContext(Startup.Authority,

               new NaiveSessionCache(userObjectID));

           authContext.TokenCache.Clear();

           AuthenticationHelper.token = null;

           HttpContext.GetOwinContext().Authentication.SignOut(

               OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);

       }

   }

Create a new partial view _LoginPartial.cshtml 

In the Views –> Shared folder, create a new partial view _LoginPartial.cshtml. Replace the contents of the file from below


@using System

@{

   var user = "Null User";

   if (!String.IsNullOrEmpty(User.Identity.Name))

   {

       user = User.Identity.Name;

   }

}

@if (Request.IsAuthenticated)

{

   <text>

       <ul class="nav navbar-nav navbar-right">

           <li>

               @Html.ActionLink(user, "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", "Index", "UserProfile", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>

   </ul>

}

Modify existing _Layout.cshtml

In the Views –> Shared folder, add a single line, @Html.Partial(“_LoginPartial”) , that lights up the previously added _LoginPartial view. See screenshot below

Authenticate Users

If you want the user to be required to sign-in before they can see any page of the app, then in the HomeController, decorate the HomeController class with the [Authorize] attribute. If you leave this out, the user will be able to see the home page of the app without having to sign-in first, and can click the sign-in link on that page to get signed in.

For more information around the AuthorizeAttribute, refer to:

AuthorizeAttribute Class

https://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute(v=vs.118).aspx

Web.Config Settings

In web.config , in <appSettings> , create keys for ida:ClientId , ida:AppKey , ida:AADInstance , ida:Tenant and ida:PostLogoutRedirectUri and set the values accordingly. For the public Azure AD, the value of ida:AADInstance is https://login.windows.net/{0} . See sample below:


&lt;!-- Values for OpenID and Graph API --&gt;

&lt;!-- ClientId is the application ID from your own Azure AD tenant --&gt;

   &lt;add key=&quot;ida:ClientId&quot; value=&quot;XXXXXXX&quot; /&gt;

   &lt;add key=&quot;ida:AppKey&quot; value=&quot;XXXXX&quot; /&gt;

   &lt;add key=&quot;ida:AADInstance&quot; value=&quot;https://login.windows.net/{0}&quot; /&gt;

&lt;!-- Tenant is the Tenant ID from your own Azure AD tenant. This is in a form of GUID.This is the value from your Federation Metadata Document URL' --&gt;

   &lt;add key=&quot;ida:Tenant&quot; value=&quot;XXXXXXXXX&quot; /&gt;

&lt;!-- Tenant is the Tenant ID from your own Azure AD tenant. This is in a form of GUID.This is the value from your Federation Metadata Document URL' --&gt;

   &lt;add key=&quot;ida:TenantId&quot; value=&quot;XXXXXXXX&quot; /&gt;

&lt;!-- PostLogoutRedirectUri is your application endpoint --&gt;

   &lt;add key=&quot;ida:PostLogoutRedirectUri&quot; value=&quot;http://xxxx.azurewebsites.net/&quot; /&gt;

   &lt;add key=&quot;aspnet:UseTaskFriendlySynchronizationContext&quot; value=&quot;true&quot; /&gt;

In web.config add this line in the <system.web> section: <sessionState timeout=”525600″ /> . This increases the ASP.Net session state timeout to its maximum value so that access tokens and refresh tokens cache in session state aren’t cleared after the default timeout of 20 minutes.

33 thoughts on “Using OpenID to authenticate in MVC via Azure AD (Manual Steps)”

  1. Just wish to say your article is as astonishing. The clearness in your post is
    just excellent and i can assume you are an expert on this subject.

    Fine with your permission allow me to grab your RSS
    feed to keep updated with forthcoming post. Thanks a million and please continue the enjoyable work.

    Like

  2. This is really fascinating, You’re an excessively professional blogger.
    I’ve joined your feed and look ahead to in search of
    extra of your fantastic post. Also, I have shared your website in my social networks

    Like

  3. Your style is really unique in comparison too other
    people I’ve read stuff from. Many thanks for postfing when you’ve got tthe opportunity, Guess I will just book
    mark this blog.

    Like

  4. Hey there, I think your website might be having browser compatibility issues.
    When I look at your website in Opera, it looks fine but when opening in Internet Explorer, it has
    some overlapping. I just wanted to give you a quick heads
    up! Other then that, wonderful blog!

    Like

  5. Hello, i believe that i saw you visited my blog thus
    i got here to return the desire?.I’m attempting
    to to find things to improve my web site!I assume its ok to make use of some of your concepts!!

    Like

  6. I have actually been checking out stuff that is associated to innovation considering that 2008 and I
    truly enjoy this profession. Now I’m collecting different sources of tech related news which I can compile and share to everyone that have excellent interest with the most current
    trends, gadgets, gadgets, and innovation.

    Have a good time and have a terrific day my buddy.

    Like

  7. Good post. I learn something new and challenging on websites
    I stumbleupon every day. It’s always exciting to read
    through articles from other authors and use a little something from other websites.

    Like

  8. Simply want to say your article is as amazing. The clarity in your post
    is just spectacular and i can assume you’re an expert
    on this subject. Well with your permission allow me to grab your RSS feed to keep updated with forthcoming
    post. Thanks a million and please carry on the rewarding work.

    Like

  9. I do not even know how I ended up here, but I thought this post was great.
    I do not know who you are but definitely you’re going to a famous blogger if you aren’t already 😉 Cheers!

    Like

  10. Have you ever considered about adding a little bit more than just
    your articles? I mean, what you say is fundamental and all.
    Nevertheless think about if you added some great images or videos to give your
    posts more, “pop”! Your content is excellent but with pics and
    clips, this site could definitely be one of the best in its niche.
    Good blog!

    Like

  11. Fascinating blog! Is your theme custom made or did you download
    it from somewhere? A theme like yours with a few simple adjustements would really make my
    blog stand out. Please let me know where you got your theme.
    Kudos

    Like

  12. Hi there! I just wanted to ask if you ever
    have any trouble with hackers? My last blog (wordpress) was hacked
    and I ended up losing several weeks of hard work due to no backup.

    Do you have any solutions to prevent hackers?

    Like

  13. Thаnk yoᥙ for shаring your info. ӏ really appreciate your effortѕ
    and Ⅰ will be waiting for your next pߋst thank
    yoᥙ once again.

    Like

  14. I’m curious to find out what blog system you’re utilizing?
    I’m experiencing some minor security issues with my latest
    blog and I’d like to find something more safe. Do you have any suggestions?

    Like

  15. Greetings from California! I’m bored to death at work so I decided to
    check out your blog on my iphone during lunch break. I
    enjoy the information you present here and can’t wait to take a
    look when I get home. I’m surprised at how quick your blog loaded on my cell
    phone .. I’m not even using WIFI, just 3G .. Anyways, very good
    blog!

    Like

  16. Heya! I understand this is somewhat off-topic but I had to ask.
    Does building a well-established blog like yours take a massive amount work?
    I’m completely new to writing a blog however I do write
    in my diary every day. I’d like to start a blog so I will be able to
    share my personal experience and feelings online.
    Please let me know if you have any ideas or tips for new aspiring bloggers.
    Thankyou!

    Like

    1. Not really and it all depends with the wealth of content and knowledge you want to share. I focus more on the quality of the content and not just the number of posts 🙂

      Like

  17. Hello this is somewhat of off topic but I was wondering if blogs use
    WYSIWYG editors or if you have to manually code with HTML.

    I’m starting a blog soon but have no coding know-how so I wanted to get
    advice from someone with experience. Any help would be enormously appreciated!

    Like

  18. Hello would you mind stating which blog platform you’re working with?
    I’m going to start my own blog in the near future but I’m having a hard time deciding between BlogEngine/Wordpress/B2evolution and Drupal.
    The reason I ask is because your design seems different
    then most blogs and I’m looking for something completely unique.
    P.S Sorry for getting off-topic but I had to ask!

    Like

  19. Nоrmally I dⲟ not read post on blogs, however I wοuld like to say thɑt this
    write-up very forϲed me to check out and do it!
    Your writing taste haѕ been amaᴢed me. Thanks, quite
    nice article.

    Like

Leave a comment