Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access Token Management In SignalR Hubs #551

Closed
erkanmaras opened this issue Mar 3, 2023 · 2 comments
Closed

Access Token Management In SignalR Hubs #551

erkanmaras opened this issue Mar 3, 2023 · 2 comments

Comments

@erkanmaras
Copy link

erkanmaras commented Mar 3, 2023

Which version of Duende BFF are you using?
BFF 2.0

Which version of .NET are you using?
.Net 6

Question

Hi,
We are trying to use BFF framework, and we want to use automatic token management of course, but when we call GetUserAccessTokenAsync() method in a SignalR-Hub, It's throwing an exception. Actually, It works until the access token-expire then tries to refresh access-token. The token is successfully refreshing, but the below exception is throwing when it attempts to update the session cookie using HttpContext.SignInAsync().

So, Is there any workaround for this?

The response headers cannot be modified because the response has already started.

System.InvalidOperationException:
   at Microsoft.AspNetCore.HttpSys.Internal.HeaderCollection.ThrowIfReadOnly (Microsoft.AspNetCore.Server.IIS, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.HttpSys.Internal.HeaderCollection.set_Item (Microsoft.AspNetCore.Server.IIS, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Http.IHeaderDictionary.set_SetCookie (Microsoft.AspNetCore.Http.Features, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Http.ResponseCookies.Append (Microsoft.AspNetCore.Http, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie (Microsoft.AspNetCore.Authentication.Cookies, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler+<HandleSignInAsync>d__26.MoveNext (Microsoft.AspNetCore.Authentication.Cookies, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.AspNetCore.Authentication.AuthenticationService+<SignInAsync>d__17.MoveNext (Microsoft.AspNetCore.Authentication.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Duende.AccessTokenManagement.OpenIdConnect.AuthenticationSessionUserAccessTokenStore+<StoreTokenAsync>d__8.MoveNext (Duende.AccessTokenManagement.OpenIdConnect, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /_/src/Duende.AccessTokenManagement.OpenIdConnect/AuthenticationSessionUserTokenStore.cs:209)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Duende.AccessTokenManagement.OpenIdConnect.UserAccessAccessTokenManagementService+<RefreshUserAccessTokenAsync>d__9.MoveNext (Duende.AccessTokenManagement.OpenIdConnect, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /_/src/Duende.AccessTokenManagement.OpenIdConnect/UserAccessTokenManagementService.cs:155)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Duende.AccessTokenManagement.OpenIdConnect.UserAccessAccessTokenManagementService+<>c__DisplayClass7_0+<<GetAccessTokenAsync>b__0>d.MoveNext (Duende.AccessTokenManagement.OpenIdConnect, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /_/src/Duende.AccessTokenManagement.OpenIdConnect/UserAccessTokenManagementService.cs:108)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Duende.AccessTokenManagement.OpenIdConnect.UserTokenRequestSynchronization+<SynchronizeAsync>d__3.MoveNext (Duende.AccessTokenManagement.OpenIdConnect, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /_/src/Duende.AccessTokenManagement.OpenIdConnect/UserTokenRequestSynchronization.cs:32)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Duende.AccessTokenManagement.OpenIdConnect.UserAccessAccessTokenManagementService+<GetAccessTokenAsync>d__7.MoveNext (Duende.AccessTokenManagement.OpenIdConnect, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /_/src/Duende.AccessTokenManagement.OpenIdConnect/UserAccessTokenManagementService.cs:113)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.AspNetCore.Authentication.TokenManagementHttpContextExtensions+<GetUserAccessTokenAsync>d__0.MoveNext (Duende.AccessTokenManagement.OpenIdConnect, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /_/src/Duende.AccessTokenManagement.OpenIdConnect/TokenManagementHttpContextExtensions.cs:35)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at App.Cloud.Web.BffUserAccessTokenProvider+<GetBearerTokenAsync>d__2.MoveNext (App.Cloud.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\App.Cloud.Web\BffUserAccessTokenProvider.cs:27)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at App.Cloud.Infrastructure.Services.CloudServices.ExternalServiceClient+<CallApi>d__3.MoveNext (App.Cloud.Shared, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\App.Cloud.Infrastructure\Services\CloudServices\ExternalServiceClient.cs:43)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at App.Cloud.Infrastructure.Services.CloudServices.AppAccessControlAssetsProvider+<GetTenantSourceSystemsAsync>d__5.MoveNext (App.Cloud.Shared, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\App.Cloud.Infrastructure\Services\CloudServices\AppAccessControlAssetsProvider.cs:45)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at WebApiShared.Infrastructure.Services.Multitenancy.UserSourceSystemsProvider+<GetUserSourceSystemsAsync>d__6.MoveNext (WebApiShared.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\WebApiShared.Infrastructure\Services\Multitenancy\UserSourceSystemsProvider.cs:44)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at WebApiShared.Infrastructure.Services.Multitenancy.UserSourceSystemsProvider+<GetUserSourceSystemAsync>d__7.MoveNext (WebApiShared.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\WebApiShared.Infrastructure\Services\Multitenancy\UserSourceSystemsProvider.cs:62)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at App.Cloud.Web.Hubs.TelemetryHub+<PerformControllerSubscriberAction>d__11.MoveNext (App.Cloud.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\App.Cloud.Web\Hubs\TelemetryHub.cs:159)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Appte.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at App.Cloud.Web.Hubs.TelemetryHub+<PerformControllerSubscriberAction>d__11.MoveNext (App.Cloud.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: D:\a\1\s\App.Cloud.Web\Hubs\TelemetryHub.cs:159)
@josephdecock
Copy link
Member

There's an architectural problem here, which is that signalr is using websockets, and websockets don't allow us to interact with the cookie where tokens are stored.

In order to make the BFF work with signalr, you'll need to change how the BFF stores tokens. To do so, you'll have to implement the IUserTokenStore to store the tokens outside the cookies. We've created an issue to add more documentation or samples about how to do that.

@erkanmaras
Copy link
Author

Thanks for the explanation, I tried several hacks as follows, but none of them worked in our case. Implementing a custom token-store seems like the only solution.

 public class SignalRTokenProvider : ITokenProvider
    {
        private readonly IHttpContextAccessor httpContextAccessor;
        private readonly IUserTokenEndpointService tokenEndpointService;
        private readonly IUserTokenRequestSynchronization sync;
        private readonly IUserSessionStore sessionStore;
        private readonly ILogger<SignalRTokenProvider> logger;
        private readonly IDataProtector protector;
        private const string ExpiresAtToken = "expires_at";

        public SignalRTokenProvider(
            IHttpContextAccessor httpContextAccessor,
            IUserTokenEndpointService tokenEndpointService,
            IUserTokenRequestSynchronization sync,
            IUserSessionStore sessionStore,
            IDataProtectionProvider dataProtectionProvider,
            ILogger<SignalRTokenProvider> logger)
        {
            this.httpContextAccessor = httpContextAccessor;
            this.tokenEndpointService = tokenEndpointService;
            this.sync = sync;
            this.sessionStore = sessionStore;
            this.logger = logger;
            this.protector = dataProtectionProvider.CreateProtector("Duende.Bff.ServerSideTicketStore");
        }
 

        public async Task<string> GetTokenAsync()
        {
            var authResultFeature = this.httpContextAccessor.HttpContext!.Features.Get<IAuthenticateResultFeature>();
            if (authResultFeature?.AuthenticateResult?.Ticket is null)
            {
                throw new InvalidOperationException($"Authentication ticket is null!");
            }

            var authResult = authResultFeature.AuthenticateResult;

            if (!DateTimeOffset.TryParse(authResult.Ticket!.Properties.GetTokenValue(ExpiresAtToken), out var expireAt))
            {
                expireAt = DateTimeOffset.MaxValue;
            }

            string accessToken = null;

            if (expireAt <= DateTimeOffset.UtcNow)
            {
                var userToken = await this.sync.SynchronizeAsync(authResult.Ticket.Properties.GetTokenValue(OpenIdConnectParameterNames.RefreshToken)!, async () =>
                {

                    var sessionId = authResult.Ticket.Principal.Claims.FirstOrDefault(c => c.Type == "sid");
                    var subjectId = authResult.Ticket.Principal.Claims.FirstOrDefault(c => c.Type == "sub");

                    if (sessionId is null || subjectId is null)
                    {
                        throw new InvalidOperationException($"SessionId or SubjectId missing in auth ticket!");
                    }

                    var sessions = await this.sessionStore.GetUserSessionsAsync(new UserSessionsFilter { SessionId = sessionId.Value, SubjectId = subjectId.Value });

                    if (sessions.Count > 1)
                    {
                        this.logger.LogDebug("More than one user session exist.{sessionId} , {subjectId}", sessionId.Value, subjectId.Value);
                    }

                    var session = sessions.MaxBy(s => s.Renewed);
                    if (session == null)
                    {
                        this.logger.LogDebug("Session is null. {sessionId} , {subject}", sessionId.Value, subjectId.Value);
                        return null;
                    }

                    var ticket = session.Deserialize(this.protector, this.logger);
                    if (ticket == null)
                    {
                        this.logger.LogDebug("Ticket is null");
                        return null;
                    }

                    var refreshToken = ticket.Properties.GetTokenValue(OpenIdConnectParameterNames.RefreshToken);

                    if (refreshToken == null)
                    {
                        this.logger.LogDebug("RefreshToken couldn't find auth ticket");
                        return null;
                    }

                    var refreshedToken = await this.tokenEndpointService.RefreshAccessTokenAsync(refreshToken, new UserTokenRequestParameters());

                    if (refreshedToken.IsError)
                    {
                        throw new InvalidOperationException($"Token refresh error: {refreshedToken.Error}");
                    }

                    ticket.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, refreshedToken.AccessToken);
                    ticket.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, refreshedToken.RefreshToken);
                    ticket.Properties.UpdateTokenValue(ExpiresAtToken, refreshedToken.Expiration.ToString("o", CultureInfo.InvariantCulture));

                    session.Ticket = ticket.Serialize(this.protector);
                    await this.sessionStore.UpdateUserSessionAsync(session.Key, session);

                    return refreshedToken;
                });

                accessToken = userToken?.AccessToken;
            }

            accessToken ??= authResult.Ticket.Properties.GetTokenValue(OpenIdConnectParameterNames.AccessToken);
            return accessToken;
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants