DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Automatic token management not working #858

Closed alexanderopdeweegh closed 1 year ago

alexanderopdeweegh commented 1 year ago

Hi,

I'm quite new to using Duende IdentityServer. I have been able to setup an IDP Server based on IdentityServer 6. I also created an API project as well as a webclient. All projects based on .Net 6.

As described on https://docs.duendesoftware.com/identityserver/v6/bff/tokens/ I've added a call to AddUserAccessTokenHttpClient() for automatic token management.

I'm not sure if it is necessary, but I've set the RefreshTokenUsage field to 0 (ReUse).

For testing purposes, I've set the lifetime of my access token to 5 minutes, the absolute refresh lifetime to 15 minutes and the sliding refresh token lifetime to 10 minutes.

My test scenario is as follows:

  1. I start all three projects and go to the home page of my web client.
  2. The web client redirects to IDP to have me logged in.
  3. After logging in, I call a page that in turn calls one of the APIs. It uses the httpClientFactory.CreateClient() created HttpClient for it.
  4. The API checks for an authenticated user and then gives me the expected data.
  5. I wait for about 6 minutes and refresh the page of the web client.
  6. An exception is thrown indicating a 401 from the API.

It seems to me that the access token is expired (which makes sense), but is not renewed by the HttpClient. I expected the HttpClient to automatically renew the access token.

The API call is called from an asynchronous task. Catching the exception therein gives me the following stack trace:

One or more errors occurred. (Response status code does not indicate success: 401 (Unauthorized).)
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task`1.get_Result() at ....BronService.GetAll() in C:\...\BronService.cs:line 16 at ....BeheerBronnen.<OnInitializedAsync>b__4_0() in C:\...\BeheerBronnen.razor:line 46

If I remove the catch parts from my code, the fronted gives me the following result:

[2023-08-28T13:07:22.022Z] Error: System.AggregateException: One or more errors occurred. (Response status code does not indicate success: 401 (Unauthorized).)
 ---> System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
   at System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsyncCore[T](Task`1 taskResponse, JsonSerializerOptions options, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at ...BronService.GetAll() in C:\...\BronService.cs:line 16
   at....BeheerBronnen.<OnInitializedAsync>b__4_0() in C:\...\BeheerBronnen.razor:line 44
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at ....BeheerBronnen.OnInitializedAsync() in C:\...\BeheerBronnen.razor:line 42
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
log @ blazor.server.js:1
tr @ blazor.server.js:1
(anonymous) @ blazor.server.js:1
(anonymous) @ blazor.server.js:1
_invokeClientMethod @ blazor.server.js:1
_processIncomingData @ blazor.server.js:1
connection.onreceive @ blazor.server.js:1
o.onmessage @ blazor.server.js:1
blazor.server.js:1

Any ideas why the automatic refresh of the access token is not working?

Kind regards, Alexander op de Wegh

=== UPDATE ===

Although I didn't change my code since I've written the above, I now get another stack trace:

blazor.server.js:1 [2023-08-28T13:36:41.764Z] Error: System.AggregateException: One or more errors occurred. (Headers are read-only, response has already started.)
 ---> System.InvalidOperationException: Headers are read-only, response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_SetCookie(StringValues value)
   at Microsoft.AspNetCore.Http.ResponseCookies.Append(ReadOnlySpan`1 keyValuePairs, CookieOptions options)
   at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie(HttpContext context, String key, String value, CookieOptions options)
   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
   at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
   at IdentityModel.AspNetCore.AccessTokenManagement.AuthenticationSessionUserAccessTokenStore.StoreTokenAsync(ClaimsPrincipal user, String accessToken, DateTimeOffset expiration, String refreshToken, UserAccessTokenParameters parameters) in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\UserAccessToken\AuthenticationSessionUserTokenStore.cs:line 189
   at IdentityModel.AspNetCore.AccessTokenManagement.UserAccessAccessTokenManagementService.RefreshUserAccessTokenAsync(ClaimsPrincipal user, UserAccessTokenParameters parameters, CancellationToken cancellationToken) in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\UserAccessToken\UserAccessTokenManagementService.cs:line 145
   at IdentityModel.AspNetCore.AccessTokenManagement.UserAccessAccessTokenManagementService.<>c__DisplayClass7_0.<<GetUserAccessTokenAsync>b__1>d.MoveNext() in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\UserAccessToken\UserAccessTokenManagementService.cs:line 99
--- End of stack trace from previous location ---
   at IdentityModel.AspNetCore.AccessTokenManagement.UserAccessAccessTokenManagementService.GetUserAccessTokenAsync(ClaimsPrincipal user, UserAccessTokenParameters parameters, CancellationToken cancellationToken) in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\UserAccessToken\UserAccessTokenManagementService.cs:line 95
   at Microsoft.AspNetCore.Authentication.TokenManagementHttpContextExtensions.GetUserAccessTokenAsync(HttpContext httpContext, UserAccessTokenParameters parameters, CancellationToken cancellationToken) in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\TokenManagementHttpContextExtensions.cs:line 31
   at IdentityModel.AspNetCore.AccessTokenManagement.UserAccessTokenHandler.SetTokenAsync(HttpRequestMessage request, Boolean forceRenewal) in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\UserAccessToken\UserAccessTokenHandler.cs:line 67
   at IdentityModel.AspNetCore.AccessTokenManagement.UserAccessTokenHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in D:\a\IdentityModel.AspNetCore\IdentityModel.AspNetCore\src\AccessTokenManagement\UserAccessToken\UserAccessTokenHandler.cs:line 35
   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)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at ....BronService.GetAll() in C:\...\BronService.cs:line 16
   at ....BeheerBronnen.<OnInitializedAsync>b__4_0() in C:\...\BeheerBronnen.razor:line 44
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at ....BeheerBronnen.OnInitializedAsync() in C:\...\BeheerBronnen.razor:line 42
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

After refreshing the page, the correct content is showing up. It seems therefore that the access token is refreshed. Although I can't explain this exception, nor do I understand how to nicely hide this exception from the user.

josephdecock commented 1 year ago

I'm a little confused by your BFF label on this question, since you're using blazor server. I'd expect to see blazor wasm used with the BFF instead.

Regardless, there are docs here on how to use Duende.AccessTokenManagment with blazor server. More details are there, but briefly, the reason that you're getting inconsistent results and failing to refresh tokens is that blazor server doesn't allow you to reliably use the HttpContext or HttpContext.User, and you need to add additional code to store your tokens elsewhere.

alexanderopdeweegh commented 1 year ago

Hi Joseph,

Thanks for your reply. As you noticed, I'm a newbie ;-).

I'm trying to modify my application based on the reference you gave me. This page indicates that "BlazorServer" sample code available. Unfortunately, I can't find it. I can find BlazorWasm, but as I understand, that's a client side application. So, I figure that the implementation differs.

Do you know where I can find this BlazorServer sample?

josephdecock commented 1 year ago

Sure, the BlazorServer sample is within the Duende.AccessTokenManagement repo here.

alexanderopdeweegh commented 1 year ago

Wow, you're quick. Thanks, I'll look into it.

alexanderopdeweegh commented 1 year ago

It's working! Thanks.