Azure Active Directory Configuration Issue: Web App Not Redirecting to Microsoft Login Page

Keevan 11 Reputation points
2023-05-16T23:21:14.85+00:00

My company has agreed to host a web application for a customer that currently has it running on their server. The web app was created by a third party and the customer doesn't have any documentation for it, only the project files for Visual Studio. It's using Azure Active Directory AAD (Microsoft.WindowsAzure.ConfigurationManager 2.0.3) for user authentication. I'm primarily a programmer so performing this kind of configuration isn't in my regular wheelhouse of skills.

The site is targeting 4.5.2 .NET Framework and using MVC 5 (5.2.3), AngularJS (1.3.15), and Azure Storage (4.3). I'm familiar with and primarily have worked with web pages and have no experience with MVC or AngularJS, but I've done some basic tutorials with both and I understand the basics of both. I've gone through the web app files and the code for the authentication is minimal.

In the Global.asax.cs we have the WSFederationAuthenticationModule_RedirectingToIdentityProvider event that is a WSFederationAuthenticationModule component redirecting the user to the identity provider during the authentication process:

private void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
{
    if (!String.IsNullOrEmpty(IdentityConfig.Realm))
    {
        e.SignInRequestMessage.Realm = IdentityConfig.Realm;
    }
}

In the IdentityConfig.cs we have the configuring and managing of the identity settings:

    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 = SettingsHelper.AzureActiveDirectorySettings.Realm; // ConfigurationManager.AppSettings["ida:realm"];

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

            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
        }

        public static void RefreshValidationSettings()
        {
            string metadataLocation = SettingsHelper.AzureActiveDirectorySettings.FederationMetadataLocation; // 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));
            }
        }
    }

In the AccountController controller we have actions related to user authentication and sign-out:

    [AllowAnonymous]
    public class AccountController : CVRController
    {
        public ActionResult SignOut()
        {
            WsFederationConfiguration config = FederatedAuthentication.FederationConfiguration.WsFederationConfiguration;

            // Redirect to SignOutCallback after signing out.
            string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);
            SignOutRequestMessage signoutMessage = new SignOutRequestMessage(new Uri(config.Issuer), callbackUrl);
            signoutMessage.SetParameter("wtrealm", IdentityConfig.Realm ?? config.Realm);
            FederatedAuthentication.SessionAuthenticationModule.SignOut();

            return new RedirectResult(signoutMessage.WriteQueryString());
        }

        public ActionResult SignOutCallback()
        {
            if (Request.IsAuthenticated)
            {
                // Redirect to home page if the user is authenticated.
                return RedirectToAction("Index", "Home");
            }

            return View();
        }
    }

In the DatabaseIssuerNameRegistry it's managing and validating issuer names and thumbprints:

    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);
        }
    }

It's using this "Server=(localdb)\v11.0;Integrated Security=true;" connection string to create the IssuingAuthorityKeys and Tenants tables on the local server database.

The CVRController class that all controllers derive from has this method:

[AllowAnonymous]
[CVRAuthorize(Roles = "*", Users = "*")]
public ViewResult AccessDenied()
{
    return View("~/Views/Shared/AccessDenied.cshtml");
}

In CVRAuthorizeAttribute.cs it extends the authorize attribute:

public class CVRAuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
    {
        public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
            if (filterContext.Result is HttpUnauthorizedResult) // && filterContext.HttpContext.User.Identity.IsAuthenticated)
            {
                try
                {
                    CVRController controller = filterContext.Controller as CVRController;
                }
                catch { }

                filterContext.Result = this.AccessDeniedResult;
            }
        }

        private RedirectResult AccessDeniedResult
        {
            get
            {
                return new RedirectResult("~/Home/AccessDenied");
            }
        }
    }

It's redirecting to the Access Denied page for unauthenticated users.

For Azure Storage I've simply updated the key values in the web.config: AzureStorage:ContainerName, AzureStorage:AccountName, and AzureStorage:AccessKey to the values specified by the customer.

I've checked the .js files and there's no settings related to AAD.

My manager has been given an account in AAD by the customer and created a new tenant, registered an app, and given me a client secret, client id, and tenant id to update the web project files with. The settings are configured in the web.config instead of the appsettings.json where all of the documentation about AAD that I've read has it. In the web.config I've updated the appSettings keys: ida:FederationMetadataLocation, ida:Realm, ida:AudienceUri, ida:ClientID, ida:Password:

<add key="ida:FederationMetadataLocation" value="https://login.microsoftonline.com/{tenant id}/federationmetadata/2007-06/federationmetadata.xml" />
<add key="ida:Realm" value="https://myapp.com" />
<add key="ida:AudienceUri" value="https://myapp.com" />
<add key="ida:ClientID" value="{client id}" />
<add key="ida:Password" value="{client secret}" />

AudienceUris in identityConfiguration:

<audienceUris>
    <add value="https://myapp.com" />
</audienceUris>

Issuer and realm attributes for the wsFederation:

<federationConfiguration>
    <cookieHandler requireSsl="true" />
    <wsFederation passiveRedirectEnabled="true" issuer="https://login.microsoftonline.com/{tenant id}/wsfed" realm="https://myapp.com" requireHttps="true" />
