A Small Dose of History…

Back in 2011, when we started our project in ASP.NET MVC, there weren’t many out-of-the-box authentication mechanisms available. Most developers ended up using Forms Authentication (usually in combination with SQL Membership & Role Providers) or Windows Authentication, the latter being the preferred choice for intranet applications.

Our case was even more constrained. Although our project was designed as a web application, we had to rely on NTLM authentication—not because Kerberos wasn’t an option for web apps, but because SharePoint 2010 was configured to use NTLM by default, and company policies prevented setting up a two-way trust between the domain hosting our MVC app and the SharePoint domain.

Additionally, the authentication had to work not just for domain-joined machines, but also for external users accessing the app from machines outside the domain. Since Kerberos requires the client machine to be in the domain or have a trust relationship, it wasn’t a viable option. NTLM, on the other hand, allowed us to authenticate users with their logged-in Windows credentials regardless of whether their machine was part of the domain.

This setup ensured seamless single sign-on (SSO) between the MVC web app and SharePoint without requiring users to log in again. Furthermore, we had to integrate with SharePoint Web Services, making this authentication flow essential.

The Need For Change

Fast forward to 2025, and our infrastructure has evolved significantly. Our on-premise SharePoint has been migrated to the cloud, and the company’s primary user directory service has transitioned from Active Directory (AD) Server to Azure AD.

Additionally, we now have two distinct authentication needs:

  1. Employees should authenticate using Azure AD, aligning with our internal IT policies.
  2. Guests should authenticate via Azure B2C, allowing external partners and customers to log in without requiring an AD account.

With our planned cloud migration, it was clear that NTLM was no longer an option. Modern cloud environments don’t support NTLM authentication, and we needed a more scalable, flexible, and secure authentication model.

The best way forward? Migrating to OAuth 2.0 and OpenID Connect.

To ensure a smooth transition without disrupting existing functionality, we followed a step-by-step migration strategy.

Here’s how we did it.

Step 1: Install the Required OWIN Middleware

Since ASP.NET MVC 5 doesn’t have built-in support for OAuth 2.0 or OpenID Connect, we had to install the necessary OWIN authentication middleware via NuGet.

📌 Run the following command in the Package Manager Console:

Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Security.OAuth
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.IdentityModel.Protocols.OpenIdConnect

Why These Packages?

  • Microsoft.Owin.Security.OpenIdConnect → Handles OpenID Connect authentication.
  • Microsoft.Owin.Security.Cookies → Manages authentication sessions using cookies.
  • Microsoft.Owin.Security.OAuth → Enables OAuth Bearer token authentication for APIs.
  • Microsoft.Owin.Host.SystemWeb → Enables OWIN middleware in ASP.NET MVC 5 applications.
  • icrosoft.IdentityModel.Protocols.OpenIdConnect → Fetches OpenID Connect metadata dynamically.

Step 2: Create the OWIN Startup Class

This class configures authentication in our ASP.NET MVC 5 application.

Create a new class called Startup.cs in your project and add the following:

using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System.IdentityModel.Tokens;

namespace MyApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
        }
    }
}

2.1 Registering the OWIN Startup Class

[assembly: OwinStartup(typeof(OwinModuleStartup))]
  • This registers OwinModuleStartup as the startup class for OWIN.
  • When the application starts, OWIN loads this class automatically.

2.2 Configuring Authentication Middleware

public void Configuration(IAppBuilder app)
{ 
    //...
    //...
    app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationType);
    //...
}
  • Sets the default authentication type to cookies.
  • Ensures that all sign-in operations store identity in a secure cookie.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationType,
    CookieSameSite = SameSiteMode.None,
    CookieDomain = ConfigurationManager.AppSettings["OpenIdCookieDomain"],
    LoginPath = new PathString(ConfigurationManager.AppSettings["OpenIdLoginRedirect"]),
    LogoutPath = new PathString(ConfigurationManager.AppSettings["OpenIdLogoutRedirect"])
});
  • Stores authentication tokens in cookies to maintain user sessions.
  • Prevents SameSite cookie issues by setting SameSiteMode.None.
  • Configures login/logout paths based on web.config settings.

2.4 Configuring OpenID Connect for Azure AD

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    ClientId = ConfigurationManager.AppSettings["OpenIdAzureAdClientId"],
    Authority = ConfigurationManager.AppSettings["OpenIdAzureAdAuthority"],
    RedirectUri = ConfigurationManager.AppSettings["OpenIdAzureAdRedirectUri"],
    ResponseType = OpenIdConnectResponseType.CodeIdToken,
    Scope = "openid profile email",
    AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
    TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = true,
        ValidateIssuer = true,
        ValidAudiences = ConfigurationManager.AppSettings["OpenIdAzureAdValidAudiences"].Split(','),
        ValidIssuers = ConfigurationManager.AppSettings["OpenIdAzureAdValidIssuers"].Split(',')
    }
});
  • Azure AD is used for employee authentication.
  • Redirects users to Microsoft login page when authentication is needed.
  • Retrieves user profile details (openid profile email).
  • Ensures tokens are validated against Azure AD’s expected issuers & audiences.

