dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Something wrong with AuthorizationMessageHandler in .net 6 #38486

Closed ghost closed 10 months ago

ghost commented 2 years ago

Hello, I currently follow this doc about attaching token to outgoing request for my blazor wasm (.net 6) based on this doc ASP.NET Core Blazor WebAssembly additional security scenarios but I got 2 issues:

builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("WebAPI", client =>
{
    client.BaseAddress = new Uri("http://localhost:5075/api/");
})
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("WebAPI"));

CustomAuthorizationMessageHandler.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

namespace EcommerceClient.Services
{
    public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
    {
        public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation) : base(provider, navigation)
        {
            ConfigureHandler(
                authorizedUrls: new[] {
                   "http://localhost:5075/api/users",
                   "http://localhost:5075/api/products"
                });
        }
    }
}

It seems fine when first loading but when I go to another url and start use it. It gives me those error below:

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: The added or subtracted value results in an un-representable DateTime. (Parameter 'value')
System.ArgumentOutOfRangeException: The added or subtracted value results in an un-representable DateTime. (Parameter 'value')
   at System.DateTime.ThrowDateArithmetic(Int32 param)
   at System.DateTime.AddTicks(Int64 value)
   at System.DateTime.Add(Double value, Int32 scale)
   at System.DateTime.AddMinutes(Double value)
   at System.DateTimeOffset.AddMinutes(Double minutes)
   at Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthorizationMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, 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 EcommerceClient.Services.AuthService.LoginAsync(LoginModel loginModel) in F:\Nguyen\Project\Hobby-Projects\Ecommerce\dotnet\EcommerceClient\Services\AuthService.cs:line 48
   at EcommerceClient.Pages.Auth.Login.HandleValidSubmitAsync() in F:\Nguyen\Project\Hobby-Projects\Ecommerce\dotnet\EcommerceClient\Pages\Auth\Login.razor:line 25
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.Forms.EditForm.HandleSubmitAsync()
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

which indicates line 48 in my AuthService:

using System.Net.Http.Json;

namespace EcommerceClient.Services;

public class AuthService : IAuthService
{
    private readonly string Prefix = "auth";
    private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true
    };
    private readonly ILogger<AuthService> _logger;
    private readonly HttpClient _client;
    private readonly AuthenticationStateProvider _provider;
    private readonly IStorageService _storageService;

    public AuthService(
        ILogger<AuthService> logger,
        HttpClient client,
        AuthenticationStateProvider provider,
        IStorageService storageService)
    {
        _logger = logger;
        _client = client;
        _provider = provider;
        _storageService = storageService;
    }

    public async Task<LoginResponse> LoginAsync(LoginModel loginModel)
    {
        try
        {
            var responseMessage = await _client.PostAsJsonAsync<LoginModel>($"{Prefix}/login", loginModel);
            var loginResult = await JsonSerializer.DeserializeAsync<LoginResponse>(
                await responseMessage.Content.ReadAsStreamAsync(),
                _jsonSerializerOptions
            );

            if (responseMessage.IsSuccessStatusCode)
            {
                await _storageService.SetItemAsync(Constant.JWT_TOKEN_NAME, loginResult.Token);
            }

            return loginResult!;
        }
        catch (Exception ex)
        {
            throw ex;  // This line only for debug purpose and also they indicates here ???
            return new LoginResponse { Error = ex.Message };
        }
    }

    public async Task LogoutAsync()
    {
        await _storageService.RemoveItemAsync(Constant.JWT_TOKEN_NAME);
        _client.DefaultRequestHeaders.Authorization = null;
    }

    public async Task<LoginResponse> RegisterAsync(SignupModel signupModel)
    {
        try
        {
            var responseMessage = await _client.PostAsJsonAsync<SignupModel>($"{Prefix}/signup", signupModel);
            var loginResult = await JsonSerializer.DeserializeAsync<LoginResponse>(
                await responseMessage.Content.ReadAsStreamAsync(),
                _jsonSerializerOptions
            );

            if (responseMessage.IsSuccessStatusCode)
                await _storageService.SetItemAsync(Constant.JWT_TOKEN_NAME, loginResult.Token);

            return loginResult!;
        }
        catch (Exception ex)
        {
            throw ex; // This line only for debug purpose
            return new LoginResponse { Error = ex.Message };
        }
    }
}

