paresh.bijvani
paresh.bijvani

Reputation: 233

ASP.NET MVC Identity - Using internal User and Azure AD Authentication together

We are already running an ASP.NET MVC web application which is using internal users via token authentication. This is implemented in the standard way the ASP.NET MVC template provides.

Now we have a requirement to extend this authentication model and allow external Azure AD users to sign into the web application for configured tenant. I have figured out everything on the Azure AD side. Thanks to Microsoft's Azure Samples example.

Now both individual account authentication and Azure AD are working well independently. But they're not working together. When I insert both middleware together its giving issue.

Here's my startup_auth.cs file:

public partial class Startup
{
    
    public void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);


        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });            
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);


        string ClientId = ConfigurationManager.AppSettings["ida:ClientID"];            
        string Authority = "https://login.microsoftonline.com/common/";

    app.UseOpenIdConnectAuthentication(
        new OpenIdConnectAuthenticationOptions
        {
            ClientId = ClientId,
            Authority = Authority,
            TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
            {                        
                ValidateIssuer = false,
            },
            Notifications = new OpenIdConnectAuthenticationNotifications()
            {
                RedirectToIdentityProvider = (context) =>
                {                            
                    string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;                         
                    context.ProtocolMessage.RedirectUri = appBaseUrl;
                    context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
                    return Task.FromResult(0);
                },                        
                SecurityTokenValidated = (context) =>
                {
                    // retriever caller data from the incoming principal
                    string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
                    string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
                    string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;

                    if (
                        // the caller comes from an admin-consented, recorded issuer
                        (db.Tenants.FirstOrDefault(a => ((a.IssValue == issuer) && (a.AdminConsented))) == null)
                        // the caller is recorded in the db of users who went through the individual onboardoing
                        && (db.Users.FirstOrDefault(b =>((b.UPN == UPN) && (b.TenantID == tenantID))) == null)
                        )
                        // the caller was neither from a trusted issuer or a registered user - throw to block the authentication flow
                        throw new SecurityTokenValidationException();                            
                    return Task.FromResult(0);
                },
                AuthenticationFailed = (context) =>
                {
                    context.OwinContext.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
                    context.HandleResponse(); // Suppress the exception
                    return Task.FromResult(0);
                }
            }
        });

    }
}

This configuration works well for local user accounts but doesn't work for AAD. To enable AAD authentication, I need to configure the UseCookieAuthentication part as shown below, which will break my local user account authentication.

app.UseCookieAuthentication(new CookieAuthenticationOptions { });

Basically I need to remove the local users middleware to get AAD work.

What I mean by AAD not working is, I am not able to go to any secured action which is protected by the [Authorize] attribute. It's calling the SecurityTokenValidated event, and I am able to get all AAD claims and able to validate against my custom tenant. But when I redirect to root of my app (which is a secured action) at the end, it throws me back to my custom login page. Seems it's not internally signing in the user and not creating the necessary authentication cookies.

I would appreciate any ideas on what I could be missing here.

Thanks

Upvotes: 6

Views: 4676

Answers (2)

Fei Xue
Fei Xue

Reputation: 14649

To support both individual accounts and other account from social data provider, only need to add them using OWIN component.

And to sign-out the users which login from Azure AD, we need to sign-out both the cookie issued from web app and Azure AD. First, I modified the ApplicationUser class to add the custom claim to detect whether the users login from Azure AD or individual accounts like below.

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        if((this.Logins as System.Collections.Generic.List<IdentityUserLogin>).Count>0)
            userIdentity.AddClaim(new Claim("idp", (this.Logins as System.Collections.Generic.List<IdentityUserLogin>)[0].LoginProvider));
        return userIdentity;
    }
}

Then we can change the LogOff method to support sign-out from Azure AD to clear the cookies from Azure AD:

// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{

    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);

    var idpClaim = ClaimsPrincipal.Current.Claims.FirstOrDefault(claim => { return claim.Type == "idp"; });
    if (idpClaim!=null)
        HttpContext.GetOwinContext().Authentication.SignOut(
            OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);

    return RedirectToAction("Index", "Home");
}

Upvotes: 2

Richard McDaniel
Richard McDaniel

Reputation: 121

It sounds like your OpenID Connect auth is not connecting to your Cookie auth. It looks like you need to specify a SignInAsAuthenticationType in your OpenIdConnectAuthenticationOptions that matches the AuthenticationType in your CookieAuthenticationOptions or your ExternalCookie auth type.

Upvotes: 0

Related Questions