asp.net-coreauthenticationazure-active-directorymulti-tenantmicrosoft-identity-platform

How to select specific app registration based on tenant ID in ASP.NET Core MVC with Microsoft Identity Platform?


I have an ASP.NET Core MVC app using the Microsoft Identity Platform for authentication, currently set up for multi-tenancy with one app registration. I want to extend this to support two separate app registrations (for two different tenants) and allow users from both tenants to authenticate, selecting the appropriate app registration dynamically based on the tenant ID.

How can I configure Program.cs to achieve this?

Current appsettings.json:

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "******.onmicrosoft.com",
    "TenantId": "**********",
    "ClientId": "**********",
    "CallbackPath": "/signin-oidc",
    "ClientSecret": "**********"
},
"AllowedTenants": [ "**********", "**********" ],
"DownstreamApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "User.Read"
}

Desired appsettings.json with two app registrations:

"AzureAdOne": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "******.onmicrosoft.com",
    "TenantId": "**********",
    "ClientId": "**********",
    "CallbackPath": "/signin-oidc",
    "ClientSecret": "**********"
},
"AzureAdTwo": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "******.onmicrosoft.com",
    "TenantId": "**********",
    "ClientId": "**********",
    "CallbackPath": "/signin-oidc",
    "ClientSecret": "**********"
},
"AllowedTenants": [ "**********", "**********" ],
"DownstreamApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "User.Read"
}

current Program.cs

string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');

// Sign-in users with the Microsoft identity platform
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options =>
    {
        Configuration.Bind("AzureAd", options);
        options.Events.OnTokenValidated = async context =>
        {
            string tenantId = context.SecurityToken.Claims.FirstOrDefault(x => x.Type == "tid" || x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")?.Value;

            var allowedTenants = Configuration.GetSection("AllowedTenants").Get<string[]>().ToList();

            if (string.IsNullOrWhiteSpace(tenantId) || !allowedTenants.Contains(tenantId))
                throw new UnauthorizedAccessException("Unable to get tenantId from token or the tenant is not authorized.");
        };
    })
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
    .AddInMemoryTokenCaches();

In the login partial, users can select their company:

<a class="dropdown-item" href="/MicrosoftIdentity/Account/SignIn?scheme=TenantIdOne">Company One</a>
<a class="dropdown-item" href="/MicrosoftIdentity/Account/SignIn?scheme=TenantIdTwo">Company Two</a>

What’s the best way to implement this in Program.cs to dynamically select the app registration based on the tenant?


Solution

  • Based on this article, one can answer this question as follows:

    To configure an ASP.NET Core MVC app to support authentication with two different app registrations, you can define multiple OpenID Connect schemes. Here’s how to set it up in your Program.cs, appsettings.json, and controller.

    Step 1: Configure Program.cs

    In the Program.cs, we set up two authentication schemes, appOne and appTwo, which correspond to two different app registrations (one for each tenant).

    builder.Services.AddAuthentication(options =>
    {
        // Set Cookie as the default scheme, which manages user sessions within the app.
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    
        // DefaultChallengeScheme indicates that we want to challenge users using OpenID Connect for login.
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie() // This handles user session management with cookies.
    .AddOpenIdConnect("appOne", options =>
    {
        // Bind configuration settings for the first app registration.
        builder.Configuration.GetSection("AppOne").Bind(options);
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    
        // Use Authorization Code flow, which is the standard flow for server-side apps.
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.SaveTokens = true;
    
        // Retrieves claims (user profile details) from the user info endpoint.
        options.GetClaimsFromUserInfoEndpoint = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name" // Specifies how to extract the user's name from claims.
        };
        options.MapInboundClaims = false; // Ensures custom claims mapping.
    })
    .AddOpenIdConnect("appTwo", options => 
    {
        // Bind configuration settings for the second app registration.
        builder.Configuration.GetSection("AppTwo").Bind(options);
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name"
        };
    });
    

    Step 2: Define Configuration in appsettings.json

    In the appsettings.json file, add separate sections for AppOne and AppTwo. This allows each OpenID Connect scheme to retrieve its specific settings, such as Authority, ClientId, ClientSecret, and callback paths.

    {
      "AppOne": {
        "Authority": "https://login.microsoftonline.com/{tenantIdOne}",
        "ClientId": "your-client-id-for-app-one",
        "ClientSecret": "your-client-secret-for-app-one",
        "CallbackPath": "/signin-appOne",
        "SignedOutCallbackPath": "/signout-callback-appOne"
      },
      "AppTwo": {
        "Authority": "https://login.microsoftonline.com/{tenantIdTwo}",
        "ClientId": "your-client-id-for-app-two",
        "ClientSecret": "your-client-secret-for-app-two",
        "CallbackPath": "/signin-appTwo",
        "SignedOutCallbackPath": "/signout-callback-appTwo"
      }
    }
    

    Step 3: Create Login Methods in the Controller

    In the controller, create two separate actions for each app. Each action initiates the authentication challenge for the specified scheme, either appOne or appTwo.

    [AllowAnonymous]
    [HttpGet]
    public ActionResult LoginAppOne(string returnUrl)
    {
        // Redirect to appOne's authentication challenge.
        return Challenge(new AuthenticationProperties
        {
            RedirectUri = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/", 
        }, "appOne");
    }
    
    [AllowAnonymous]
    [HttpGet]
    public ActionResult LoginAppTwo(string returnUrl)
    {
        // Redirect to appTwo's authentication challenge.
        return Challenge(new AuthenticationProperties
        {
            RedirectUri = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/"
        }, "appTwo");
    }
    

    Step 4: Add Links in the View

    To let users select the correct login option, add links in the view that call each login method:

    <li class="nav-item">
        <a class="nav-link text-dark" href="~/api/Account/LoginAppOne">Login with App One</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" href="~/api/Account/LoginAppTwo">Login with App Two</a>
    </li>
    

    How It Works:

    This setup gives users the choice to log in through either app registration, dynamically adjusting based on their selection.