P\S: Before I use those everything still normal. Thank you.

ghost commented 2 years ago

Hi @NbN12. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

mkArtakMSFT commented 2 years ago

Thank you for filing this issue. In order for us to investigate this issue, please provide a minimalistic repro project (ideally a GitHub repo) that illustrates the problem.

mkArtakMSFT commented 2 years ago

For context, here are the two issues reported here:

  1. It seems that our docs don't call out the need to assign InnerHandler and that's the culprit here. So we need to update the docs to show how to do that.
  2. We still need to investigate the second issue where the DateTime construction / parsing failed
ghost commented 2 years ago

Here is repo: https://github.com/NbN12/Hobby-Projects/tree/main/Ecommerce/dotnet

OsirisTerje commented 2 years ago

I can confirm the same issue, using net6.0, and using the code from the Refit library/docs https://github.com/reactiveui/refit#bearer-authentication for non-DI:

var api = RestService.For<ISomeThirdPartyApi>(new HttpClient(new AuthHeaderHandler(tenantProvider, authTokenStore))
    {
        BaseAddress = new Uri("https://api.example.com")
    }
);

and I get the following crash:

Message: 
System.InvalidOperationException : The inner handler has not been assigned.

  Stack Trace: 
DelegatingHandler.SetOperationStarted()
DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
AuthHeaderHandler.<>n__0(HttpRequestMessage request, CancellationToken cancellationToken)
AuthHeaderHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) line 119
HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
<<BuildCancellableTaskFuncForMethod>b__0>d.MoveNext() line 245
--- End of stack trace from previous location ---
VerifiserAuthenticationTest.ThatAuthenticationWithJwkKeypairsWorks() line 21
GenericAdapter`1.GetResult()
AsyncToSyncAdapter.Await(Func`1 invoke)
TestMethodCommand.Execute(TestExecutionContext context)
<>c__DisplayClass4_0.<PerformWork>b__0()
<>c__DisplayClass1_0`1.<DoIsolated>b__0(Object _)
ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
ContextUtils.DoIsolated(ContextCallback callback, Object state)
ContextUtils.DoIsolated[T](Func`1 func)
SimpleWorkItem.PerformWork()
OsirisTerje commented 2 years ago

@mkArtakMSFT You point to the need to assign Innerhandler, but other docs say that should not be needed, and doesn't seem needed in earlier dotnet versions. Anyway, what is the exact assignment one need to do?

If I just do:

InnerHandler = new HttpClientHandler();

Then the call goes through. Is this the right way - or is anything being missed then?

fmashozhera commented 2 years ago

I am also getting the same error as your second issue 'The added or subtracted value results in an un-representable DateTime.' Have you find a solution yet?

ghost commented 2 years ago

Still waiting to fix this

dombott commented 2 years ago

I had the same problem with The added or subtracted value results in an un-representable DateTime.. My IAccessTokenProvider returned an AccessToken with the Expiry set to the date 01.01.0001, which led to the exception above. Setting the Expiry manually to the DateTime stored in the token made it work. Hope this helps anyone.

ghost commented 2 years ago

Thanks for contacting us. We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. Because it's not immediately obvious that this is a bug in our framework, we would like to keep this around to collect more feedback, which can later help us determine the impact of it. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 1 year ago

To learn more about what this message means, what to expect next, and how this issue will be handled you can read our Triage Process document. We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. Because it's not immediately obvious what is causing this behavior, we would like to keep this around to collect more feedback, which can later help us determine how to handle this. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact work.

mkArtakMSFT commented 10 months ago

Thanks for contacting us. We couldn't prioritize this investigation given the number of customers are affected. Hence closing this issue. You can learn more about our triage process and how we handle issues by reading our Triage Process writeup.