Skip to content

Changing access token during SignalR session (with websockets protocol) #13144

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

Open
Tracked by #23357
simeyla opened this issue Jul 6, 2019 — with docs.microsoft.com · 22 comments
Open
Tracked by #23357
Labels
product-question SignalR Source - Docs.ms Docs Customer feedback via GitHub Issue
Milestone

Comments

Copy link

simeyla commented Jul 6, 2019

The following phrase is emphasized on https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-2.2:

The access token function you provide is called before every HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.

However it's easy to confuse 'http request' here with the concept of 'message sent'.

When using websockets there is only one http request made when you first connect - after that the token isn't sent again and my function isn't called. The above paragraph kind of implies it will automagically refresh itself when it changes which for websockets isn't entirely accurate.

Can you make it clearer what the recommended procedure is for changing access token when connected with websockets. I assume I'm supposed to just disconnect and reconnect.

Although to be frank this is kind of a massive pain and I'm almost tempted to just send the access token as part of the message (which in my case is rarely needed anyway).


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

@dotnet-bot dotnet-bot added SignalR Source - Docs.ms Docs Customer feedback via GitHub Issue labels Jul 6, 2019
@simeyla simeyla changed the title Changing access token during session Changing access token during SignalR session (with websockets protocol) Jul 6, 2019
@Rick-Anderson Rick-Anderson added this to the 3.0 milestone Jul 6, 2019
Copy link

1 million times this. I'm bashing my head against the desk wondering why my accessTokenFactory method isn't being called once the connection is established, wondering if I'm doing something wrong. This note is completely misleading and should be removed. What is MSFT's recommended approach? Is it in fact to disconnect and reconnect when a new access token is received? This seems like a massive oversight.

@calhamdower
Copy link

+1 here also. Very confusing.
I've also found that the Authorize attribute doesnt really protect you from expired tokens also.
dotnet/aspnetcore#5283
So with Websockets, once you set up a connection.. on the client side you're not easily able to keep your tokens up to date: and then on the server side: it doesn't check if they're expired anyway...

@darkpq
Copy link

darkpq commented Oct 16, 2019

tl;dr: Does using a pattern of establishing/reestablishing websocket-based connections to a hub on an as-needed basis create too much connection overhead (or defeat the purpose of web-sockets to begin with) to be useful as a solution to the above problems? (The idea being that each re-established connection would use a token re-fresh pattern)

Story:
I'm currently trying to write a live multi-user app using Blazor client-side with a feature-rich server-side using signalr. I have signalr connections working and token validation working with all of the above issues with authorization (tokens only validated on connection, and web-socket calls to hub methods are 'authorized' with expired tokens indefinitely on the same connection as was established when the token was valid. Re-connection attempts fail authorization with an expired token).

All that said, if I'm going to stick with jwt auth, is it just better to use a short expiry and a refresh token pattern, with a policy that between client-side actions that don't occur frequently enough connections are dropped, as a solution as opposed to having to have "timed tasks" on both the client side and server side? (Those tasks being what causes expired tokens to be re-issued?)

Copy link

Microsoft, what is your guidance regarding expiring tokens both on the client and the server?

Copy link

kherona commented May 18, 2020

This would cause a lot of errors goes undetected. I am hoping that someone can provide a viable solution or workaround.

Copy link

kherona commented May 18, 2020

Here is another request about the same issue: #18265

@Tommigun1980
Copy link

Microsoft, there seems to be quite a lot of developers who are confused about how to refresh the access token in a SignalR connection (me included), as the token factory seems to be executed only on connection start and if the token is revoked it's not checked for again. Could you please clarify what the suggested flow is for handling this as it is not brought up in the documentation. Thanks so much.

@davidfowl
Copy link
Member

I think this is something we’ll have to think about more but let me ask some questions about the expected behavior:

Whats the ideal experience? Do you want to have the token expire and have the physical connection drop? That’s the easiest thing to do. The client would then re-connect and rerun then access token factory client side and you could re run whatever logic you need to to get a new token.

