6
votes

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

Now we have requirement to extend this authentication model and allow external Azure AD user to sign into web application for configured tenant. I have figured out everything on Azure AD side. Thanks to microsoft github example here

Now both Individual account authentication and Azure AD are working well independently. But its 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 doesnt work for AAD. To enable AAD authentication I need to configure UseCookieAuthentication part as below. Which will break my local user account authentication.

app.UseCookieAuthentication(new CookieAuthenticationOptions { });

Basically I need to remove middleware of local users 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 [Authoroze] attribute. Its calling event SecurityTokenValidated and I am able to get all AAD claims and able to validate against my custom tenant. But only when at the end I redirect to root of my app which is secured action, its throwing back to my custom login page. Seems its not internally signing in user and not creating necessary authentication cookies.

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

Thanks

2
I am testing it using the code above, it works well for me. Since you mentioned that your web app was using token authentication, and after you enable the Azure Active Directory authentication, it break the individual account authentication. Can you please describe about how you implement the individual accounts authentication? Based on my understanding, the individual accounts authentication also use the cookie authentication by default.Fei Xue - MSFT
So I already had individual user account enabled in my application with same we as default MVC template provides. With code above which enables Azure AD authentication, individual accounts working fine. But Azure AD is not working. Azure AD triggers SecurityTokenValidated event where i can get all the user claims, but at the end when i redirect to root of application which is secured action, it returns back 401. Only when I set up app.UseCookieAuthentication with blank option my Azure AD works fine, but that breaks individual account case.paresh.bijvani
Since the code works well for me, suspect the issue may caused by other code. I have upload the code sample here. If you still have the problem, you may share a runnable code sample to help to reproduce this issue.Fei Xue - MSFT
Thanks. I will check this sample my end. will update you.paresh.bijvani
thanks @FeiXue-MSFT. Your example worked for me. I was missing some wire up in account controller. Only question i have, when you log out from app signed in by azure, app redirects to azure screen for log out and redirect back to application login page. This clears probably all azure cookies and next time you login it asks you azure credential again. But in this sample we are not getting this behaviour. After logout if we login again, we are in without entering credentials. Is there a way to configure other behaviour?paresh.bijvani

2 Answers

2
votes

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");
}
0
votes

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.