2.5 Configuring OpenID Connect for Azure B2C

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    ClientId = ConfigurationManager.AppSettings["OpenIdAzureAdB2CClientId"],
    Authority = ConfigurationManager.AppSettings["OpenIdAzureAdB2CAuthority"],
    RedirectUri = ConfigurationManager.AppSettings["OpenIdAzureAdB2CRedirectUri"],
    ResponseType = OpenIdConnectResponseType.IdToken,
    Scope = "openid profile email",
    AuthenticationType = "AzureADB2C",
    TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = true,
        ValidateIssuer = true,
        ValidAudiences = ConfigurationManager.AppSettings["OpenIdAzureAdB2CValidAudiences"].Split(','),
        ValidIssuers = ConfigurationManager.AppSettings["OpenIdAzureAdB2CValidIssuers"].Split(',')
    }
});
  • Azure B2C is used for guest authentication.
  • Redirects external users (customers, partners) to B2C login page.
  • Validates ID tokens issued by Azure B2C.

2.6 Configuring OAuth Bearer Token Authentication (For API Calls)

app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
    AuthenticationMode = AuthenticationMode.Active,
    Provider = new OAuthBearerAuthenticationProvider
    {
        OnRequestToken = async context =>
        {
            var token = context.Request.Headers.Get("Authorization")?.Replace("Bearer ", "");
            if (!string.IsNullOrEmpty(token))
            {
                var tokenHandler = new JwtSecurityTokenHandler();
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuer = ConfigurationManager.AppSettings["OpenIdAzureAdJwtIssuer"],
                    ValidateAudience = true,
                    ValidAudience = ConfigurationManager.AppSettings["OpenIdAzureAdJwtValidAudience"],
                    ValidateIssuerSigningKey = true
                };
                var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
                context.OwinContext.Authentication.User = new ClaimsPrincipal(principal);
            }
            await Task.CompletedTask;
        }
    }
});
  • Handles API authentication using OAuth 2.0 Bearer tokens.
  • Extracts and validates JWT tokens from the Authorization header.
  • Verifies issuer, audience, and signature keys before authenticating API users.

Step 3: Updating web.config for Authentication

  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="OpenIdCookieDomain" value="" />
    <add key="OpenIdLoginRedirect" value="/Account/Login" />
    <add key="OpenIdLogoutRedirect" value="/Account/SignOut" />
    <add key="OpenIdAzureAdSilentLogin" value="/Account/SignSilentlyInWithAzureAd" />
    <add key="OpenIdAzureAdClientId" value="" />
    <add key="OpenIdAzureAdClientSecret" value="" />
    <add key="OpenIdAzureAdClientScopes" value="openid profile offline_access" />
    <add key="OpenIdAzureAdAuthority" value="https://login.microsoftonline.com/{REPLACE_WITH_YOUR_TENANT_ID}/v2.0" />
    <add key="OpenIdAzureAdRedirectUri" value="https://localhost:8081/account/login" />
    <add key="OpenIdAzureAdPostLogoutRedirectUri" value="https://localhost:8081/account/login" />
    <add key="OpenIdAzureAdNameClaimType" value="preferred_username" />
    <add key="OpenIdAzureAdRoleClaimType" value="role" />
    <add key="OpenIdAzureAdValidIssuers" value="https://sts.windows.net/{REPLACE_WITH_YOUR_TENANT_ID}/,https://login.microsoftonline.com/{REPLACE_WITH_YOUR_TENANT_ID}/v2.0" />
    <add key="OpenIdAzureAdValidAudiences" value="" />
    <add key="OpenIdAzureAdB2CSilentLogin" value="/Account/SignSilentlyInWithAzureAdB2c" />
    <add key="OpenIdAzureAdB2CClientId" value="" />
    <add key="OpenIdAzureAdB2CClientSecret" value="" />
    <add key="OpenIdAzureAdB2CClientScopes" value="openid offline_access https://{REPLACE_WITH_YOUR_TENANT}.onmicrosoft.com/{REPLACE_WITH_YOUR_SCOPE}" />
    <add key="OpenIdAzureAdB2CAuthority" value="https://{REPLACE_WITH_YOUR_TENANT}.b2clogin.com/{REPLACE_WITH_YOUR_TENANT}.onmicrosoft.com/{REPLACE_WITH_YOUR_POLICY}/v2.0" />
    <add key="OpenIdAzureAdB2CRedirectUri" value="https://localhost:8081/account/login" />
    <add key="OpenIdAzureAdB2CPostLogoutRedirectUri" value="https://localhost:8081/account/login" />
    <add key="OpenIdAzureAdB2CNameClaimType" value="emails" />
    <add key="OpenIdAzureAdB2CRoleClaimType" value="role" />
    <add key="OpenIdAzureAdB2CValidIssuers" value="https://solidfdvtest.b2clogin.com/{REPLACE_WITH_YOUR_TENANT_ID}/v2.0/,https://solidfdvtest.b2clogin.com/tfp/{REPLACE_WITH_YOUR_TENANT_ID}/{REPLACE_WITH_YOUR_POLICY}/v2.0/" />
    <add key="OpenIdAzureAdB2CValidAudiences" value="" />
    <add key="OpenIdAzureAdJwtValidAudience" value="" />
    <add key="OpenIdAzureAdJwtClientId" value="" />
    <add key="OpenIdAzureAdJwtIssuer" value="https://sts.windows.net/{REPLACE_WITH_YOUR_TENANT_ID}/" />
    <add key="OpenIdAzureAdJwtAuthority" value="https://login.microsoftonline.com/{REPLACE_WITH_YOUR_TENANT_ID}/v2.0" />
  </appSettings>
  <system.web>
    <sessionState mode="InProc" timeout="20" cookieless="UseCookies" />
    <authentication mode="None" />
  </system.web>
  • Handsover authentication to OWIN middelware.

Step 4: Managing Login & Logout in MVC Controllers

public class AccountController : Controller
{
    [AllowAnonymous]
    public ActionResult SignInWithAzureAd()
    {
        if (User.Identity.IsAuthenticated)
        {
            return Redirect("/");
        }

        HttpContext.GetOwinContext().Authentication.Challenge(
            new AuthenticationProperties { RedirectUri = "/" },
            OpenIdConnectAuthenticationDefaults.AuthenticationType);

        return new HttpStatusCodeResult(401);
    }

    [AllowAnonymous]
    public void SignSilentlyInWithAzureAd(string redirect)
    {
        if (User.Identity.IsAuthenticated)
        {
            return;
        }

        var properties = new AuthenticationProperties
        {
            RedirectUri = redirect,
            Dictionary =
            {
                ["prompt"] = "none",
            }
        };

        var loginHint = Request.Cookies[OwinModuleStartup.IdentityProviderLoginHintCookieKey]?.Value;

        if (!string.IsNullOrEmpty(loginHint))
        {
            properties.Dictionary.Add("login_hint", loginHint);
        }

        HttpContext.GetOwinContext().Authentication.Challenge(properties,
            OpenIdConnectAuthenticationDefaults.AuthenticationType);
    }

    [AllowAnonymous]
    // Sign in using Azure AD B2C
    public ActionResult SignInWithAzureB2C()
    {
        if (User.Identity.IsAuthenticated)
        {
            return Redirect("/");
        }

        HttpContext.GetOwinContext().Authentication.Challenge(
            new AuthenticationProperties { RedirectUri = "/" },
            "AzureADB2C");

        return new HttpStatusCodeResult(401);
    }

    [AllowAnonymous]
    public void SignSilentlyInWithAzureAdB2c(string redirect)
    {
        if (User.Identity.IsAuthenticated)
        {
            return;
        }

        var properties = new AuthenticationProperties
        {
            RedirectUri = redirect,
            Dictionary =
            {
                ["prompt"] = "none",
            }
        };

        var loginHint = Request.Cookies[OwinModuleStartup.IdentityProviderLoginHintCookieKey]?.Value;

        if (!string.IsNullOrEmpty(loginHint))
        {
            properties.Dictionary.Add("login_hint", loginHint);
        }

        HttpContext.GetOwinContext().Authentication.Challenge(properties,
            "AzureADB2C");
    }

    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        if (User.Identity.IsAuthenticated)
        {
            if (Request.IsAjaxRequest())
            {
                return new HttpStatusCodeResult(HttpStatusCode.NoContent);
            }

            if (!string.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            // Redirect authenticated users to the homepage
            return RedirectToAction("Index", "Home");
        }

        if (Request.IsAjaxRequest())
        {
            return new HttpStatusCodeResult(401);
        }

        return View();
    }
}
  • Contains methods for login for Azure AD and Azure B2C providers
  • Contains logout method.
  • Handles successfull/failed login redirect.

Full OwinModuleStartup.cs

using System;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Mvc5AuzreAd;
using Owin;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Text.Encodings.Web;

[assembly: OwinStartup(typeof(OwinModuleStartup))] // This is the registration of startup class for Owin