If we don’t do that does it mean the physical connection stays open and it’s less clear what it would do, how the client would re-negotiate etc

@mmulhearn
Copy link

IMO, the access token factory should maintain access token state and once the token expires, should invoke for a new one. If a new one is provided and is valid, the connection maintains, otherwise the connection drops.

@kherona
Copy link

kherona commented May 19, 2020

@davidfowl I believe we should invoke new refresh token as suggested by mmulhearn, this is how we do it in non-websocket anyway, however the feature should be supported on client side/server side or both. Currently on server side we have read query_string and set Context.Token while I would expect the SignalR middle ware to automatically take care of that.

Now on the clientside I would suggest to add onAuthenticationFailure retry/reconnect (i.e 401 received) behavior similar to withAutomaticReconnect, then the accessTokenFactory can re-read whatever value we provided and attempt to reconnect the connection with the new accessTokenFactory.

@CodyPaul
Copy link

CodyPaul commented Mar 5, 2021

What's the state of this? Curious what the team has come up with or decided on before I roll my own refresh logic.

@Tommigun1980
Copy link

Somebody at Microsoft needs to take point on these issues. Using SignalR has been more time consuming than doing everything from scratch, as it doesn’t handle the most primitive cases.
@CodyPaul Considering the complete lack of interest to fix this most primitive issue don’t hold your breath for a resolution any time soon. The current handling of the token clearly shows it’s not meant for production use and you’ll need to make your own anyway.

@davidfowl
Copy link
Member

We're looking to invest in this area (whether doc or feature) during this time frame. Perhaps we should write something even if temporary in the docs to explain the current options (even if they aren't good).

@Niproblema
Copy link

We're looking to invest in this area (whether doc or feature) during this time frame. Perhaps we should write something even if temporary in the docs to explain the current options (even if they aren't good).

Any news @davidfowl ?

@davidfowl
Copy link
Member

@BrennanConroy can you provide an update based on our discussion yesterday?

@Niproblema
Copy link

@BrennanConroy can you provide an update based on our discussion yesterday?

Anxiously awaiting reply

@willykurmann
Copy link

I think this is something we’ll have to think about more but let me ask some questions about the expected behavior:

Whats the ideal experience? Do you want to have the token expire and have the physical connection drop? That’s the easiest thing to do. The client would then re-connect and rerun then access token factory client side and you could re run whatever logic you need to to get a new token.

If we don’t do that does it mean the physical connection stays open and it’s less clear what it would do, how the client would re-negotiate etc

Hi @davidfowl,

Don't have the answer on how it'd reconnect, but it seems there's an actual way to update the HttpContext from a hub method:

var httpContext = this.Context.GetHttpContext()!;
httpContext.Items["access_token"] = updatedToken;
... re-authenticate user ...
httpContext.User = authenticateResult.Principal

And the only thing that prevent this new user to be used is how HubConnectionContext.Usermethod has been implemented.

 public virtual ClaimsPrincipal User
        {
            get
            {
                if (_user is null)
                {
                    _user = Features.Get<IConnectionUserFeature>()?.User ?? new ClaimsPrincipal();
                }
                return _user;
            }
        }

It'd work fine with this new code IMO as all subsequent calls to HubConnectionContext.Userwould use the new user:

public virtual ClaimsPrincipal User
        {
            get
            {
               return Features.Get<IConnectionUserFeature>()?.User ?? new ClaimsPrincipal();
            }
        }

This only thing is that theres no way to extend/proxy HubConnectionContext because of HubConnectionHandler
and as mentionned here: dotnet/aspnetcore#41709

public override async Task OnConnectedAsync(ConnectionContext connection)
        {
           ...
           var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory);
          ...
       }

@Niproblema
Copy link

Niproblema commented Aug 11, 2022

