hullflyer
hullflyer

Reputation: 167

Azure AD Multi-Tenant + Authentication-Filtering Best Practices

I am new to C# and Azure AD, so please give me a nudge toward some guides and resources to figure out how best to only allow SSO and authorization to my MVC app for my approved list of customers. I have a small app I host on an Azure VM and have single tenant Azure AD working using OpenID Connect. I made the app registration multi-tenant in Azure AD. I set up a new org account with some users to test against in Azure AD. I read this guide, which is good but stops short noting in its examples

issuer validation is disabled to enable any Azure AD tenant to sign in

I can't find guide for how to qualify the tenant as an org I want to access my app. What's the best way to authorize users from, say, domain CustomerCompany.com but not SomeOtherCompany.com even if both have Azure AD org accounts?

I do not want to maintain the list of users, so the Azure AD collaboration B2B stuff does not fill the bill (uploading lists of users and issuing invites). I would like the customer to maintain their own list of users.

I want to first simply allow users from my customers and deny all others.

If the customer wants to restrict usage of my app to certain people in their org is it best practice to:

Any how-to and best practice guidance you can provide a beginner here would be appreciated. Thank you.

Upvotes: 2

Views: 1002

Answers (2)

hullflyer
hullflyer

Reputation: 167

Thank you. I am also answering my own Q to add some more detail to how I have progressed (hope it helps some reader) and to ask a more detailed question to get to the nub of it.

I now have a way to compare an incoming authenticated user's graph data to a list of customer company names I keep. But this is not ideal. It depends on the graph api getting the customer tenant's DisplayName, which should match my list. I fear that might change and then disallow legit customers. I would prefer to use the Tenant ID from Azure AD. But I do not know how to get that immediately on signup. I can get that after the first user authenticates from that tenant, but how do I get the customer's Tenant ID before anyone from that company authenticates? Do I have to ask for it, or can I get that on my own?

Is there a better way to filter access to my web app (IIS) to allow only my customers? That is my detailed Q.

Please help a beginner figure out how the big boys authorize only their customers to an IIS web app. Here is the beginning of how I do it (cheating with pseudo code in the customer filtering part because I'd like to compare a GUID like TenantID (Issuer?) rather than a string like DisplayName):

public partial class Startup
{
    private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
    private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
    private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];

    private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
    public static string custTenantID = "";

    //for multi tenant sso, can be 'common' for consumer, 'organization' for work+school, or tenantID for single company
    public static readonly string Authority = aadInstance + "common/";

    //this is my model for the customer db
    public CustomerTenant tenant = new CustomerTenant();

    // This is the resource ID of the AAD Graph API.  We'll need this to request a token to call the Graph API.
    string graphResourceId = "https://graph.windows.net";


    public void ConfigureAuth(IAppBuilder app)
    {       
        ApplicationDbContext db = new ApplicationDbContext();

        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                ClientId = clientId,
                Authority = Authority,
                PostLogoutRedirectUri = postLogoutRedirectUri,


                //for multi-tenant
                TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
                {
                    // instead of using the default validation (validating against a single issuer value, as we do in line of business apps)
                    // we inject our own multitenant validation logic
                    ValidateIssuer = false,
                },

                Notifications = new OpenIdConnectAuthenticationNotifications()
                {
                   // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.

//AuthorizationCodeReceived is invoked after SecurityTokenValidated
//using this Notification to inject logic to authorize only my customers
                   AuthorizationCodeReceived = (context) =>
                   {
                       var code = context.Code;
                       ClientCredential credential = new ClientCredential(clientId, appKey);
                       string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
                         Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));

                       AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                        code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);

//look up the customer tenant's DisplayName to compare to my list
                       Uri servicePointUri = new Uri(graphResourceId);
                       Uri serviceRoot = new Uri(servicePointUri, custTenantID);
                       ActiveDirectoryClient client = new ActiveDirectoryClient(serviceRoot, async () => { return await Task.FromResult(result.AccessToken); });
                       string tenantName = client.TenantDetails.ExecuteAsync().Result.CurrentPage.First().DisplayName;

//pseudo-code:  compare this tenantName to db.CustomerTenants.CustomerName and if there is a match then authorize else do not authorize

                       return Task.FromResult(0);

                   },


                    //for multi-tenant sso
                    RedirectToIdentityProvider = (context) =>
                    {
                        string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
                        context.ProtocolMessage.RedirectUri = appBaseUrl;

                        context.ProtocolMessage.PostLogoutRedirectUri = postLogoutRedirectUri;
                        return Task.FromResult(0);
                    },


                    SecurityTokenValidated = (context) =>
                    {
                        // retrieve caller data from the incoming principal
                        string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
                        string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
                        custTenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;


                        return Task.FromResult(0);
                    },

                    AuthenticationFailed = (context) =>
                    {

                   context.OwinContext.Response.Redirect("urlToMyErrorPage?message=" + context.Exception.Message);
                        context.HandleResponse(); // Suppress the exception
                        return Task.FromResult(0);
                    }
            }
        });

    }

Upvotes: 0

Fei Xue
Fei Xue

Reputation: 14649

Is there a way I can create a relationship between my org and others in Azure AD and somehow allow/deny authorization to my app using those relationships at the AAD login?

Is there a tenantid value that comes from Azure AD that I can compare to a list I keep in SQL server to allow/deny authorization after AAD login? Is that typically a domain name or some GUID or other value I have to get from the customer?

Yes. We are able to write the custom code to verify the iss claim from token to make it meet the business logic. Here is the code using the OpenIdConnect OWIN component for your reference:

 app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                ClientId = ClientId,
                Authority = Authority,
                TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
                {
                    // instead of using the default validation (validating against a single issuer value, as we do in line of business apps), 
                    // we inject our own multitenant validation logic
                    ValidateIssuer = false,
                },
                Notifications = new OpenIdConnectAuthenticationNotifications()
                {
                 
                    // we use this notification for injecting our custom logic
                    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);
                    }
                }
            });

And here is a helpful code sample for your reference.

Have the customer register my app or somehow assign it to groups they set up in their own Azure AD so the user won't authenticate if not allowed by their admin? OR

Query the incoming user profile at my server for some value (Group Membership, Department, Manager, etc.) using Graph API and allow/deny based on that value?

Based on my understanding, it should let the custom company to manage the users who have the access to visit your application because the users management is responsibly of the partners' company. So after the partners' company decide to enable/disable users, your company and application doesn't require make and additional work or changing.

And to manage the users who can access the application, the partners' company can use the Requiring User Assignment feature.

Upvotes: 2

Related Questions