How to Authenticate and Authorize ASP.NET MVC Applications Using Microsoft Entra ID

How to Authenticate and Authorize ASP.NET MVC Applications Using Microsoft Entra ID

Secure Your ASP.NET MVC Application with Azure Active Directory

·

14 min read

In this blog, I'll guide you on how to connect an ASP.NET MVC web application with Microsoft Entra ID (Azure Active Directory is now Microsoft Entra ID) for login and access control. As a developer, I've found that using Azure Active Directory (Azure AD) with ASP.NET MVC5 apps is a strong way to manage user identities and permissions. There are many guides on this topic, but I'll share my own experiences and tips to help you through this integration easily.

When connecting Microsoft Entra ID (formerly Azure Active Directory) with your ASP.NET MVC application, it's important to know how authentication and authorization work. This process makes sure that only users who are logged in and have permission can use your app and its resources. Here's a simple explanation of how it works:

🤔What is Microsoft Entra ID (formerly Azure Active Directory)?

Microsoft Entra ID is Microsoft's cloud service for managing identities and access, helping organizations manage user identities and control access to apps and resources safely. Azure AD is commonly used for Single Sign-On (SSO), multifactor authentication, and role-based access control (RBAC).

Microsoft Entra ID offers a range of benefits, including: 🔝

  • Single Sign-On (SSO): Users can access multiple applications with a single set of credentials.

  • Multi-Factor Authentication (MFA): Enhanced security by requiring multiple forms of verification.

  • Conditional Access: Policies to control access based on user location, device, and other factors.

  • Integration with Microsoft Services: Seamless integration with Office 365, Azure, and other Microsoft services.

  • Cost-Effective: Reduces infrastructure costs associated with on-premises identity solutions

  • Centralized Management: Allows IT administrators to manage user identities and permissions from a centralized location.

  • Scalability: Supports both cloud and hybrid environments, ensuring flexibility for growing organizations.

  • Integration with Microsoft Services: Works seamlessly with Office 365, Microsoft Teams, and other Microsoft services.

🏗️Creating an Azure AD App

Before integrating with your MVC5 application, you need to register your app in Azure AD. Here's a high-level overview: 🔝

  1. Sign in to the Azure portal.

  2. Navigate to Azure Active Directory.

  3. Select "App registrations" and click "New registration".

  4. Provide a name for your application and configure the redirect URI.

  5. Note down the Application (client) ID and Directory (tenant) ID for later use.

®️Steps to Create and Register an Azure AD App

Before we integrate Azure AD with the ASP.NET MVC5 application, we need to create and register the application in Azure AD. Here’s how you can do it:

1. Sign in to Azure Portal

  1. Navigate to Azure Portal. 🔝

  2. Sign in with your Azure account credentials.

