Category Archives: Asp.net

Enabling Organizational Authentication on an existing MVC project

If you have been working with MVC applications should already know that when you first create the project, you sort of have to make call on how the application is going to authenticate users, options are no authentication, Individual Accounts which essentially means users are going to be stored in a database and template auto generates all the plumbing code for authentication, user management etc. and lastly Organizational accounts. This is where you basically outsource all authentication function to external authentication system, typically a federated identity management system. But what if you started off with individual accounts and later you decide to move to organizational accounts, if you have run into this scenario its not quite easy to swap. I wish there was an easier way in VisualStudio. Goal with this post is to to document the steps you can take to swap the authentication mechanism to organizational accounts.

Nuget Packages to be Uninstalled

In package manager console run commands in the order listed below to uninstall all Owin nuget packages that was pulled down by the project template

Uninstall-Package Microsoft.Owin.Security.Twitter
Uninstall-Package Microsoft.AspNet.Identity.Owin
Uninstall-Package Microsoft.Owin.Security.OAuth
Uninstall-Package Microsoft.Owin.Security.MicrosoftAccount
Uninstall-Package Microsoft.Owin.Security.Google
Uninstall-Package Microsoft.Owin.Security.Facebook
Uninstall-Package Microsoft.Owin.Security.Cookies
Uninstall-Package Microsoft.Owin.Security
Uninstall-Package Microsoft.Owin.Host.SystemWeb
Uninstall-Package Microsoft.Owin
Uninstall-Package Owin

Since Application insights packages are not pulled down when you select organization authentication when a new ASP.NET application is created we will  go ahead and uninstall those packages as well. I did not test to see if there are any issues with enabling Application Insights on ASP.NET web application configured for Organization Authentication. May be that’s for another day Smile

In package manager console run following commands in order listed below

Uninstall-Package Microsoft.ApplicationInsights.Web
Uninstall-Package Microsoft.ApplicationInsights.Web.TelemetryChannel
Uninstall-Package Microsoft.ApplicationInsights.PerfCounterCollector
Uninstall-Package Microsoft.ApplicationInsights.JavaScript
Uninstall-Package Microsoft.ApplicationInsights.DependencyCollector
Uninstall-Package Microsoft.ApplicationInsights.Agent.Intercept
Uninstall-Package Microsoft.ApplicationInsights

Nuget Packages to be installed

Next we need to Install System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, this package adds token validation capabilities and ensures that the signer and issuer are a valid pair.

In package manager console run below command

Install-Package System.IdentityModel.Tokens.ValidatingIssuerNameRegistry

Assembly references to be added

Update Project references and add following assembly references listed below

  • System.IdentityModel
  • System.IdentityModel.Services

EntityFramework wireup to push keys and tenant IDs into database

Next we need to add entity framework classes which enables saving of keys and tenant ids into a database after token validation process is complete during the organization authentication process. Follow steps below.

  • Right click on “Models” folder and add a new class and name it “TenantRegistrationModels.cs”
  • Replace using statements in TenantRegistrationModels.cs file with below:
 using System;
 using System.Collections.Generic;
 using System.Linq;
  • Add following model classes to “TenantRegistrationModels.cs” file. You can break this into individual files if you prefer that.
public class IssuingAuthorityKey
{
	public string Id { get; set; }
}

public class Tenant
{
	public string Id { get; set; }
}
  • Right click on “Models” folder in your existing MVC project and add a new class and name it “TenantDbContext.cs”
  • Replace using statements in “TenantDbContext.cs” file with below:
 using System;
 using System.Data.Entity;
  • Replace the contents of the “TenantDbContext” class with code below
 public class TenantDbContext : DbContext
 {
         public TenantDbContext()
             : base("DefaultConnection")
         {
         }

        public DbSet IssuingAuthorityKeys { get; set; }

        public DbSet Tenants { get; set; }
 }
  • Delete files below that are stored under “Models” folder in your existing MVC project as we no longer need them since we are switching to Organization Authentication model from Individual user accounts.
    • AccountViewModels.cs
    • IdentityModels.cs
    • ManageViewModels.cs

