Search code examples
asp.net-coreasp.net-core-identitymicrosoft-identity-platformazure-ad-b2b

Using Azure AD with ASP.NET Core Identity


I am trying to crack the code to use an Azure AD multi-tenant app to sign in to a ASP.NET Core app that uses ASP.NET Core Identity. It's a pretty standard setup but it doesn't seem to accept Microsoft identity. Nothing breaks or no errors, but the Authorize attribute indicates the current user isn't authenticated.

void configureCookieAuthOptions(CookieAuthenticationOptions options)
{
  options.Cookie.IsEssential = true;
  options.ExpireTimeSpan = TimeSpan.FromDays(1);
  options.SlidingExpiration = true;
  options.LoginPath = "/Account/Login";
}

void configureJwtBearerOptions(JwtBearerOptions options)
{
  options.TokenValidationParameters = new TokenValidationParameters
  {
    // snip
  };           
}

services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationIdentityDbContext>()
        .AddClaimsPrincipalFactory<ApplicationUserClaimsPrincipalFactory>()
        .AddUserManager<ApplicationUserManager>()
        .AddUserStore<ApplicationUserStore>()
        .AddDefaultTokenProviders();

services
  .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddCookie(configureCookieAuthOptions)
  .AddJwtBearer(configureJwtBearerOptions);

services.AddAuthorization();

When I add AddMicrosoftIdentityWebApp to the mix, it all works great, except that the Authorize attribute returns false when I attempt to access a protected resource. In fact, it looks like there is no identity in the context:

enter image description here


void configureMicrosoftIdentityOptions(MicrosoftIdentityOptions microsoftOptions)
{
  microsoftOptions.TenantId = "common";
  microsoftOptions.Instance = "https://login.microsoftonline.com/";
  microsoftOptions.Domain = config["Authentication:Microsoft:Domain"];
  microsoftOptions.ClientId = config["Authentication:Microsoft:ClientId"];
  microsoftOptions.ClientSecret = config["Authentication:Microsoft:ClientSecret"];
  //microsoftOptions.SignInScheme = IdentityConstants.ExternalScheme;
  microsoftOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  microsoftOptions.Events = new OpenIdConnectEvents
  {
    OnTokenValidated = async context =>
    {
      var _userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
      var _claimsIdentityFactory = context.HttpContext.RequestServices.GetRequiredService<IUserClaimsPrincipalFactory<ApplicationUser>>();

      ClaimsIdentity claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
      ApplicationUser user = await _userManager.FindByEmailAsync(claimsIdentity.Name);
      if (user == null)
      {
        context.Fail("Not authorized.");
        return;
      }
      
      var id = await _claimsIdentityFactory.CreateAsync(user);
      context.Principal.AddIdentity(id.Identity as ClaimsIdentity);
      claimsIdentity.AddClaims(id.Claims);
      context.Success();
    }
  };
};

services
  .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddCookie(configureCookieAuthOptions)
  .AddJwtBearer(configureJwtBearerOptions)
  .AddMicrosoftIdentityWebApp(configureMicrosoftIdentityOptions, cookieScheme: null); // <-- Added this line

I've tried all sorts of combinations and variations, like changing the default schemes, using different schemes, configure the scheme to external, etc. to no avail.

For the sake of completeness, here's the code that is invoked when I call the external login button:

[AllowAnonymous]
public IActionResult SignInExternal([FromRoute] string scheme, [FromQuery] string redirectUri)
{
  scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
  string redirect = !string.IsNullOrEmpty(redirectUri) && Url.IsLocalUrl(redirectUri) ? redirectUri : Url.Content("Account/SignInSuccess");
  return Challenge(new AuthenticationProperties { RedirectUri = redirect }, scheme);
}

As far as I can see, it's exactly the same story when I swap out AddMicrosoftIdentityWebApp for AddOpenIdConnect. It's a similar story when I use .AddGitHub("Github", options => {}), so there's certainly something missing in my setup.

I am over my head here, so I was wondering what I am missing or understanding incorrectly here.


Solution

  • Not completely sure about the legitimacy of the fix, but I was able to get authenticated with Azure AD, GitHub, and others by setting the OpenIdConnectOptions' SignInScheme to IdentityConstants.ApplicationScheme.

    internal static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder authenticationBuilder, IConfiguration config)
    {
      void configureMicrosoftIdentityOptions(OpenIdConnectOptions options)
      {
        // ...
        options.SignInScheme = IdentityConstants.ApplicationScheme;
        // ...
      
        return authenticationBuilder.AddOpenIdConnect("AzureAD", configureMicrosoftIdentityOptions);
    }