3

I have an ASP.NET Core web app that is authenticating with Azure AD in a multi-tenant configuration using Microsoft.Identity.Web. We use a tenant/company identifier as the subdomain of our apps URL. (companyA.myapp.com, companyB.myapp.com). Some users have access to more than one tenant of the application, so we cannot map a Azure AD tenant directly to a single tenant/company in our app.

With Microsoft.Identity.Web, how is the state parameter set or manipulated as described here? I would like to follow the guidance provided here, but am not sure where to start. https://learn.microsoft.com/en-us/azure/active-directory/develop/reply-url#use-a-state-parameter

If you have several subdomains and your scenario requires that, upon successful authentication, you redirect users to the same page from which they started, using a state parameter might be helpful.

In this approach:

  1. Create a "shared" redirect URI per application to process the security tokens you receive from the authorization endpoint.
  2. Your application can send application-specific parameters (such as subdomain URL where the user originated or anything like branding information) in the state parameter. When using a state parameter, guard against CSRF protection as specified in section 10.12 of RFC 6749).
  3. The application-specific parameters will include all the information needed for the application to render the correct experience for the user, that is, construct the appropriate application state. The Azure AD authorization endpoint strips HTML from the state parameter so make sure you are not passing HTML content in this parameter.
  4. When Azure AD sends a response to the "shared" redirect URI, it will send the state parameter back to the application.
  5. The application can then use the value in the state parameter to determine which URL to further send the user to. Make sure you validate for CSRF protection.
Benjamin Brandt
  • 367
  • 3
  • 13
  • I did find this related post: https://stackoverflow.com/a/65973879/5167537 But, this seems to explain how to only change the domain, but I feel like I'm going to need to redirect back to the subdomain (https://companyA.myapp.com/widget/342) after redirecting to the common endpoint (maybe https://signin.myapp.com). I'm going to fiddle with the context in the `OnRedirectToAuthorizationEndpoint ` and will post an answer here if I get it solved. – Benjamin Brandt Sep 17 '21 at 15:49
  • A solution with MSAL.js: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/2139 – Benjamin Brandt Sep 27 '21 at 13:44

1 Answers1

2

Here is how I eventually solved the the MS Login infinite redirects with the tenant per subdomain scheme problem. (Trying to come up with a better name for the problem. )

Provide a SigninRedirect controller action that accepts a returnUrl parameter that we must validate to avoid being an Open Redirect.

Example URL with returnUrl set to companyA.example.com/foo&bar=1:

https://signin.example.com/signin-redirect?returnUrl=companyA.example.com%2Ffoo%26bar%3D1
[Route("")]
public class SigninController : Controller
{
    private readonly IMediator _mediator;
    private readonly IConfiguration _configuration;
    public SigninController(IMediator mediator, IConfiguration configuration)
    {
        _mediator = mediator;
        _configuration = configuration;
    }
    [Authorize]
    [HttpGet("/signin-redirect")]
    public IActionResult SigninRedirect(string returnUrl)
    {
        string redirect;
        if (!string.IsNullOrEmpty(returnUrl) && IsValidSubdomainUrl(returnUrl))
        {
            redirect = returnUrl;
        }
        else
        {
            var appHost = _configuration.GetValue<string>("General:ApplicationHost");
            var home = new UriBuilder("https", appHost).Uri;
            return Redirect(home.AbsoluteUri);
        }
        return Redirect(redirect);
    }
    /// <summary>
    /// Avoid Open Redirect Vulnerability
    /// https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
    /// </summary>
    /// <param name="returnUrl"></param>
    /// <returns></returns>
    private bool IsValidSubdomainUrl(string returnUrl)
    {
        var appHost = _configuration.GetValue<string>("General:ApplicationHost");
        var isValid = Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute) &&
            Uri.TryCreate(returnUrl, UriKind.Absolute, out var uri) &&
            uri?.Host.EndsWith(appHost) == true;
        return isValid;
    }
}

In ConfigureServices(IServiceCollection services) set cookies to be shared across all the subdomains and configure an OnRedirectToIdentityProvider event to redirect to your signin URL when the user is not yet authenticated:


var cookieDomain = _configuration.GetValue<string>("General:CookieDomain");

services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromDays(3);
    options.Cookie.Domain = cookieDomain;
    options.Cookie.Path = "/";
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options =>
    {
        _configuration.Bind("AzureAd", options);
        options.Events ??= new OpenIdConnectEvents();
                        options.Events.OnRedirectToIdentityProvider += context => {
            var originalRequestUri = context.HttpContext.Request.GetUri();

            var signInHost = _configuration.GetValue<string>("General:SignInHost");
            var signInPath = _configuration.GetValue<string>("General:SignInUrl");

            if (!originalRequestUri.Host.Equals(signInHost, StringComparison.InvariantCultureIgnoreCase))
            {
                var signInUrl = QueryHelpers.AddQueryString(signInPath, "returnUrl", originalRequestUri.AbsoluteUri);

                // When on a subdomain and not authorized, then redirect to
                // our signin URL.
                context.Response.Redirect(signInUrl);

                // Let Microsoft.Identity.Web know that we already handled 
                // this redirect
                context.HandleResponse();
            }

            return Task.CompletedTask;
        };
    },
    cookieOptions => 
    {
        cookieOptions.Cookie.Domain = cookieDomain;
        cookieOptions.Cookie.Path = "/";
        cookieOptions.Cookie.SameSite = SameSiteMode.Lax;
    })
    .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" })
    .AddDistributedTokenCaches();

services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Domain = cookieDomain;
    options.Cookie.Name = ".AspNet.SharedCookie";
    options.Cookie.Path = "/";
    options.Cookie.SameSite = SameSiteMode.Lax;
});

Configuration defined:

"General:CookieDomain": ".example.com",
"General:ApplicationHost": "example.com",
"General:SignInHost": "signin.example.com",
"General:SignInUrl": "https://signin.example.com/signin-redirect"

In addition, you will need the usual AzureAd configuration section from the Microsoft Docs for Azure AD.

"AzureAd:Instance": "https://login.microsoftonline.com/",
"AzureAd:Domain": "...",
"AzureAd:TenantId": "common",
"AzureAd:ClientId": "...",
"AzureAd:ClientSecret": "...",
"AzureAd:CallbackPath": "/signin-oidc",
"AzureAd:SignedOutCallbackPath": "/signout-callback-oidc",

Subdomain Per Tenant Login Flow Sequence Diagram

Benjamin Brandt
  • 367
  • 3
  • 13
  • I don't really like that there's the double redirect in the flow, so if anyone comes up with something better, please let me know. – Benjamin Brandt Oct 15 '21 at 16:04