2. Create an Azure AD App Registration

  1. In the Azure Portal, go to Azure Active Directory > App registrations > New registration.

  2. Enter a Name for your application (e.g., "Contoso App Integration").

  3. Choose Accounts in this organizational directory only for the supported account types.

  4. Add a Redirect URI for your app (e.g., https://www.contoso.com/signin-oidc If you are testing on a local setup, the URL can be https://localhost:44300/signin-oidc).

  5. Click Register.

3. Note Down Application Details

Once the app is registered, note the following details: 🔝

  • Application (client) ID

  • Directory (tenant) ID

4. Configure Authentication

  1. Under the app registration, go to Authentication.

  2. Add a platform and select Web.

  3. Enter the Redirect URI (e.g., https://www.contoso.com/signin-oidc If you are testing on a local setup, the URL can be https://localhost:44300/signin-oidc).

  4. Enable the option ID tokens and save changes.

5. Create a Client Secret

  1. Go to Certificates & secrets > New client secret.

  2. Add a description (e.g., "Contoso Integration Secret") and set an expiry period.

  3. Click Add and copy the generated secret value. Store it securely as you won’t see it again.

6. Assign API Permissions

  1. Go to API Permissions > Add a permission.

  2. Select Microsoft Graph > Delegated permissions.

  3. Add openid, profile, and email permissions. 🔝

  4. Add API permission in Azure AD application for Application type Directory.Read.All, Directory.ReadWrite.All

  5. Grant admin consent for your organization.

Now your Azure AD application is ready for integration.

➕Integrating Azure AD with ASP.NET MVC

Once the Azure AD app is set up, follow these steps to integrate it with your ASP.NET MVC5 application.

1. Install Required NuGet Packages

Install the following NuGet packages: 🔝

Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.IdentityModel.Abstractions
Install-Package Microsoft.IdentityModel.JsonWebTokens
Install-Package Microsoft.IdentityModel.Logging
Install-Package Microsoft.IdentityModel.Protocols
Install-Package Microsoft.IdentityModel.Protocols.OpenIdConnect
Install-Package Microsoft.IdentityModel.Tokens
Install-Package Microsoft.Owin
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.Owin.Security
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Owin
Install-Package Microsoft.Identity.Client

You can view all the assembly details with their versions here.

2. Add Web.config Authorization Settings

Add the necessary Azure AD settings to your Web.config file to connect with Azure AD and apply the changes:

<!-- =>  File: Contoso.AzureAD/Contoso.AzureAD/Web.config --> 
<appSettings>
    <!--Azure AD Settings-->
    <add key="AAD-ClientId" value="ClientId"/>
    <add key="AAD-TenantId" value="TenantId"/>
    <add key="AAD-PostLogoutRedirectUriComplete" value="https://[Host Name]/signin-oidc"/>
    <add key="AAD-AuthorityInstance" value="https://login.microsoftonline.com/"/>
    <add key="AAD-AppScopes" value="openid email profile offline_access"/>
    <add key="AAD-MSGrapshScopes" value="User.Read Calendars.Read"/>
  </appSettings>

3. Configure the OWIN Middleware

a) Create the the Startup.Auth.cs file at root of your application. 🔝

The Startup.Auth.cs class in ASP.NET applications is essential for configuring authentication and authorization services. Its key responsibilities include:

  • Configuring Authentication Middleware: Sets up middleware for OAuth, OpenID Connect, or cookie-based authentication to manage user sign-ins and sign-outs.

  • Defining Authentication Options: Specifies settings like token validation parameters, client IDs, secrets, and callback URLs.

  • Registering Authentication Providers: Integrates external providers (e.g., Google, Facebook, Microsoft) and configures their settings.

  • Enabling Application Cookies: Uses cookies to store user information and manage sessions.

  • Configuring Authorization Policies: Establishes policies and requirements for access control within the application.

The Startup.Auth.cs class: 🔝

// => File: Contoso.AzureAD/Contoso.AzureAD/Startup.Auth.cs
using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Configuration;
using Microsoft.Owin.Security.Notifications;
using System.Threading.Tasks;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.Web;
using System;
using Microsoft.Identity.Client;

namespace Contoso.AzureAD
{
    public partial class Startup
    {
        // Get the values from the web.config file
        private static string tenantId = ConfigurationManager.AppSettings["AAD-TenantId"];
        private static string clientId = ConfigurationManager.AppSettings["AAD-ClientId"];
        private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["AAD-PostLogoutRedirectUriComplete"];
        private static string aadInstance = ConfigurationManager.AppSettings["AAD-AuthorityInstance"];
        private static string authority = aadInstance + tenantId + "/v2.0";
        private static string aadScopes = ConfigurationManager.AppSettings["AAD-AppScopes"];
        private static string msGraphScope = ConfigurationManager.AppSettings["AAD-MSGrapshScopes"];
        private static string redirectUri = postLogoutRedirectUri;

        /// <summary>
        /// This method is called to configure the authentication process.
        /// </summary>
        /// <param name="app"></param>
        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = authority,
                    Scope = $"{aadScopes} {msGraphScope}",
                    RedirectUri = redirectUri,
                    PostLogoutRedirectUri = postLogoutRedirectUri,
                    ResponseType = OpenIdConnectResponseType.CodeIdToken,
                    TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = false
                    },
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        SecurityTokenValidated = (context) =>
                        {
                            // Get the user's email from claims
                            string email = context.AuthenticationTicket.Identity.FindFirst("preferred_username").Value;

                            // Get the user's name from claims
                            string name = context.AuthenticationTicket.Identity.FindFirst("name").Value;

                            // Add the value to the name claim
                            context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, name + "(" + email + ")!", string.Empty));

                            return System.Threading.Tasks.Task.FromResult(0);
                        },
                        AuthenticationFailed = OnAuthenticationFailedAsync,
                        AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
                    }
                });

        }


        /// <summary>
        /// This method is called if the OpenIdConnect authentication process fails.
        /// </summary>
        /// <param name="notification"></param>
        /// <returns></returns>
        private static Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage,
            OpenIdConnectAuthenticationOptions> notification)
        {
            notification.HandleResponse();
            string redirect = $"/Home/Error?message={notification.Exception.Message}";
            if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
            {
                redirect += $"&debug={notification.ProtocolMessage.ErrorDescription}";
            }
            notification.Response.Redirect(redirect);
            return Task.FromResult(0);
        }

        /// <summary>
        /// This method is called when the authorization code is received.
        /// </summary>
        /// <param name="notification"></param>
        /// <returns></returns>
        private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
        {
            notification.HandleCodeRedemption();

            var httpContext = notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase;

            try
            {
                // You can write logic here to decide who can sign in based on the groups assigned to the user.
                bool isAuthorized = true;
                if (httpContext != null)
                {
                    httpContext.Session["IsAuthorized"] = isAuthorized;
                }

                if (!isAuthorized)
                {
                    throw new UnauthorizedAccessException("You are not part of the required group.");
                }
                notification.HandleCodeRedemption(null);
            }
            catch (MsalException ex)
            {
                string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
                notification.HandleResponse();
                notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
            }
            catch (UnauthorizedAccessException ex)
            {
                // Log the exception
                // Redirect to the error page with a custom message
                notification.HandleResponse();
                notification.OwinContext.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);

                var urlHelper = new System.Web.Mvc.UrlHelper(httpContext.Request.RequestContext);
                string callbackUrl = urlHelper.Action("Error", "Home", new { message = ex.Message }, httpContext.Request.Url.Scheme);

                notification.Response.Redirect(callbackUrl);
                return;
            }
        }
    }
}