Here is how I did it;

        private void ProlongConnectionAuthenticationExpiration(ClaimsPrincipal claimsPrincipal)
        {
            if (claimsPrincipal == null)
            {
                throw new ArgumentNullException(nameof(claimsPrincipal));
            }

            // Get validation parameters.
            TokenValidationParameters? validationParameters = _serviceProvider.GetService<TokenValidationParameters>();
            if (validationParameters == null)
            {
                throw new InvalidOperationException("Failed to determine validation parameters.");
            }

            // Define new expiration time.
            string? newExpirationTime = claimsPrincipal.Claims.FirstOrDefault(claim => claim.Type == "exp")?.Value;
            if (newExpirationTime == null && validationParameters.ValidateLifetime)
            {
                throw new InvalidOperationException("Newly provided claims do not contain expiration time.");
            }
            if (!long.TryParse(newExpirationTime, out long unixSecondsExpiration))
            {
                throw new InvalidOperationException("Failed to parse new expiration time.");
            }

            DateTimeOffset expirationTimeUtc;
            try
            {
                expirationTimeUtc = DateTimeOffset.FromUnixTimeSeconds(unixSecondsExpiration).Add(validationParameters.ClockSkew);
            }
            catch (ArgumentOutOfRangeException)
            {
                throw new InvalidOperationException("Failed to parse new expiration time. Out of range exception");
            }

            // Get internal http context
            IHttpContextFeature? contextFeature = Context.Features.Get<IHttpContextFeature>();

            if (contextFeature == null)
            {
                throw new InvalidOperationException("Failed to resolve feature " + nameof(IHttpContextFeature));
            }

            PropertyInfo? pi = contextFeature.GetType().GetProperty("AuthenticationExpiration", BindingFlags.Instance | BindingFlags.NonPublic);
            if (pi == null)
            {
                throw new InvalidOperationException("Failed to resolve http connection context fields.");
            }
            object? previousExpirationObject = pi.GetValue(contextFeature);
            if (previousExpirationObject == null)
            {
                throw new InvalidOperationException("Failed to determine previous expiration time.");
            }
            DateTimeOffset previousExpiration = (DateTimeOffset)previousExpirationObject;

            // Log update.
            _logger.LogDebug("Prolonging authentication for connection {0}. Previous expiration [{1}]. Prolonged expiration [{2}].",
                Context.ConnectionId,
                previousExpiration.ToUniversalTime().ToString(),
                expirationTimeUtc.ToString());

            // Update AuthenticationResult feature if used.
            IAuthenticateResultFeature? authResultFeature = Context.Features.Get<IAuthenticateResultFeature>();
            if (authResultFeature != null)
            {
                AuthenticationProperties ap = new AuthenticationProperties();
                AuthenticationTicket authTicket = new AuthenticationTicket(claimsPrincipal, ap, JwtBearerDefaults.AuthenticationScheme);
                authResultFeature.AuthenticateResult = AuthenticateResult.Success(authTicket);
            }

            // Set new expiration date.
            pi.SetValue(contextFeature, expirationTimeUtc);
        }

Nasty, but that's how it goes with bad frameworks.
Expected nothing less from microsoft managed projects

@Tommigun1980
Copy link

Nasty, but that's how it goes with bad frameworks. Expected nothing less from microsoft managed projects

It's quite incredible that the response from Microsoft was that "they'll have to think about it". Think about it, years after release? This is such a common case that it's incredible it wasn't thought about when the framework was made. Like... how can you miss adding any kind of mechanism for expiring tokens? I'm so glad I stopped using these kinds of Microsoft products, I was naive enough to think they'd save me some time.

@Gruski
Copy link

Gruski commented Jan 5, 2023

Is there a fix for this yet? It's been like 3 years.

@davidfowl
Copy link
Member

No, there is no fix, you can follow this issue dotnet/aspnetcore#5297

@s-beji
Copy link

s-beji commented May 9, 2023

Microsoft please dont be so slow by fixing so important aspekts from the framework
5 Years is not excusable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
product-question SignalR Source - Docs.ms Docs Customer feedback via GitHub Issue
Projects
None yet
Development

No branches or pull requests