Next we need to add a folder to existing project and name it “Utils”

Right click on this folder and add a class and name it “DatabaseIssuerNameRegistry.cs”

Replace using statements in “DatabaseIssuerNameRegistry.cs” file with following

 using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Web;
using System.Web.Hosting;
using System.Xml.Linq;

Additionally you have to include another using statement above to bring in the namespace for your “Models”.

Replace the “DatabaseIssuerNameRegistry” class with code below

 public class DatabaseIssuerNameRegistry : ValidatingIssuerNameRegistry
     {
         public static bool ContainsTenant(string tenantId)
         {
             using (TenantDbContext context = new TenantDbContext())
             {
                 return context.Tenants
                     .Where(tenant => tenant.Id == tenantId)
                     .Any();
             }
         }

        public static bool ContainsKey(string thumbprint)
         {
             using (TenantDbContext context = new TenantDbContext())
             {
                 return context.IssuingAuthorityKeys
                     .Where(key => key.Id == thumbprint)
                     .Any();
             }
         }

        public static void RefreshKeys(string metadataLocation)
         {
             IssuingAuthority issuingAuthority = ValidatingIssuerNameRegistry.GetIssuingAuthority(metadataLocation);

            bool newKeys = false;
             bool refreshTenant = false;
             foreach (string thumbprint in issuingAuthority.Thumbprints)
             {
                 if (!ContainsKey(thumbprint))
                 {
                     newKeys = true;
                     refreshTenant = true;
                     break;
                 }
             }

            foreach (string issuer in issuingAuthority.Issuers)
             {
                 if (!ContainsTenant(GetIssuerId(issuer)))
                 {
                     refreshTenant = true;
                     break;
                 }
             }

            if (newKeys || refreshTenant)
             {
                 using (TenantDbContext context = new TenantDbContext())
                 {
                     if (newKeys)
                     {
                         context.IssuingAuthorityKeys.RemoveRange(context.IssuingAuthorityKeys);
                         foreach (string thumbprint in issuingAuthority.Thumbprints)
                         {
                             context.IssuingAuthorityKeys.Add(new IssuingAuthorityKey { Id = thumbprint });
                         }
                     }

                    if (refreshTenant)
                     {
                         foreach (string issuer in issuingAuthority.Issuers)
                         {
                             string issuerId = GetIssuerId(issuer);
                             if (!ContainsTenant(issuerId))
                             {
                                 context.Tenants.Add(new Tenant { Id = issuerId });
                             }
                         }
                     }
                     context.SaveChanges();
                 }
             }
         }

        private static string GetIssuerId(string issuer)
         {
             return issuer.TrimEnd('/').Split('/').Last();
         }

        protected override bool IsThumbprintValid(string thumbprint, string issuer)
         {
             return ContainsTenant(GetIssuerId(issuer))
                 && ContainsKey(thumbprint);
         }
     }

Updates to App_Start

Make following Changes to App_Start folder

  • Delete “Startup.Auth.cs” file from App_Start folder as we no longer need to wire up authentication to Owin middleware
  • Edit the “IdentityConfig.cs” file and make changes below
    • Replace using statements with below
using System;
 using System.Collections.Generic;
 using System.Configuration;
 using System.IdentityModel.Claims;
 using System.IdentityModel.Services;
 using System.Linq;
 using System.Web.Helpers;
 using WebApplication3.Utils;
    • Replace everything inside the namespace statement with code below