namespace Mvc5AuzreAd
{
    public class OwinModuleStartup
    {
        public const string IdentityProviderCookieKey = "ipd";
        public const string IdentityProviderLoginHintCookieKey = "idp.login_hint";
        public const string IdentityProviderCookieValueB2C = "b2c";
        public const string IdentityProviderCookieValueAAD = "aad";
        private const string DefaultAuthenticationType = "ApplicationCookie";
        public void Configuration(IAppBuilder app)
        {

            // Get OpenIdConnectConfiguration for Bearer token validation.
            var authority = ConfigurationManager.AppSettings["OpenIdAzureAdJwtAuthority"];

            var wellKnownEndpoint = $"{authority}/.well-known/openid-configuration";
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
                wellKnownEndpoint, new OpenIdConnectConfigurationRetriever());

            var openIdConfig = Task.Run(() => configurationManager.GetConfigurationAsync()).Result;

            // Configure OAuth Bearer token authentication
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
            {
                AuthenticationMode = AuthenticationMode.Active,
                Provider = new OAuthBearerAuthenticationProvider
                {
                    OnRequestToken = async context =>
                    {
                        // Ensure Bearer token is in the Authorization header
                        var token = context.Request.Headers.Get("Authorization")?.Replace("Bearer ", "");

                        if (string.IsNullOrEmpty(token))
                        {
                            await Task.CompletedTask;
                            return;
                        }

                        try
                        {
                            var tokenHandler = new JwtSecurityTokenHandler();

                            var validationParameters = new TokenValidationParameters
                            {
                                ValidateIssuer = true,
                                ValidIssuer = ConfigurationManager.AppSettings["OpenIdAzureAdJwtIssuer"],
                                ValidateAudience = true,
                                ValidAudience = ConfigurationManager.AppSettings["OpenIdAzureAdJwtValidAudience"],
                                ValidateIssuerSigningKey = true,
                                IssuerSigningKeys = openIdConfig.SigningKeys,
                                ValidateLifetime = true
                            };

                            var principal = tokenHandler.ValidateToken(token, validationParameters, out _);

                            var claimsIdentity = (ClaimsIdentity)principal.Identity;

                            if (!claimsIdentity.HasClaim(c => c.Type == "IsExternal")) // Add extral claim if needed
                            {
                                claimsIdentity.AddClaim(new Claim("IsExternal", "False", "Boolean"));
                            }

                            var cp = new ClaimsIdentity(claimsIdentity.Claims, "JWT", ClaimTypes.Name, ClaimTypes.Role);

                            context.OwinContext.Authentication.User = new ClaimsPrincipal(cp);
                        }
                        catch (SecurityTokenExpiredException)
                        {
                            // Handle expired token - return 401 without redirecting
                            context.Response.StatusCode = 401;
                            context.Response.Headers.Append("WWW-Authenticate", "Bearer error=\"invalid_token\", error_description=\"Token has expired\"");
                        }
                        catch (SecurityTokenValidationException)
                        {
                            // Handle invalid token - return 401 without redirecting
                            context.Response.StatusCode = 401;
                            context.Response.Headers.Append("WWW-Authenticate", "Bearer error=\"invalid_token\", error_description=\"Token validation failed\"");
                        }
                        catch (Exception)
                        {
                            // Handle unexpected validation errors
                            context.Response.StatusCode = 500;
                            context.Response.Headers.Append("WWW-Authenticate", "Bearer error=\"server_error\", error_description=\"Token validation error\"");
                        }

                        await Task.CompletedTask;
                    }
                }
            });

