0

Repository that can be run locally if the README is followed.

I have an ASP.NET Core 3.1 IdentityServer4 server with a local api that is being called from an Angular server using the oidc js library with the template code.

The login flow works fine and the token is issued, but when calling the api from the Angular client nothing seems to work, the CORS policy always blocks it. I followed the docs here and checked it against the sample quickstart repository here. Even with the CORS service explicitly allowing any origin. The following code snippets have some side code removed, so only the pertinent code remains. The repo has the complete up to date and runnable code. I am currently going to try updating the core server to use the latest IdentityServer package with the latest ASP.NET setup.

When the HttpClient makes the call from the Angular client, this is what IdentityServer shows:

CORS request made for path /api/v1.0/users/{guid}/organizations from origin: https://localhost:44459 but was ignored because path was not for an allowed IdentityServer CORS endpoint.

enter image description here

Config.cs Using an ApiResource here doesn't work as shown in the docs, but using an ApiScope like in the quickstart works fine. The scope exists in the end access token.

public static IEnumerable<IdentityResource> Ids =>
    new IdentityResource[]
    {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
    };

public static IEnumerable<ApiScope> ApiScopes =>
    new ApiScope[]
    {
                new ApiScope(IdentityServerConstants.LocalApi.ScopeName)

    };

public static IEnumerable<Client> Clients =>
    new Client[]
    {
                new Client()
                {
                    ClientName = "Test Angular Client",
                    ClientId = "testclient",
                    AllowedGrantTypes = GrantTypes.Code,
                    AllowedCorsOrigins = { "https://localhost:44459" },
                    RequireConsent = false,
                    AllowAccessTokensViaBrowser = true,
                    RedirectUris = { "https://localhost:44459/authentication/login-callback" },
                    PostLogoutRedirectUris = { "https://localhost:44459/authentication/logout-callback" },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.LocalApi.ScopeName,
                        "testapi",
                    },
                    RequirePkce = true,
                    RequireClientSecret = false
                },
    };

In Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddApiVersioning();

    var builder = services.AddIdentityServer()
        .AddInMemoryIdentityResources(Config.Ids)
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryClients(Config.Clients)
        .AddAspNetIdentity<User>();

    builder.AddDeveloperSigningCredential();

    services.AddRazorPages()
        .AddRazorPagesOptions(o =>
        {
            o.Conventions.AddPageRoute("/home/index", "");
        })
        .AddRazorRuntimeCompilation();

    services.AddLocalApiAuthentication();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/api/error");
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthorization();

    app.UseEndpoints(e =>
    {
        e.MapRazorPages();
        e.MapControllers();
    });
}

and the controller trying to be called here, which can be called directly from the browser once authenticated (not through the client).

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Authorize(LocalApi.PolicyName)]
public class UsersController : ControllerBase
{
    [HttpGet("{userId}/organizations")]
    public async Task<IActionResult> GetOrganizations(Guid userId)
    {
         // do things
    }
}

The Angular client uses the code provided by the Microsoft template. But to summarize the main authentication code is below in snippets. I don't think this is the issue as it's unchanged from the examples besides the identity server uris and information. It's all just using the oidc js provided classes.

UserManagerSettings

const settings: UserManagerSettings = {
  authority: "https://localhost:5100",
  client_id: "testclient",
  redirect_uri: "https://localhost:44459/authentication/login-callback",
  post_logout_redirect_uri: "https://localhost:44459/authentication/logout-callback",
  scope: "openid profile IdentityServerApi",
  response_type: "code",
  filterProtocolClaims: true,
  loadUserInfo: true,
  automaticSilentRenew: true,
  includeIdTokenInSilentRenew: true,
};

authorize.interceptor.ts as provided by Microsoft.

export class AuthorizeInterceptor implements HttpInterceptor {
  constructor(private authorize: AuthorizeService) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authorize.getAccessToken()
      .pipe(mergeMap(token => this.processRequestWithToken(token, req, next)));
  }

   private processRequestWithToken(token: string | null, req: HttpRequest<any>, next: 
   HttpHandler) {
    if (!!token && this.isSameOriginUrl(req)) {
      req = req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(req);
  }
  // same origin checking code as provided...
}

and then finally the actual component code where the client is instantiated and the request made. The interceptor should be adding the access token to the header when needed. portal.component.ts

export class PortalComponent implements OnInit{
  idpApiUrl: string;
  idpBaseUrl: string;
  userOrganizations: UserOrganization[] = [];

