AspNetCore.Docs
AspNetCore.Docs copied to clipboard
Changing access token during SignalR session (with websockets protocol)
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.
- ID: d0da2144-3730-f8ef-1d16-a89f11fd6198
- Version Independent ID: 6d8f5bc8-8f5c-ba13-b7f2-0366eac46f81
- Content: Authentication and authorization in ASP.NET Core SignalR
- Content Source: aspnetcore/signalr/authn-and-authz.md
- Product: aspnet-core
- Technology: aspnetcore-signalr
- GitHub Login: @bradygaster
- Microsoft Alias: bradyg
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.
+1 here also. Very confusing. I've also found that the Authorize attribute doesnt really protect you from expired tokens also. https://github.com/aspnet/AspNetCore/issues/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...
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?)
Microsoft, what is your guidance regarding expiring tokens both on the client and the server?
This would cause a lot of errors goes undetected. I am hoping that someone can provide a viable solution or workaround.
Here is another request about the same issue: https://github.com/dotnet/AspNetCore.Docs/issues/18265
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.
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
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.
@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.
What's the state of this? Curious what the team has come up with or decided on before I roll my own refresh logic.
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.
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).
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 ?
@BrennanConroy can you provide an update based on our discussion yesterday?
@BrennanConroy can you provide an update based on our discussion yesterday?
Anxiously awaiting reply
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.User
method 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.User
would 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: https://github.com/dotnet/aspnetcore/issues/41709
public override async Task OnConnectedAsync(ConnectionContext connection)
{
...
var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory);
...
}
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
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.
Is there a fix for this yet? It's been like 3 years.
No, there is no fix, you can follow this issue https://github.com/dotnet/aspnetcore/issues/5297
Microsoft please dont be so slow by fixing so important aspekts from the framework 5 Years is not excusable