b) Create the Startup.cs file at the root of your application to set up the OWIN pipeline.

The Startup.cs class: 🔝

// => File: Contoso.AzureAD/Contoso.AzureAD/Startup.cs
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(Contoso.AzureAD.Startup))]
namespace Contoso.AzureAD
{
    public partial class Startup
    {
        /// <summary>
        /// This method is called to configure the authentication process.
        /// </summary>
        /// <param name="app"></param>
        public void Configuration(IAppBuilder app)
        {
            // Configure the OWIN pipeline to use cookie auth.
            ConfigureAuth(app);
        }

    }
}

Strategies to Secure Your ASP.NET Controller

Use the [Authorize] attribute to secure your controllers or actions, and to allow anonymous access, decorate your controller or actions with [AllowAnonymous].

// => File: Contoso.AzureAD/Contoso.AzureAD/Controllers/HomeController.cs
[Authorize]
public class HomeController : Controller
{
        public ActionResult Index()
        {
            return View();
        }

        [AllowAnonymous]
        public ActionResult Error(string message = "")
        {
            ViewBag.Message = message ?? "Your error page.";

            return View();
        }
}

Once everything is set up and configured with the necessary changes, your application will look like this after successful authentication and authorization

Implementing Authorization and Handling Unauthorized Access with Azure AD in ASP.NET MVC

When using the Identity and Access Management (IAM) system, our main goal is to keep the application secure and stop unauthorized users from getting in. We usually do this with role-based access control (RBAC). 🔝

To do this, we will use the AuthorizationCodeReceived method. This method is called after the security token is checked, if there is an authorization code in the message from Microsoft Entra ID and updated the Startup.Auth.cs class:


        // => File: Contoso.AzureAD/Contoso.AzureAD/Startup.Auth.cs
        /// <summary>
        /// This method is called when the authorization code is received.
        /// </summary>
        /// <param name="notification"></param>
        /// <returns></returns>
        private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
        {
            notification.HandleCodeRedemption();

            var httpContext = notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase;

            try
            {
                // You can write logic here to decide who can sign in based on the groups assigned to the user.
                bool isAuthorized = true;
                if (httpContext != null)
                {
                    httpContext.Session["IsAuthorized"] = isAuthorized;
                }

                if (!isAuthorized)
                {
                    throw new UnauthorizedAccessException("You are not part of the required group.");
                }
                notification.HandleCodeRedemption(null);
            }
            catch (MsalException ex)
            {
                string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
                notification.HandleResponse();
                notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
            }
            catch (UnauthorizedAccessException ex)
            {
                // Log the exception
                // Redirect to the error page with a custom message
                notification.HandleResponse();
                notification.OwinContext.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);

                var urlHelper = new System.Web.Mvc.UrlHelper(httpContext.Request.RequestContext);
                string callbackUrl = urlHelper.Action("Error", "Home", new { message = ex.Message }, httpContext.Request.Url.Scheme);

                notification.Response.Redirect(callbackUrl);
                return;
            }
        }

In the OnAuthorizationCodeReceivedAsync method, you can check the logged-in User Groups received from Azure AD to decide whether to grant access to the user, and if not, raise the UnauthorizedAccessException. For testing purposes, I am just setting the isAuthorized variable here.

If the user is not authorized but is logged in at this stage, you should show the Signout link based on isAuthorized. 🔝

// => File: Contoso.AzureAD/Contoso.AzureAD/Views/Shared/_LoginPartial.cshtml
else if (isAuthorized.Equals("false",StringComparison.InvariantCultureIgnoreCase))
{
    <text>
        <ul class="navbar-nav navbar-right">
            <li>
                @Html.ActionLink("Sign out", "SignOut", "Account", new { area = "" }, new { @class = "nav-link" })
            </li>
        </ul>
    </text>
}

If the user is not authorized, the screen below will appear with a Signout link

Once the user clicks on the Signout link, I redirect the user to the Account/SignOutCallback controller from the Account/Signout controller so that I can show a message to the end-user that says, "You have successfully signed out."

        // => File: Contoso.AzureAD/Contoso.AzureAD/Controllers/AccountController.cs
        /// <summary>
        /// This method is called to sign out the user.
        /// </summary>
        /// <param name="message"></param>
        [AllowAnonymous]
        public void SignOut(string message = "")
        {
            string callbackUrl = Url.Action("SignOutCallback", "Account", new { message = message }, protocol: Request.Url.Scheme);

            HttpContext.GetOwinContext().Authentication.SignOut(
                new AuthenticationProperties { RedirectUri = callbackUrl },
                OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
        }

After clicking the Signout link, the user will be redirected to the SignOutCallback page, and the screen below will appear:

🤯Challenges I Faced and Tips

  1. Redirect URI Mismatch: Make sure the redirect URI in Azure is the same as the URI in your app’s OWIN configuration.

  2. Admin Consent Issues: Without admin consent, the app might not be able to access Microsoft Graph API resources. 🔝

  3. Error Handling: Always set up the AuthenticationFailed notification to handle authentication errors smoothly.

  4. Group Membership Retrieval: Ensure you have added the necessary Graph API permissions when checking user group membership.

  5. The startup code is not executing: If your OwinStartup code is not firing then ensure following items:

    a) Ensure that both Startup.cs and Startup.Auth.cs files are located in the root folder.

    b) Ensure the statement [assembly: OwinStartup(typeof(Contoso.AzureAD.Startup))] is included in the Startup.cs file.

    c) Ensure the Microsoft.Owin.Host.SystemWeb package is installed in the project.

  6. Could not load file or assembly 'Microsoft.IdentityModel.Tokens:

    This might be happening because:

    a) The version of Microsoft.IdentityModel.Tokens in your project might not match the installed version. Ensure that the version in your packages.config or .csproj file aligns with the installed version.

    b) There could be a mismatch in the PublicKeyToken or Culture settings; check that these settings are the same in your project files and the actual assembly. 🔝

    c) There might be a binding redirect issue in your web.config or app.config file, so check for any binding redirects for Microsoft.IdentityModel.Tokens and ensure they are set up correctly. You can verify the binding redirect and the assembly order here.

  7. Single Sign-Out: Configure proper sign-out to clear both local and Azure AD sessions.

🔦Conclusion

Integrating an ASP.NET MVC web application with Azure Active Directory provides secure authentication and authorization capabilities. As cloud-based identity solutions become more common, mastering this integration is a valuable skill for any .NET developer. 🔝

By following this guide and learning from my experiences, you'll be ready to implement Azure AD authentication in your MVC applications, ensuring a secure and seamless experience for your users.

I hope this blog helps you in your Azure AD integration journey. If you have any questions or face any challenges, feel free to reach out in the comments section. Happy coding!

💡
Explore My GitHub Repo for Authenticating and Authorizing ASP.NET MVC Applications with Microsoft Entra ID/Azure AD

🙏Credit/References

🏓Pingback

Learn how to secure your ASP.NET MVC applications with Microsoft Entra ID. Step-by-step guide for authentication and authorization.Discover the best practices for integrating Microsoft Entra ID with ASP.NET MVC for robust authentication and authorization.Secure your ASP.NET MVC applications using Microsoft Entra ID. Comprehensive tutorial for authentication and authorization.
Step-by-Step Guide to Secure Your ASP.NET MVC Applications with Microsoft Entra IDEnhance your ASP.NET MVC application's security with Microsoft Entra ID. Learn how to implement authentication and authorization.Master the integration of Microsoft Entra ID with ASP.NET MVC for secure authentication and authorization.
Learn how to integrate Microsoft Entra ID with ASP.NET MVC for robust authentication and authorization.Step-by-step tutorial on integrating Microsoft Entra ID with ASP.NET MVC for secure authentication and authorization.Enhance your ASP.NET MVC application's security with Microsoft Entra ID. Comprehensive guide for authentication and authorization.
Implementing Single Sign-On in ASP.NET MVC with Azure ADUnderstanding OAuth2 and OpenID Connect for Microsoft Identity IntegrationSecuring Your ASP.NET MVC Application: Authentication and Authorization Simplified
How to Configure Azure AD for Multi-Tenant Applications in ASP.NETIntegrating OWIN Middleware for Azure AD Authentication in ASP.NET MVC AppsCommon Challenges in Azure AD Integration and How to Overcome Them
Exploring the Benefits of Azure Active Directory for Enterprise ApplicationsConfiguring Group-Based Authorization in Azure Active Directory for ASP.NET MVCTop Security Best Practices for ASP.NET MVC Applications Using Microsoft Identity 🔝
Modernizing ASP.NET MVC Authentication with Microsoft Entra IDEnhancing Security: Microsoft Entra ID Authentication in ASP.NET MVC AppsFrom Zero to Hero: Implementing Microsoft Entra ID in ASP.NET MVC
Mastering Single Sign-On: ASP.NET MVC and Microsoft Entra IDBulletproof Authentication: ASP.NET MVC with Microsoft Entra IDStreamlining User Access: Microsoft Entra ID in ASP.NET MVC Applications
Sitecore Headless SXAsitecore jss nextjsAuthenticate ASP.NET MVC apps with Entra ID - ADAL and OWIN
Sitecore XM Cloud Local Setup Error DockerSetting up your full-stack XM Cloud local developmentSitecore XM Cloud Local Setup without Docker
Sitecore XM Cloud local setup error with Node.jsSitecore XM Cloud local setup docker errorXM Cloud local setup error 'nodejs' failed to build
Sitecore XM Cloud - possible issues when starting DockerXM Cloud — Troubleshooting the integration of Pages appTroubleshooting the Cloud SDK
Creating A Local Sitecore XM Installation For XM CloudContainer build Failed Error — Setting up XM Cloud projectConnect XM Cloud Pages to your local XM instance 🔝
How to setup Sitecore XM Cloud?What is Sitecore Stream?What is XM Cloud?
What is Sitecore Experience Edge?What is Experience Edge?What is Sitecore Search?
XM Cloud Search OptionsSitecore Experience Edge GraphQL QueriesSitecore Docker Images