  constructor(private http: HttpClient, 
    private authorizeSvc: AuthorizeService,
    private router: Router,
    @Inject('IDP_API_URL') idpApiUrl: string) {
    this.idpApiUrl = idpApiUrl;
  }

  ngOnInit() {
    this.authorizeSvc.getUser()
      .pipe(map(u => u && u.sub))
      .subscribe(userId => 
        // this is the api call that gets blocked by the IDP
        this.http.get<UserOrganization[]>(this.idpApiUrl + 
        `/users/${userId}/organizations`).subscribe({
          next: (res) => this.userOrganizations = res,
          error: (e) => console.log(e)})
      )
  } 
}

Note again that while the server blocks these calls from the client, I can manually just type in the api request url in my browser once authenticated and the call goes through fine.

I attempted to just have a CORS policy allowing any origin, and using the IdentityServer4 docs, added the code below. It did nothing for me though, no change at all.

in Startup.cs ConfigureServices method

services.AddSingleton<ICorsPolicyService>((container) =>
{
    var logger = container.GetRequiredService<ILogger<DefaultCorsPolicyService>>();
    return new DefaultCorsPolicyService(logger)
    {
        AllowAll = true
    };
});

I also tried to look at what actually was the source code providing this issue, and if it helps at all it is here. It seems like the policy is triggering at the wrong time and is checking among the few IdentityServer4 endpoints which should be protected. As far as I can tell the api should not even be handled by this, and should be handled by the authorize attribute. For reference here are the registered paths it's validating against. enter image description here

EDIT: So, two things here fixed the problem. Firstly, adding in a default CORS policy in ASP.NET Core's default middleware is necessary like the accepted answer. Here is what I added to Startup.cs

services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
        {
            policy.WithOrigins("https://localhost:44459")
            .AllowAnyHeader()
            .AllowAnyMethod();
        });
});

and

app.UseCors();

Secondly, the default angular code from Microsoft doesn't work for my specific use case. In the template the idp, api, and client share the same origin. In my case it's separate so I changed the code to include the access token if the token exists and the request is for the idp. In the future I'll change it to include the api when that time comes.

authorize.interceptor.ts

// snipped code, unchanged from above
private processRequestWithToken(token: string | null, req: HttpRequest<any>, next: HttpHandler) {
    if (!!token && req.url.startsWith("https://localhost:5100")) {
      req = req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(req);
  }
}

The problem was the default template code was not including the access token with the request to the api on the idp. The IdentityServer4 middleware was then checking if the request path belonged to one of the authentication or information endpoints before blocking it.

3
  • Please paste in the error message shown in i.sstatic.net/gY8vx.png as text rather than as an image. Copy and paste the text from the console into the question. Otherwise, the error message isn’t available to the SO search engine nor to Google Search or other search engines. Which means that others seeing the same error later would not find this question. Commented Sep 10, 2022 at 7:28
  • You are using a very outdated version of identityserver, you should use Duende.IdentityServer4 instead of IdentityServer4 Commented Sep 10, 2022 at 11:58
  • 1
    @AviadP. Okay, I was considering doing this. I'll take the time and update everything. Commented Sep 11, 2022 at 3:00

1 Answer 1

1

In general I recommend that you always try to put IdentityServer, the client and the API in separate services, because it is really hard to reason and troubleshoot when you mix them together.

In your case you need to realise that CORS in IdentityServer and ASP.NET Core is configured separately and the CORS settings in IdentityServer is only for IdentityServer and not for ASP.NET core in general.

You need to also configure CORS for ASP.NET Core then see this tutorial:

Hope this helps

Sign up to request clarification or add additional context in comments.

3 Comments

My main API is separate but I want to keep tenancy and user manipulation on the identity server since the database has all the user information. So I just want to expose some extra user/tenant management settings via api endpoints. IdentityServer4 has built in functionality for this. And yes I understand they are separate, the issue is Identityserver is blocking an endpoint which shouldn't be managed by it.
As I said ,you need to enable CORS for the API endpoint as well, I didn't see you use app.UseCors(...); in the code above?
Ah, I don't know why I didn't include that. I initially tried it but it didn't seem to change anything. After I updated, included a default CORS policy, and removed a piece of the typescript code it works now. I'd like to accept your answer but I also need to include a change to the typescript code from the default Microsoft template

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.