2

I am trying to set up a multi-tenant OpenId authentication with AzureAD on a web page built on the basic .NET Core 2.2 + React-project template from Visual Studio 2019. Core 2.2 because the authentication middleware on 3.0 did not fire at all with any configuration, this seems to be a common problem and the documentation on Core 3.x authentication is scarce and sometimes even contradictory. I think I have tried everything and I am at loss now.

Here the authentication middleware seems to kick in correctly according to the server output when the API is called:

From javascript:
    // Tried also without custom headers and trying to make the middleware handle the thing by itself
    fetch('api/SampleData/WeatherForecasts', {
      method: "get",
      headers: new Headers({
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "true"
      })
    })

Server output:
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:44363/api/SampleData/WeatherForecasts  
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '********.Controllers.SampleDataController.WeatherForecasts (******)'
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
      Route matched with {action = "WeatherForecasts", controller = "SampleData", area = "", page = ""}. Executing controller action with signature System.Collections.Generic.IEnumerable`1[******.Controllers.SampleDataController+WeatherForecast] WeatherForecasts() on controller ******.Controllers.SampleDataController (*********).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
      Authorization failed.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
      Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
      Executing ChallengeResult with authentication schemes ().
info: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[12]
      AuthenticationScheme: AzureADOpenID was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
      Executed action *****.Controllers.SampleDataController.WeatherForecasts (*******) in 439.0343ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '******.Controllers.SampleDataController.WeatherForecasts (*******)'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 464.9224ms 302 

But i get always the cors error in the browser when returning a HTTP 200 response:

Access to fetch at 'https://login.microsoftonline.com/common/oauth2/authorize?client_id=**********************&redirect_uri=https%3A%2F%2Flocalhost%3A44363%2Fsignin-oidc&response_type=id_token&scope=openid%20profile&response_mode=form_post&nonce=**************************************************&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0' (redirected from 'https://localhost:44363/api/SampleData/WeatherForecasts') from origin 'https://localhost:44363' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

I can open the login page manually but the browser fails in redirection because of this. I even published the application as Azure Web App and still get the same CORS error. I think I've set up everything correctly in Startup.cs but nothing seems to work. Then I even set up the cors-policy from Azure Web App to allow * origins and then allow the relevant origins but the problem persists.

Startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

      services.AddSpaStaticFiles(configuration => {
        configuration.RootPath = "ClientApp/build";
      });

      services.Configure<CookiePolicyOptions>(options => {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
      });

      services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
          .AddAzureAD(options => Configuration.Bind("AzureAd", options)).AddCookie();

      services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => {
        options.TokenValidationParameters = new TokenValidationParameters {
          ValidateIssuer = false,
        };

        options.Events = new OpenIdConnectEvents {
          OnTicketReceived = context => {
            return Task.CompletedTask;
          },
          OnAuthenticationFailed = context => {
            context.Response.Redirect("/Error");
            context.HandleResponse(); // Suppress the exception
            return Task.CompletedTask;
          }
        };
      });
      /*
      Tried also with this
      services.AddCors(setup => {
        setup.AddPolicy("corspolicy", policy => {
          policy
          .AllowAnyOrigin()
          .AllowAnyHeader()
          .AllowAnyMethod();          
        });
      });
      */
    }

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
      if (env.IsDevelopment()) {
        app.UseDeveloperExceptionPage();
      } else {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      app.UseAuthentication(); // Tried putting this and .UseCors to different places
      app.UseStaticFiles();
      app.UseSpaStaticFiles();
      //app.UseCors("corspolicy"); 
      app.UseCors(policy => {
        policy
        .AllowAnyOrigin() // Tried with hardcoded origins
        .AllowAnyMethod()
        .AllowCredentials() // Tried without this also
        .AllowAnyHeader();        
      });
      app.UseHttpsRedirection();

      app.UseMvc(routes => {
        routes.MapRoute(
            name: "default",
            template: "{controller}/{action=Index}/{id?}");
      });
      app.UseSpa(spa => {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment()) {
          spa.UseReactDevelopmentServer(npmScript: "start");
        }
      });
    }

The controller:

  [Authorize]
  [EnableCors] // Tried with named policy and without EnableCors
  [ApiController] // Tried without this
  [Route("api/[controller]")]
  public class SampleDataController : Controller
  {
    private static string[] Summaries = new[]
    {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

    [HttpGet("[action]")]
    public IEnumerable<WeatherForecast> WeatherForecasts()
    {
      var rng = new Random();
      return Enumerable.Range(1, 5).Select(index => new WeatherForecast {
        DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
      });
    }
   // Nonrelevant things omitted
  }
  • Have you tried `.UserCors()` before `.UseAuthentication()` ? – Hadi Samadzad Dec 09 '19 at 10:30
  • You have configured OpenID Connect authentication, which is meant for interactive web clients. You need to configure JWT Bearer authentication since it is an API, and then handle the authentication of the user in the front-end using MSAL.js. – juunas Dec 09 '19 at 10:40
  • @juunas But how come the automatic authentication with redirect to login page and redirecting back again work if i make a plain .NET Core 2.2 MVC without React in it? – Ivan Nifantyef Dec 09 '19 at 11:02
  • @Hadi Yes, like i said in the code comments. – Ivan Nifantyef Dec 09 '19 at 11:03
  • That's because the browser receives the redirect as a response to its top-level request. You are receiving the response to an AJAX request, which causes _it_ to be redirected, causing the CORS issue. – juunas Dec 09 '19 at 11:07
  • @juunas Would it be possible to get at least some hints on how to do it? Am i putting the configuration to the void Configure(IApplicationBuilder app, IHostingEnvironment env) or void ConfigureServices(IServiceCollection services)? – Ivan Nifantyef Dec 09 '19 at 13:49
  • @IvanNifantyef did you manage to make it works? – Agustin Luques Jul 27 '20 at 20:33

1 Answers1

1

After long investigation the short answer to your question is "you can't achieve it with ajax request". You actually need your browser to go to that controller where you are making you "challenge" request. This post here is explaining everything: https://www.blinkingcaret.com/2018/10/10/sign-in-with-an-external-login-provider-in-an-angular-application-served-by-asp-net-core/

Peter M
  • 21
  • 4
  • 1
    While this might answer the question, if possible you should [edit] your answer to include the most important information in the link you've provided in the answer itself. This will help to prevent your answer from becoming invalid if the link stops working or the content significantly changes. – Hoppeduppeanut Oct 13 '20 at 23:12
  • Thank you for this. You saved me hours and hours of horror. – benjamingranados Dec 03 '20 at 18:20
  • [Microsoft docs](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-understand-cors-issues#option-5-extend-the-lifetime-of-the-access-token) (option 5 section) also state that this issue can't be resolved if it's specific to login.microsoftonline.com redirection. – Lukas Apr 08 '21 at 13:10