IdentityServer 4 2.0 userInfo "No 'Access-Control-Allow-Origin' header is present on the requested resource."

Viewed 7888

The scenario.

Everything works when running local on development machine. But when CI deploys to development server we get the following problem.

The site correctly redirects to login and upon login success I am redirected back to the client callback page which updates state and then attempts to load the user via userManager.getUser() which causes the following:

The error message:

Failed to load https://dev-auth.mysite.com/connect/userinfo: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://dev-spa.mysite.com' is therefore not allowed access. The response had HTTP status code 404.

The Logs

From the Seq Serilog we see the following other calls succeed.

Request starting HTTP/1.1 GET http://dev-auth.mysite.com/.well-known/openid-configuration/jwks Request starting HTTP/1.1 GET http://dev-auth.mysite.com/.well-known/openid-configuration Request starting HTTP/1.1 GET http://dev-auth.mysite.com/connect/authorize/callback?client_id=... Request starting HTTP/1.1 POST http://dev-auth.mysite.com/Account/Login application/x-www-form-urlencoded 643 Request starting HTTP/1.1 GET http://dev-auth.mysite.com/account/login?returnUrl=... Request starting HTTP/1.1 GET http://dev-auth.mysite.com/connect/authorize?client_id=... Request starting HTTP/1.1 GET http://dev-auth.mysite.com/.well-known/openid-configuration

The Code

The projects are:

  • api.mysite.com
  • auth.mysite.com
  • spa.mysite.com

All projects are .net core 2.0. The spa project serves a react application that uses the oidc-client package.

auth.mysite.com Startup.cs

The CofigureServices for Identity server portion of Startup is:

public void ConfigureServices(IServiceCollection aServiceCollection)
{
  // CorsPolicy.Any.Apply(aServiceCollection);
  aServiceCollection.AddMvc();
  //aServiceCollection.AddMvcCore()
  //  .AddJsonFormatters()
  //  .AddRazorViewEngine();

  aServiceCollection.AddSession();

  ConfigureServicesIdentityServer(aServiceCollection);

  aServiceCollection.Configure<RazorViewEngineOptions>(options =>
  {
    options.ViewLocationExpanders.Add(new ViewLocationExpander());
  });

  aServiceCollection.AddMediatR();
}

private static void ConfigureServicesIdentityServer(IServiceCollection aServiceCollection)
{
  aServiceCollection.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddInMemoryApiResources(Resources.GetApiResources())
    .AddInMemoryClients(Clients.GetClients())
    .AddInMemoryIdentityResources(Resources.GetIdentityResources())
    // .AddJwtBearerClientAuthentication();
}

The Configure

public void Configure(
  IApplicationBuilder aApplicationBuilder,
  IHostingEnvironment aHostingEnvironment,
  IOptions<RequestLocalizationOptions> aRequestLocalizationOptions)
{
  Environment.UseExceptionPage(aHostingEnvironment, aApplicationBuilder);
  // aApplicationBuilder.UseCors(CorsPolicy.Any.DisplayName);
  aApplicationBuilder.UseIdentityServer();
  aApplicationBuilder.UseRequestLocalization(aRequestLocalizationOptions.Value);
  aApplicationBuilder.UseStaticFiles(); // Serve up static files from wwwroot images etc...
  aApplicationBuilder.UseSession();
  aApplicationBuilder.UseMvc();
}

The Clients configuration:

 new Client
        {
          ClientId = "someGUID",
          ClientName = "MySite",
          RequireClientSecret=false, // if false this is a public client.
          AllowedGrantTypes = GrantTypes.Implicit,
          AllowAccessTokensViaBrowser = true,
          AlwaysIncludeUserClaimsInIdToken = true,

          RedirectUris = {
            "http://local-spa.mysite.com:3000/callback",
            "https://dev-spa.mysite.com/callback",
          },
          PostLogoutRedirectUris = {
            "http://local-spa.mysite.com:3000/",
            "https://dev-spa.mysite.com/",
          },
          AllowedCorsOrigins = {
            "http://local-spa.mysite.com:3000",
            "https://dev-spa.mysite.com",
          },

          AllowedScopes =
          {
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            IdentityServerConstants.StandardScopes.Email,
            IdentityServerConstants.StandardScopes.Phone,
            Resources.WebOrderingApi,
          },

          RequireConsent = false,
        },

The TypeScript client:

import { Log, OidcClientSettings, UserManager, UserManagerSettings, } from 'oidc-client';

...

const protocol: string = window.location.protocol;
const hostname: string = window.location.hostname;
const port: string = window.location.port;

const oidcClientSettings: OidcClientSettings = {
  client_id: '46a0ab4a-1321-4d77-abe5-98f09310df0b',
  post_logout_redirect_uri: `${protocol}//${hostname}${port ? `:${port}` : ''}/`,
  redirect_uri: `${protocol}//${hostname}${port ? `:${port}` : ''}/callback`,
  response_type: 'id_token token',
  scope: 'openid profile email phone WebOrderingApi',
};

const userManagerSettings: UserManagerSettings = {
  ...oidcClientSettings,
  automaticSilentRenew: false,
  filterProtocolClaims: true,
  loadUserInfo: true,
  monitorSession: false,
  silent_redirect_uri: `${protocol}//${hostname}${port ? `:${port}` : ''}/callback`,
};

Update

So it turns out that the development IIS Server did not allow the OPTIONS verb. This is a recommended thing to do in production servers that don't need CORS but I would not recommended that for development environment.

Thanks to all for their contribution.

IIS Request Filtering Settings

4 Answers

One fix for this issue is to allow the OPTIONS verb in IIS for the auth server.

Why this is not working

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://dev-spa.mysite.com' is therefore not allowed access. The response had HTTP status code 404.

Before a request is made cross origins a CORS request is made, to check if the request should be allowed, this is sent with the OPTIONS method, which your server responded to with 404, stopping the request

How to fix

You need to send back a valid OPTIONS response, allowing the request.

I would recommend using Wireshark or some other inspector to see the requests that are sent and their responses, to help with debugging why the CORS response is stopping the request.

As @Kirk mentioned above, the issue may be down to HTTPS, either with the CORS request, or the whole server not supporting HTTPS.

Resources

From your error message, I note the following right at the end:

The response had HTTP status code 404.

This suggests that your problem might not actually be a CORS issue - it might be more that the endpoint you are trying to reach is not found, resulting in a 404 response. Of course, this 404 response is not likely to include the relevant Access-Control-Allow-Origin header.

Looking at your logs, all the successful calls to dev-auth.mysite.com use the http scheme. However, when refering to the connect/userinfo endpoint, your error message refers to the https scheme:

Failed to load https://dev-auth.mysite.com/connect/userinfo

Per the documentation you can mix in .NET Core CORS, and IdentityServer will honor it. In your Startup.cs, add:

public void ConfigureServices(IServiceCollection services)
{
...
    services.AddCors(options =>
    {
        options.AddPolicy("WhateverNameYouWantHere",
            builder =>
            {
                builder.WithOrigins($"http://{_frontendUri}",
                                $"https://{_frontendUri}")
                    .AllowCredentials()
                    .AllowAnyMethod()
                    .AllowAnyHeader();
            });
    });
...
}

Then later

public void Configure(IApplicationBuilder app)
{
    ...
    app.UseCors("WhateverNameYouWantHere");
}
Related