</federationConfiguration>

As well as updating the connection strings to our database.

The site is running, and before we starting using a SSL certificate it was showing error 404 in the browser and the IIS logs show status code 302, trying to redirect to another location. Now we've got it to the point that it redirects to URL ".../AccessDenied" page and says, "Sorry, but you do not have the appropriate access permissions to view the page you requested. If you feel this to be in error, please contact customer support. Alternatively, you can Sign out and sign in as a different user." Signing out gives error "System.NullReferenceException: Object reference not set to an instance of an object." in the AccountController.cs file on line:

FederatedAuthentication.SessionAuthenticationModule.SignOut();

Obviously this is because I never signed in. What I find odd is when I run the web app on my local system it redirects me to the "https://login.microsoftonline.com/{tenant id}/wsfed?wa=wsignout1.0&wreply=https%3a%2f%2flocalhost%3a44301%2fAccount%2fSignOutCallback&wtrealm=http%3a%2f%2flocalhost%3a44301" instead of giving me the error that the hosted site does.

There must be something wrong with the configuration in the AAD portal or in the configuration settings of the project that isn't redirecting to the (https://login.microsoftonline.com) login page. The site currently hosted by the customer always redirects to the Microsoft login page for unauthenticated users.

I got the login info from my manager for the Azure portal, I checked the configuration of the newly created App registration against the configuration of the current Tenant's App registration and found a few differences. In the Authentication ours had Mobile and desktop applications as the platform instead of Web with the redirect URIs set as each site's URI . In the Expose an API ours had the Application ID URI as "api://{client id}" instead of the site's URI.

I couldn't add Web as a platform with our site's URI initially because HTTP scheme is not allowed. The site on our server now has a signed SSL certificate so I was able to add the Web platform with our site's URI.

When I tried to update the Application ID URI to our site's URI it says, "Failed to update Application ID URI application property. Error detail: Values of IdentifierUris property must use a verified domain of the organization or its subdomain: https://myapp.com". I checked the custom domain names and it has "{customer name}outlook.onmicrosoft.com" as the only one which is not our domain name and must've been what was automatically entered when my manager created the new tenant as theirs has domain "{customer name}.com". This appears to be important as it does not match the settings of the current configuration for the customers web app. I've also read that the Application ID URI doesn't need to be set to our site's URI because it's using a client secret. This is unclear because of conflicting information I've gathered.

What else needs to be done to the configuration to get the login working? Any suggestions or links to guides would be much appreciated and I'll answer any questions that I can.

I've read through the Microsoft documentation and nothing I've read sheds light on our issue. From all the information I have found our configuration settings appear to be correct, though something is clearly not set correctly. I'm coming up empty on Google searches for any more information about this issue. I've gone through the Web app tutorial and Configure Azure AD Login documents. I'm not finding anymore information about what else I can try.

Microsoft Identity Manager
Microsoft Identity Manager
A family of Microsoft products that manage a user's digital identity using identity synchronization, certificate management, and user provisioning.
607 questions
Microsoft Entra ID
Microsoft Entra ID
A Microsoft Entra identity service that provides identity management and access control capabilities. Replaces Azure Active Directory.
19,383 questions
0 comments No comments
{count} vote

2 answers

Sort by: Most helpful
  1. Shweta Mathur 27,141 Reputation points Microsoft Employee
    2023-05-18T08:15:54.6533333+00:00

    Hi @Keevan ,

    Thanks for reaching out.

    I understand you are trying to integrate a web app with Azure AD using WS-Federation but getting error as page is not redirecting to login.microsoftonline.com while trying to authenticate the users in Azure AD.

    As you mentioned, you register the application in Azure AD.

    1. For the App ID URI, enter https://<your_tenant_name>/<registered application name>, replacing <your_tenant_name> with the name of your Azure AD tenant.

    In the web.config file , you need to add

    1. ida:Tenant and replace the value with your AAD tenant name.
    2. ida:Wtrealm and replace the value with the App ID URI mentioned above.
      <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="ida:Wtrealm" value="[Enter the App ID URI of your application https://contoso.onmicrosoft.com/WebApp-DotNet]" />
        <add key="ida:AADInstance" value="https://login.microsoftonline.com" />
        <add key="ida:Tenant" value="[Enter tenant name, e.g. contoso.onmicrosoft.com]" />
      </appSettings>
    
    

    There is an old reference sample to integrate Azure AD with WS-Federation which you can refer to but there are many changes in the portal settings and application libraries which got updated with time.

    Reference: https://github.com/Azure-Samples/active-directory-dotnet-webapp-wsfederation

    Hope this will help.

    Thanks,

    Shweta


    Please remember to "Accept Answer" if answer helped you.


  2. Jussi Palo 6 Reputation points
    2023-06-12T08:42:50.45+00:00

    It was TLS1.2 requirement that caused this error. Adding support for Tls1.2 fixed the error.

    // add this
    System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
    
    // before this
    IssuingAuthority issuingAuthority = GetIssuingAuthority(metadataLocation);
    
    0 comments No comments