Categories
Multi-tenant Azure AD Application

Hello, Multi-tenant World !

SERIES:
End-to-end walkthrough: How to Multi-tenant Application, register it with Azure AD, onboard tenants, go through the admin consent workflow, work with incremental [just-in-time] consent. All the code we write will be in C# and the application is an ASP.NET MVC application.

This is Step 4 of the series. To navigate to other articles in this series, look at the end of any article in the series for the list.


In this chapter, we wire it all up in a very basic way and test that our application runs and can indeed accept login from any tenant. While you can technically login to any “xxxxxxx.onmicrosoft.com” Azure AD tenant with your app, let’s restrict ourselves to our own AD for this chapter.

The first change you want to make is in your appsettings.json file. Ensure your file looks like this:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "AzureAd": {
    "TenantId": "organizations",
    "ClientId": "",
    "ClientSecret": "",
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "",
    "CallbackPath": "/signin-oidc"
  }
}

Now, remember the “multitenant app credentials” text file we created in Chapter 1 ? Open that Notepad file and paste in the Client ID and Client Secret values against the entries in appsettings.json. The value for Domain should be the fully qualified “.onmicrosoft.com” domain for your tenant [eg: “contoso.onmicrosoft.com”].

Note that instead of specifying our Tenant’s ID here, we are using “organizations“. If you replace “organizations” with the Guid you have in your “multitenant app credentials” file, your app will authenticate only with your own tenant [or with whatever tenant is identified by that Guid]. Other possible values here are: “common” [for “Microsoft Account” logins] and “consumer” [which is same as “common”].

If you look through our code even after we are completely done, you will not see a route or reference to “/signin-oidc” anywhere. This path is handled deep within the ASP.NET Core authentication system and needs to be exactly “/signin-oidc” or it will not work [then the OpenID system that we are using will expect you to supply that mechanism!].


Now we move on to Startup.cs. We add the following to Configure():

//app.UseHsts();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseCookiePolicy();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

Within the existing app.UseEndpoints() statement, add this:

endpoints.MapRazorPages();

Without this MapRazorPages() statement, the Azure AD UI library’s system will fail. Those are implemented using ASP.NET Razor Pages while the rest of our app is MVC.

Now, your Configure() method should look like this:

public void Configure(IApplicationBuilder app)
{
       //app.UseHsts();
       app.UseHttpsRedirection();
       app.UseStaticFiles();
       app.UseSession();
       app.UseCookiePolicy();
       app.UseRouting();
       app.UseAuthentication();
       app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}"
        );

        // Required for the AzureAD Auth Module
        endpoints.MapRazorPages();
    });
}

What did we do there? We simply wired up the most basic of all ASP.NET Core MVC stuff. Hsts is essentially for tamper protection. You can remove it if you don’t use tamper-protection cookies etc in your forms. We need sessions and cookies to be able to login, routing for MVC to work. Without Authentication and Authorization, we might as well have a dumb-app that cannot login anywhere.

Now we go into ConfigureServices() where what we added above [and more] are configured:

You will notice that in my code in ConfigureServices(), I have this:

services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => false;
    options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
    options.ConsentCookie = new Microsoft.AspNetCore.Http.CookieBuilder()
    {
        IsEssential = true
    };
});

On the first line, I have turned off the cookie consent process. If you use the template code, your users will see a bar on the UX asking them to accept the use of cookies. Since there is a ton of code that is unrelated to our app, I am ignoring all of that and simply setting the need for cookie-consent to FALSE, meaning all cookies will be allowed on this app.

Below this, we add and configure our Session (note that we are setting our session to the standard 30 minute timeout):

services.AddSession(options =>
  {
    options.IdleTimeout = new System.TimeSpan(0, 30, 0);
    options.Cookie.IsEssential = true;
  }
);

Next, we have to plug in our Azure AD auth system. This code is long. But we can easily get this right. First thing to do is get a reference to the “AzureAd” section we added in our appsettings.json at the start of this chapter.

IConfigurationSection azureAdSection = GLOBALS.Configuration.GetSection("AzureAd");

Once we have that, we bind those values with a whole bunch of things:

services                
  .AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(
      options => azureAdSection.Bind(options)
    );

services.Configure<AzureADOptions>(
  options => azureAdSection.Bind(options)
);

services.Configure<ConfidentialClientApplicationOptions>(
  options => azureAdSection.Bind(options)
);

You will start noticing squiggles under a lot of that code. Set focus on each squiggle, hit CTRL+. (that is, CTRL and ‘.’ key), apply the first “using…” statement suggested in the popup.

Below that, add this block:

services.Configure<OpenIdConnectOptions>(
  AzureADDefaults.OpenIdScheme,
  (options) =>
  {
    options.Authority += "/v2.0/";
    if (options.Authority.Contains("/common/"))
    {
      options.Authority = options.Authority.Replace("/common/", "/organizations/");
    }

    options.ResponseType = OpenIdConnectResponseType.Code;

    foreach (string item in CONSTANTS.DEFAULT_AUTH_SCOPES)
    {
      options.Scope.Add(item);
    }
                    
    options.TokenValidationParameters.ValidateIssuer = false;
    options.TokenValidationParameters.NameClaimType = "preferred_username";
  }
);

We will revisit this block in a later chapter and add a lot more code. Currently what we are doing is, we force OAuth 2.0 authentication by suffixing the “/v2.0/” path to the Authority string. Without this, a lot of our code is going to fail. We also replace the word “common” in the Authority string with “organizations” because we want to restrict logins only to organizational tenants.

Notice the foreach-loop? We are adding the scope constants from the CONSTANTS.cs file we defined earlier. This basically adds the mandatory OpenID scopes of profile, email and offline_access. Without this set, all authentication requests to Azure AD will fail.

We have set ValidateIssuer to false. This is because we want to allow login from tenants other than our own. Don’t worry about this one at present. We are going to write our own Issuer Validation code and plug it in here later.


At this point, your project has been configured with everything it needs to be able to login someone. But, how?

Let’s open the Controllers/HomeController.cs file. We are going to add a temporary action here to let us login. Add the following code:

[Authorize, HttpGet("/login")]
public IActionResult AuthenticatedIndex()
{
  return View("Homepage");
}

The code only returns the same homepage view as before. But notice the Authorize annotation above it. This instructs the ASP.NET Core engine to ensure the user is logged in before attempting to run the code within.

We will also need to make one more change to that file before things will work. Notice the AllowAnonymous above the class declaration? Move it into the Index action’s HttpGet attribute. Your finished class should look like this:

[Controller]
public class HomeController : Controller
{

  [AllowAnonymous, HttpGet("/")]
  public IActionResult Index()
  {
    return View("Homepage");
  }

  [Authorize, HttpGet("/login")]
  public IActionResult AuthenticatedIndex()
  {
    return View("Homepage");
  }

}

There is no menu item or onscreen element yet to trigger this action, we will have to do it manually [and let’s keep it like that, shall we?]. So, hit F5 to run the project.

Now, open an Incognito / Private window of any web browser on the same PC/laptop and visit the same URL you went to when you hit F5 above. Once the homepage is displayed, add “/login” to go there. For example, on my system the app is running on “https://localhost:44347/“, which means I have to go to “https://localhost:44347/login“.

While you can try logging in at this time [you will also see consent prompts], our code is not currently set up to handle the results of the login workflow. We will do that in the next chapter.

If you get this far, your code has been set up correctly and we are ready to do more complicated things.

The code base at this time is this changeset.


Leave a Reply

Your email address will not be published. Required fields are marked *