            app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions()
            {
                CookieSameSite = SameSiteMode.None,
                CookieDomain = ConfigurationManager.AppSettings["OpenIdCookieDomain"],
                AuthenticationType = DefaultAuthenticationType,
                LoginPath = new PathString(ConfigurationManager.AppSettings["OpenIdLoginRedirect"]),
                LogoutPath = new PathString(ConfigurationManager.AppSettings["OpenIdLogoutRedirect"]),
                Provider = new CookieAuthenticationProvider
                {
                    OnApplyRedirect = context =>
                    {
                        var token = context.Request.Headers.Get("Authorization")?.Replace("Bearer ", "");

                        if (!string.IsNullOrEmpty(token))
                        {
                            context.Response.StatusCode = 401;
                            context.Response.Headers.Remove("Location");
                            context.Response.Write("reauth_failed,bearer_provider");
                            return;
                        }

                        if (IsAjaxRequest(context.OwinContext) && context.OwinContext.Response.Headers.Any(i =>
                                i.Key == "X-ReauthState" && i.Value != null &&
                                i.Value.Contains("NoActiveProviderSession")))
                        {
                            context.Response.StatusCode = 401;
                            context.Response.Headers.Remove("Location");
                            context.Response.Write("reauth_failed,no_active_provider_session");
                        }
                        else if (IsAjaxRequest(context.OwinContext))
                        {
                            // For AJAX requests, return 401 Unauthorized without redirecting
                            context.Response.StatusCode = 401;
                            context.Response.Headers.Remove("Location");
                            context.Response.Write("reauth_required");
                        }
                        else
                        {
                            // For non-AJAX requests, perform the standard redirect to the login page
                            context.Response.Redirect(context.RedirectUri);
                        }
                    }
                }
            });

            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = ConfigurationManager.AppSettings["OpenIdAzureAdClientId"],
                ClientSecret = ConfigurationManager.AppSettings["OpenIdAzureAdClientSecret"],
                Authority = ConfigurationManager.AppSettings["OpenIdAzureAdAuthority"],
                RedirectUri = ConfigurationManager.AppSettings["OpenIdAzureAdRedirectUri"],
                PostLogoutRedirectUri = ConfigurationManager.AppSettings["OpenIdAzureAdPostLogoutRedirectUri"],
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                Scope = ConfigurationManager.AppSettings["OpenIdAzureAdClientScopes"],
                AuthenticationMode = AuthenticationMode.Passive,
                AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidAudiences = ConfigurationManager.AppSettings["OpenIdAzureAdValidAudiences"].Split(','),
                    ValidIssuers = ConfigurationManager.AppSettings["OpenIdAzureAdValidIssuers"].Split(','),
                    NameClaimType = ConfigurationManager.AppSettings["OpenIdAzureAdNameClaimType"],
                    RoleClaimType = ConfigurationManager.AppSettings["OpenIdAzureAdRoleClaimType"]
                },
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = context =>
                    {
                        if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                        {
                            // Prevent redirecting to Microsoft logout page
                            context.HandleResponse();

                            context.Response.Redirect("/account/postsignout");

                            return Task.CompletedTask;
                        }

                        // Add "domain_hint" to redirect users to correct login type (work/school vs personal)
                        context.ProtocolMessage.DomainHint = "organizations";  // or "consumers" for personal accounts

                        var cookie = context.Request.Cookies[IdentityProviderLoginHintCookieKey];
                        if (cookie != null)
                        {
                            context.ProtocolMessage.Prompt = "none";
                            context.ProtocolMessage.LoginHint = cookie;
                        }
                        else
                        {
                            if (IsAjaxRequest(context.OwinContext))
                            {
                                context.HandleResponse();
                                context.Response.Write("no_account_selected");
                                context.Response.StatusCode = 401;
                                return Task.CompletedTask;
                            }

                            context.ProtocolMessage.Prompt = "select_account";
                        }

                        return Task.CompletedTask;
                    },
                    AuthenticationFailed = context =>
                    {
                        if (context.Exception.Message.Contains("AADSTS50058") && IsAjaxRequest(context.OwinContext))
                        {
                            context.HandleResponse();
                            context.Response.StatusCode = 401;
                            context.Response.Headers.Append("X-ReauthState", "NoActiveProviderSession");
                            RemoveLoginCookies(context.OwinContext);
                            return Task.FromResult(0);
                        }

                        if (context.Exception.Message.Contains("AADSTS50058"))
                        {
                            if (context.Request.Headers.ContainsKey("sec-fetch-dest") && context.Request.Headers["sec-fetch-dest"] == "iframe")
                            {
                                context.HandleResponse();
                                context.Response.StatusCode = 401;

                                RemoveLoginCookies(context.OwinContext);

                                context.Response.Redirect("/error/AuthenticationFailed");
                                return Task.CompletedTask;
                            }

                            context.HandleResponse();
                            RemoveLoginCookies(context.OwinContext);
                            context.Response.Redirect("/error/AuthenticationFailed");
                            return Task.FromResult(0);
                        }

                        if (context.Request != null && IsAjaxRequest(context.OwinContext))
                        {
                            context.HandleResponse();
                            context.Response.StatusCode = 401;
                            context.Response.Write("reauth_required");
                            return Task.FromResult(0);
                        }

                        context.HandleResponse();

                        RemoveLoginCookies(context.OwinContext);

                        context.Response.Redirect("/account/login");
                        return Task.FromResult(0);
                    },
                    SecurityTokenValidated = context =>
                    {
                        var identity = context.AuthenticationTicket.Identity;

                        if (!identity.HasClaim(c => c.Type == "IsExternal"))
                        {
                            identity.AddClaim(new Claim("IsExternal", "False", "Boolean"));
                        }

                        if (!identity.HasClaim(c => c.Type == IdentityProviderCookieKey))
                        {
                            identity.AddClaim(new Claim(IdentityProviderCookieKey, IdentityProviderCookieValueAAD, "string"));
                        }

                        var cookie = context.Request.Cookies[IdentityProviderCookieKey];

                        if (cookie == null)
                        {
                            context.Response.Cookies.Append(IdentityProviderCookieKey, IdentityProviderCookieValueAAD, new CookieOptions() { SameSite = SameSiteMode.Lax, HttpOnly = false, Secure = false }); // Add the cookie to the response
                        }

                        if (context.AuthenticationTicket.Identity.HasClaim(p => p.Type == ConfigurationManager.AppSettings["OpenIdAzureAdLoginHintCookieClaimType"]))
                        {
                            context.Response.Cookies.Append(IdentityProviderLoginHintCookieKey, context.AuthenticationTicket.Identity.Claims.First(i => i.Type == ConfigurationManager.AppSettings["OpenIdAzureAdLoginHintCookieClaimType"]).Value, new CookieOptions() { SameSite = SameSiteMode.Lax, HttpOnly = true, Secure = context.Request.IsSecure }); // Add the cookie to the response
                        }

                        return Task.FromResult(0);
                    }
                },
            });

            app.UseOpenIdConnectAuthentication(
              new OpenIdConnectAuthenticationOptions
              {
                  ClientId = ConfigurationManager.AppSettings["OpenIdAzureAdB2CClientId"],
                  ClientSecret = ConfigurationManager.AppSettings["OpenIdAzureAdB2CClientSecret"],
                  Authority = ConfigurationManager.AppSettings["OpenIdAzureAdB2CAuthority"],
                  RedirectUri = ConfigurationManager.AppSettings["OpenIdAzureAdB2CRedirectUri"],
                  PostLogoutRedirectUri = ConfigurationManager.AppSettings["OpenIdAzureAdB2CPostLogoutRedirectUri"],
                  ResponseType = OpenIdConnectResponseType.IdTokenToken,
                  Scope = ConfigurationManager.AppSettings["OpenIdAzureAdB2CClientScopes"],
                  AuthenticationMode = AuthenticationMode.Passive,
                  TokenValidationParameters = new TokenValidationParameters
                  {
                      ValidateAudience = true,
                      ValidateIssuer = true,
                      ValidAudiences = ConfigurationManager.AppSettings["OpenIdAzureAdB2CValidAudiences"].Split(','),
                      ValidIssuers = ConfigurationManager.AppSettings["OpenIdAzureAdB2CValidIssuers"].Split(','),
                      NameClaimType = ConfigurationManager.AppSettings["OpenIdAzureAdB2CNameClaimType"],
                  },
                  AuthenticationType = "AzureADB2C",
                  Notifications = new OpenIdConnectAuthenticationNotifications
                  {
                      AuthenticationFailed = context =>
                      {
                          if (context.Exception.Message.Contains("AADB2C90118"))
                          {
                              context.HandleResponse();
                              context.Response.Redirect("/Account/ResetPassword");
                              return Task.FromResult(0);
                          }

                          if ((context.Exception.Message.Contains("AADSTS50058") || context.Exception.Message.Contains("AADB2C90077")) && IsAjaxRequest(context.OwinContext))
                          {
                              context.HandleResponse();
                              context.Response.StatusCode = 401;
                              context.Response.Headers.Append("X-ReauthState", "NoActiveProviderSession");
                              RemoveLoginCookies(context.OwinContext);
                              return Task.FromResult(0);
                          }

                          if (context.Exception.Message.Contains("AADSTS50058") || context.Exception.Message.Contains("AADB2C90077"))
                          {
                              context.HandleResponse();
                              RemoveLoginCookies(context.OwinContext);
                              context.Response.Redirect("/error/AuthenticationFailed");
                              return Task.FromResult(0);
                          }

                          context.HandleResponse();

                          RemoveLoginCookies(context.OwinContext);

                          context.Response.Redirect("/account/login");
                          return Task.FromResult(0);
                      },
                      SecurityTokenValidated = context =>
                      {
                          var identity = context.AuthenticationTicket.Identity;

                          var requiredClaim = identity.FindFirst("extension_PortalAccountsPerm");

                          if (requiredClaim == null)
                          {
                              context.HandleResponse();
                              context.Response.StatusCode = 401;
                              context.Response.Write("Unauthorized: Required claim missing or invalid.");
                              return Task.FromResult(0);
                          }

                          if (!requiredClaim.Value.Split(',').Any(i => i.Equals("Pw2", StringComparison.InvariantCultureIgnoreCase)))
                          {
                              context.HandleResponse();
                              context.Response.StatusCode = 401;
                              context.Response.Write("Unauthorized: Required claim missing or invalid.");
                              return Task.FromResult(0);
                          }

                          if (!identity.HasClaim(c => c.Type == "IsExternal"))
                          {
                              identity.AddClaim(new Claim("IsExternal", "True", "Boolean"));
                          }

                          if (!identity.HasClaim(c => c.Type == IdentityProviderCookieKey))
                          {
                              identity.AddClaim(new Claim(IdentityProviderCookieKey, IdentityProviderCookieValueB2C, "string"));
                          }

                          var cookie = context.Request.Cookies[IdentityProviderCookieKey];

                          if (cookie == null)
                          {
                              context.Response.Cookies.Append(IdentityProviderCookieKey, IdentityProviderCookieValueB2C, new CookieOptions() { SameSite = SameSiteMode.Lax, HttpOnly = false, Secure = false }); // Add the cookie to the response// context.Response.Cookies.Append("ASP.NET_SessionId", "", new CookieOptions() { Expires = DateTime.UtcNow.AddYears(1), SameSite = SameSiteMode.Lax, HttpOnly = true, Secure = context.Request.IsSecure }); // Add the cookie to the response
                          }

                          if (context.AuthenticationTicket.Identity.HasClaim(p => p.Type == "emails"))
                          {
                              context.Response.Cookies.Append(IdentityProviderLoginHintCookieKey, context.AuthenticationTicket.Identity.Name, new CookieOptions() { SameSite = SameSiteMode.Lax, HttpOnly = true, Secure = context.Request.IsSecure }); // Add the cookie to the response
                          }

                          return Task.FromResult(0);
                      },
                      RedirectToIdentityProvider = context =>
                      {
                          if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                          {
                              // Prevent redirecting to Microsoft logout page
                              context.HandleResponse();

                              context.Response.Redirect("/account/postsignout");

                              return Task.CompletedTask;
                          }

                          var cookie = context.Request.Cookies[IdentityProviderLoginHintCookieKey];
                          if (cookie != null)
                          {
                              context.ProtocolMessage.Prompt = "none";
                              context.ProtocolMessage.LoginHint = cookie;
                          }
                          else
                          {
                              if (IsAjaxRequest(context.OwinContext))
                              {
                                  context.HandleResponse();
                                  context.Response.Write("no_account_selected");
                                  context.Response.StatusCode = 401;
                                  return Task.CompletedTask;
                              }

                              context.ProtocolMessage.Prompt = "select_account";
                          }

                          return Task.CompletedTask;
                      },
                  }
              });

            app.Use(async (context, next) =>
            {

                var token = context.Request.Headers.Get("Authorization")?.Replace("Bearer ", "");
                if (!string.IsNullOrEmpty(token))
                {
                    await next.Invoke();
                    return;
                }

                if (context.Request.Path.Value.ToLower().StartsWith("/error"))
                {
                    await next.Invoke();
                    return;
                }

                if (context.Request.Path.Value.ToLower().StartsWith("/scripts") ||
                    context.Request.Path.Value.ToLower().StartsWith("/content") || context.Request.Path.Value.ToLower() == "/favicon.ico")
                {
                    await next.Invoke();
                    return;
                }

                var user = context.Authentication.User;

                if (context.Request.Path.Value.ToLower() == "/account/postsignout")
                {
                    RemoveLoginCookies(context);
                    context.Response.Redirect("/account/login");

                    return;
                }

                if (context.Request.Path.Value.ToLower() == "/account/resetpassword")
                {
                    string resetPasswordPolicy = ConfigurationManager.AppSettings["OpenIdAzureAdB2CResetPasswordPolicy"];
                    string redirectUri = $"{ConfigurationManager.AppSettings["OpenIdAzureAdB2CRedirectUri"]}";
                    string tenant = ConfigurationManager.AppSettings["OpenIdAzureAdB2CTenant"];
                    string clientId = ConfigurationManager.AppSettings["OpenIdAzureAdB2CClientId"];

                    // Build the URL for the password reset
                    string resetPasswordUrl = $"https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/v2.0/authorize?p={resetPasswordPolicy}&client_id={clientId}&redirect_uri={redirectUri}&response_type=id_token&scope={ConfigurationManager.AppSettings["OpenIdAzureAdB2CClientScopes"]}&nonce=defaultNonce";

                    // Redirect the user to Azure AD B2C password reset page
                    context.Response.Redirect(resetPasswordUrl);
                    return;
                }

                if (context.Request.Path.Value.ToLower() == "/account/signout")
                {
                    if (!user.Identity.IsAuthenticated)
                    {
                        context.Response.Redirect("/account/postsignout");
                        return;
                    }

                    var idpCookie = context.Request.Cookies[IdentityProviderCookieKey];

                    if (idpCookie == null && (user == null || !user.HasClaim(i => i.Type == OwinModuleStartup.IdentityProviderCookieKey)))
                    {
                        context.Response.Redirect("/");

                        await next.Invoke();
                        return;
                    }

                    string authType;

                    var idp = idpCookie ?? user.Claims.First(i => i.Type == OwinModuleStartup.IdentityProviderCookieKey).Value;

                    switch (idp)
                    {
                        case OwinModuleStartup.IdentityProviderCookieValueAAD:
                            authType = OpenIdConnectAuthenticationDefaults.AuthenticationType;
                            break;
                        case OwinModuleStartup.IdentityProviderCookieValueB2C:
                            authType = "AzureADB2C";
                            break;
                        default:
                            {
                                context.Response.Redirect("/");
                                await next.Invoke();
                                return;
                            };
                    }

                    context.Authentication.SignOut(new AuthenticationProperties() { RedirectUri = $"{context.Request.Scheme}://{context.Request.Host}/account/postsignout" }, authType, DefaultAuthenticationType);

                    await next.Invoke();
                    return;
                }

                if (user != null && !user.Identity.IsAuthenticated && context.Request.Cookies.Any(i => i.Key == IdentityProviderCookieKey) && context.Request.Path.Value.ToLower() != $"/account/SignInWithAzureAd".ToLower() &&
                    context.Request.Path.Value.ToLower() != $"/account/SignInWithAzureB2C".ToLower() && context.Request.Path.Value.ToLower() != $"/Account/SignSilentlyInWithAzureAd".ToLower() && context.Request.Path.Value.ToLower() != $"/Account/SignSilentlyInWithAzureAdB2c".ToLower())
                {
                    var isAjaxRequest = context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";

                    if (isAjaxRequest)
                    {
                        // Unauthorized XMLHttpRequest detected
                        context.Response.StatusCode = 401;
                        return;
                    }

                    // Check if ID token is expired or close to expiring (custom logic)
                    var identityProvider = context.Request.Cookies[IdentityProviderCookieKey];

                    switch (identityProvider)
                    {
                        case IdentityProviderCookieValueAAD:
                            context.Response.Redirect($"{ConfigurationManager.AppSettings["OpenIdAzureAdSilentLogin"]}?redirect={UrlEncoder.Default.Encode(context.Request.Path.Value)}");
                            break;
                        case IdentityProviderCookieValueB2C:
                            context.Response.Redirect($"{ConfigurationManager.AppSettings["OpenIdAzureAdB2CSilentLogin"]}?redirect={UrlEncoder.Default.Encode(context.Request.Path.Value)}");
                            break;
                        default: goto case IdentityProviderCookieValueAAD;
                    }

                    return;
                }

                if (!user.Identity.IsAuthenticated && (context.Request.Path.Value.ToLower() != ConfigurationManager.AppSettings["OpenIdLoginRedirect"].ToLower() &&
                    context.Request.Path.Value.ToLower() != ConfigurationManager.AppSettings["OpenIdAzureAdSilentLogin"].ToLower() &&
                     context.Request.Path.Value.ToLower() != ConfigurationManager.AppSettings["OpenIdAzureAdB2CSilentLogin"].ToLower() &&
                     context.Request.Path.Value.ToLower() != "/account/SignInWithAzureAd".ToLower() &&
                      context.Request.Path.Value.ToLower() != "/account/SignInWithAzureB2C".ToLower()))
                {
                    if (IsAjaxRequest(context))
                    {
                        // Unauthorized XMLHttpRequest detected
                        context.Response.Headers.Append("X-ReauthState", "NoActiveProviderSession");
                        context.Response.StatusCode = 401;
                        return;
                    }
                    context.Response.Redirect(ConfigurationManager.AppSettings["OpenIdLoginRedirect"]);
                    return;
                }

                await next.Invoke();
            });
        }

        private static void RemoveLoginCookies(IOwinContext context)
        {
            context.Response.Cookies.Append(IdentityProviderCookieKey, string.Empty, new CookieOptions() { Expires = DateTime.UtcNow.AddYears(-10), SameSite = SameSiteMode.Lax, HttpOnly = true, Secure = context.Request.IsSecure }); // Add the cookie to the response

            context.Response.Cookies.Append("ASP.NET_SessionId", "", new CookieOptions() { Expires = DateTime.UtcNow.AddDays(-1), SameSite = SameSiteMode.Lax, HttpOnly = true, Secure = context.Request.IsSecure }); // Add the cookie to the response

            context.Response.Cookies.Append(IdentityProviderLoginHintCookieKey, "", new CookieOptions() { Expires = DateTime.UtcNow.AddDays(-1), SameSite = SameSiteMode.Lax, HttpOnly = true, Secure = context.Request.IsSecure }); // Add the cookie to the response
        }

        private static bool IsAjaxRequest(IOwinContext context)
        {
            return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
        }
    }
}

Final Thoughts

  • Azure AD now handles employee authentication.
  • Azure B2C allows guest logins seamlessly.
  • Our ASP.NET MVC 5 app now follows modern OAuth 2.0 & OpenID Connect standards.

For a fully working implementation, check out the full source code on GitHub:

🔗 Full Example on GitHub