public static class IdentityConfig
     {
         public static string AudienceUri { get; private set; }
         public static string Realm { get; private set; }

        public static void ConfigureIdentity()
         {
             RefreshValidationSettings();
             // Set the realm for the application
             Realm = ConfigurationManager.AppSettings["ida:realm"];

            // Set the audienceUri for the application
             AudienceUri = ConfigurationManager.AppSettings["ida:AudienceUri"];
             if (!String.IsNullOrEmpty(AudienceUri))
             {
                 UpdateAudienceUri();
             }

            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
         }

        public static void RefreshValidationSettings()
         {
             string metadataLocation = ConfigurationManager.AppSettings["ida:FederationMetadataLocation"];
             DatabaseIssuerNameRegistry.RefreshKeys(metadataLocation);
         }

        public static void UpdateAudienceUri()
         {
             int count = FederatedAuthentication.FederationConfiguration.IdentityConfiguration
                 .AudienceRestriction.AllowedAudienceUris.Count(
                     uri => String.Equals(uri.OriginalString, AudienceUri, StringComparison.OrdinalIgnoreCase));
             if (count == 0)
             {
                 FederatedAuthentication.FederationConfiguration.IdentityConfiguration
                     .AudienceRestriction.AllowedAudienceUris.Add(new Uri(IdentityConfig.AudienceUri));
             }
         }
     }

Make following changes to files in “Controllers” folder

  • Delete “ManageController.cs” file. If you want to provide user management functions you’ll probably want to keep this controller and update code to do those operations against Azure AD using Graph API

Make following changes to “Global.asax.cs” by right clicking on “Global.asax” file and selecting view code option.

  • Add below code to using statements

using System.IdentityModel.Services;

  • Update code in “Application_Start” event and add a call to ConfigureIdentity method in IdentityConfig class like below:
    • IdentityConfig.ConfigureIdentity();
  • Add a new event handler shown below to hook into RedirectingToIdentityProvider event on WSFederationAuthenticationModule
private void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
         {
             if (!String.IsNullOrEmpty(IdentityConfig.Realm))
             {
                 e.SignInRequestMessage.Realm = IdentityConfig.Realm;
             }
         }

View Updates

Add a new view under “Account” folder and name it “SignOutCallback.cshtml” and add following snippet below. You can also add your own fancy custom message you want to show when users signs out of the application.

<p class=”text-success”>You have successfully signed out.</p>

Web.Config Updates

We’ll need to apply some updates to the web.config for your existing MVC web site. Changes that needed to be made are listed below:

  • Under “configSections” element add snippet below
  • Add snippet below right under closing element for system.web

     
       
       
         
       
       
         
         
       
       
     
   
  • Add section for system.identityModel.services right under “entityFramework” element as shown below
   
     
       
       
     
   
  • Add following keys to “appSettings”

  
  
  

  • Add authorization element under system.web to deny anonymous users access. See below:
  
       
 
  • Add following snippet right after “appSettings” element
  
     
       
         
       
     
   
  • Replace contents of “modules” element under “system.webServer” element with snippet below
  
  

Additional Clean Up

You may also want to replace the contents of “_LoginPartial.cshtml” view with snippet below especially if you have the original auto generated code, you may still have requests going to “Accounts” controller methods that no longer exists or needed

@if (Request.IsAuthenticated)
{
     <text>
     <ul class=”nav navbar-nav navbar-right”>
         <li class=”navbar-text”>
             Hello, @User.Identity.Name!
         </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”, “Home”, routeValues: null, htmlAttributes: new { id = “loginLink” })</li>
     </ul>
}

If you decided to delete “ManageController” controller per earlier step, we no longer needed to keep any views that are related to account management activities, so I would recommend deleting them. Additionally all the auto generated views in Account folder could also be removed. You should just have one here which is “SignOutCallback.cshtml” that display user a message when he/she signs out of application.

Delete the “Startup.cs” file from existing MVC project.

Add the Application to Azure AD

Lastly add the application in Azure Active Directory. I won’t go into the details about that as you might already be familiar with it.

Last thing I want to mention is if you started of with “No Authentication” option steps should be 80-90% same as above, couple of additional stuff you’ll need to do is bringing in EntityFramework, Microsoft.AspNet.Identity.Core and Microsoft.AspNet.Identity.EntityFramework packages

Hope this helps.

Cheers,

</Ram>