DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

[demo.duendesoftware.com] Cors error testing from Blazor WebApp Client #1048

Closed mohaaron closed 8 months ago

mohaaron commented 9 months ago

Which version of Duende IdentityServer are you using? Demo Server

Which version of .NET are you using? 8.0.100

Describe the bug Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘https://demo.duendesoftware.com/api/test’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).

A clear and concise description of what the bug is.

To Reproduce

I created this sample from a Blazor Server sample provided by Duende. I have a sample Github repo here: https://github.com/mohaaron/CustomAuthorizationHandler

Steps to reproduce the behavior.

Pull Github repo Run WebApp project Login to the demo identity server Navigate to the Fetch Remote Data menu item

A clear and concise description of what you expected to happen.

Log output/exception with stacktrace

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: TypeError: NetworkError when attempting to fetch resource.
System.Net.Http.HttpRequestException: TypeError: NetworkError when attempting to fetch resource.
 ---> TypeError: NetworkError when attempting to fetch resource.
   --- End of inner exception stack trace ---
   at System.Net.Http.BrowserHttpInterop.<CancelationHelper>d__13`1[[System.Runtime.InteropServices.JavaScript.JSObject, System.Runtime.InteropServices.JavaScript, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].MoveNext()
   at System.Net.Http.BrowserHttpHandler.CallFetch(HttpRequestMessage request, CancellationToken cancellationToken, Nullable`1 allowAutoRedirect)
   at System.Net.Http.BrowserHttpHandler.<SendAsync>g__Impl|53_0(HttpRequestMessage request, CancellationToken cancellationToken, Nullable`1 allowAutoRedirect)
   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.<SendCoreAsync>g__Core|5_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken)
   at BlazorWebApp.Client.CookieHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in C:\Users\Aaron Prohaska\Projects\Learning\BlazorNet8\BlazorWebApp.CustomerAuthorization\BlazorWebApp.Client\CookieHandler.cs:line 13
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.<SendCoreAsync>g__Core|5_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at BlazorWebApp.Client.Services.RemoteApiService.GetData() in C:\Users\Aaron Prohaska\Projects\Learning\BlazorNet8\BlazorWebApp.CustomerAuthorization\BlazorWebApp.Client\Services\RemoteApiService.cs:line 25
   at BlazorWebApp.Client.Pages.FetchRemoteData.OnInitializedAsync() in C:\Users\Aaron Prohaska\Projects\Learning\BlazorNet8\BlazorWebApp.CustomerAuthorization\BlazorWebApp.Client\Pages\FetchRemoteData.razor:line 30
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

Additional context

Add any other context about the problem here.

kkdeveloper7 commented 9 months ago

I think you are passing ‘Access-Control-Allow-Origin’ header with value "*". If you allow credentials on your API I dont think you can pass ‘Access-Control-Allow-Origin’ with value "*". You are saying that you allow anyone to come in but also allow credentials. Either remove allow credentials from server or remove ‘Access-Control-Allow-Origin’ value on the request.

mohaaron commented 9 months ago

I think you are passing ‘Access-Control-Allow-Origin’ header with value "*". If you allow credentials on your API I dont think you can pass ‘Access-Control-Allow-Origin’ with value "*". You are saying that you allow anyone to come in but also allow credentials. Either remove allow credentials from server or remove ‘Access-Control-Allow-Origin’ value on the request.

I don't understand your answer. The I don't have control over this API as it's a Duende test site. I am successfully logging in to the test site and then trying to call the API test address using the credentials that were created during the login.

I just tried the following but this seems to not pass a token in the request and I get a 401 response. I've updated the repo to use this CookieHandler instead of the AuthorizationMessageHandler. My goal here is to understand how to pass the token in HttpClient requests to the remote API server which in this case is Duende's demo server.

public class CookieHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        request.SetBrowserRequestMode(BrowserRequestMode.NoCors);
        return await base.SendAsync(request, cancellationToken);
    }
}
kkdeveloper7 commented 9 months ago

I think you are passing ‘Access-Control-Allow-Origin’ header with value "*". If you allow credentials on your API I dont think you can pass ‘Access-Control-Allow-Origin’ with value "*". You are saying that you allow anyone to come in but also allow credentials. Either remove allow credentials from server or remove ‘Access-Control-Allow-Origin’ value on the request.

I don't understand your answer. The I don't have control over this API as it's a Duende test site. I am successfully logging in to the test site and then trying to call the API test address using the credentials that were created during the login.

I just tried the following but this seems to not pass a token in the request and I get a 401 response. I've updated the repo to use this CookieHandler instead of the AuthorizationMessageHandler. My goal here is to understand how to pass the token in HttpClient requests to the remote API server which in this case is Duende's demo server.

public class CookieHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        request.SetBrowserRequestMode(BrowserRequestMode.NoCors);
        return await base.SendAsync(request, cancellationToken);
    }
}

Are you trying to pass token in the cookie or in authorization header? I am trying to understand if you are trying to authenticate via cookie or Bearer Token? I am assuming you are trying to call REST API via HttpClient and trying to pass same credentials you via cookie or Authorization Header.

HttpClient will not automatically append access cookie or token, you have to manually include cookie into cookie container or include your Bearer token in Authorization header.

josephdecock commented 9 months ago

@mohaaron did you try using our sample here: https://github.com/DuendeSoftware/Duende.AccessTokenManagement/tree/main/samples/BlazorServer?

Does that sample work for you unchanged?

Can you highlight what is different about your sample please?

mohaaron commented 9 months ago

@josephdecock Thank you for the response.

I originally pulled down that example sample and it's what I started with. I added a new Blazor WebApp project to the solution that uses Interactive Auto mode. I copied what I needed to from the Blazor server project into the SSR side of the web app. As you can see in the repo sample that I provided the SSR side works correctly and routes the request to the demo site for login. Once logged in the demo site redirects back to the app. This is all good and being done on the server side of things. the entire app is being authenticated correctly.

Now the difference is in the fact that the auto render mode runs as WASM after the initial load. When this happens the authentication token needs to be passed up to the demo API endpoint which is where things go wrong. I'm getting a CORS error back from the demo API endpoint that is used in the Blazor Server sample. I'm guessing this is happening due to the HttpClient being used to make the call.

Have you looked at the sample I provided?

Maybe the issue is that I'm trying to use the CookieHandler when I should be using the CustomAuthorizationMessageHandler. Only problem is that I keep getting a dependency injection error saying that IAccessTokenProvider is not in DI. I have an open issue in the aspnet Github repository about this which has no answer yet. https://github.com/dotnet/aspnetcore/issues/53066

Any help would be greatly appreciated. I think I'm not the only one trying to figure out how to do this with the new WebApp template.

josephdecock commented 9 months ago

Thanks for clarifying. The new automatic modes in blazor 8 are a really big change, and supporting it well is a problem that we are actively working on.

Blazor server, blazor wasm, and server side rendering (SSR) have fundamentally different security models.

In blazor server, you have a server side component streaming updates over a web socket. There are no http requests sent during that streaming, so you have no cookies to work with. This means that you need a server side store of tokens, and you need the server to manage tokens, store them in the server side token store, and make api calls.

In blazor wasm, you have the UI running in the browser. Just as with a more "traditional" (non-wasm) SPA, our advice is to use the BFF pattern with blazor wasm. This means that the UI will make HTTP requests secured with a cookie to the BFF, and then the BFF will manage tokens for the session and proxy API calls for the UI.

Finally in blazor ssr, there is an http request authenticated with a cookie and then server side code runs to create an http response.

The interactive render modes cause multiple of these code paths to be needed. InteractiveServer and InteractiveWasm both do an initial SSR before switching to server and wasm mode respectively, while interactive auto does SSR and then switches between server and wasm as the wasm resources load.

To sum up, sometimes the code runs in the browser and you need a BFF, and sometimes the code runs in the server and you don't need a BFF.

One way to achieve this is to have an interface that describes the data access you need, and then implement it two ways (one calling the bff and one not). Then you register the bff based implementation in the client project and the direct access in the server project. The downside of this is that it is rather complex to implement, and we'd really like to improve the dev experience when working with this kind of architecture. This is an area of active development and experimentation for us, so stay tuned!

josephdecock commented 8 months ago

In the interest of keeping things organized, I'm closing this along with some other related issues, but feel free to provide feedback and comments in (https://github.com/DuendeSoftware/Samples/issues/142)