How to Authenticate and Authorize ASP.NET MVC Applications Using Microsoft Entra ID
Secure Your ASP.NET MVC Application with Azure Active Directory
Table of contents
- 🤔What is Microsoft Entra ID (formerly Azure Active Directory)?
- 🏗️Creating an Azure AD App
- 🔦Conclusion
- 🙏Credit/References
- 🏓Pingback
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: 🔝
Sign in to the Azure portal.
Navigate to Azure Active Directory.
Select "App registrations" and click "New registration".
Provide a name for your application and configure the redirect URI.
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
Navigate to Azure Portal. 🔝
Sign in with your Azure account credentials.
2. Create an Azure AD App Registration
In the Azure Portal, go to Azure Active Directory > App registrations > New registration.
Enter a Name for your application (e.g., "Contoso App Integration").
Choose Accounts in this organizational directory only for the supported account types.
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 behttps://localhost:44300/signin-oidc
).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
Under the app registration, go to Authentication.
Add a platform and select Web.
Enter the Redirect URI (e.g.,
https://www.contoso.com/signin-oidc
If you are testing on a local setup, the URL can behttps://localhost:44300/signin-oidc
).Enable the option ID tokens and save changes.
5. Create a Client Secret
Go to Certificates & secrets > New client secret.
Add a description (e.g., "Contoso Integration Secret") and set an expiry period.
Click Add and copy the generated secret value. Store it securely as you won’t see it again.
6. Assign API Permissions
Go to API Permissions > Add a permission.
Select Microsoft Graph > Delegated permissions.
Add
openid
,profile
, andemail
permissions. 🔝Add API permission in Azure AD application for
Application
typeDirectory.Read.All
,Directory.ReadWrite.All
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
Redirect URI Mismatch: Make sure the redirect URI in Azure is the same as the URI in your app’s OWIN configuration.
Admin Consent Issues: Without admin consent, the app might not be able to access Microsoft Graph API resources. 🔝
Error Handling: Always set up the
AuthenticationFailed
notification to handle authentication errors smoothly.Group Membership Retrieval: Ensure you have added the necessary Graph API permissions when checking user group membership.
The startup code is not executing: If your OwinStartup code is not firing then ensure following items:
a) Ensure that both
Startup.cs
andStartup.Auth.cs
files are located in the root folder.b) Ensure the statement
[assembly: OwinStartup(typeof(Contoso.AzureAD.Startup))]
is included in theStartup.cs
file.c) Ensure the
Microsoft.Owin.Host.SystemWeb
package is installed in the project.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.
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!