-
Notifications
You must be signed in to change notification settings - Fork 0
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
Comments
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. |
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;
}
} |
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 text was updated successfully, but these errors were encountered: