Issue
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.
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.

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.
Solution
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
Answered By - Tore Nestenius

0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.