diff --git a/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiateProcessor.cs b/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiateProcessor.cs index d945cdee7..83ef0afe4 100644 --- a/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiateProcessor.cs +++ b/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiateProcessor.cs @@ -50,7 +50,9 @@ public async Task NegotiateAsync(string hubName, Negotiatio { claimProvider = () => claims; } - var claimsWithUserId = ClaimsUtility.BuildJwtClaims(httpContext?.User, userId: userId, claimProvider, enableDetailedErrors: enableDetailedErrors, isDiagnosticClient: isDiagnosticClient); + var closeOnAuthenticationExpiration = negotiationOptions.CloseOnAuthenticationExpiration; + var authenticationExpiresOn = closeOnAuthenticationExpiration ? DateTimeOffset.UtcNow.Add(negotiationOptions.TokenLifetime) : default(DateTimeOffset?); + var claimsWithUserId = ClaimsUtility.BuildJwtClaims(httpContext?.User, userId: userId, claimProvider, enableDetailedErrors: enableDetailedErrors, isDiagnosticClient: isDiagnosticClient, closeOnAuthenticationExpiration: closeOnAuthenticationExpiration, authenticationExpiresOn: authenticationExpiresOn); var tokenTask = provider.GenerateClientAccessTokenAsync(hubName, claimsWithUserId, lifetime); await tokenTask.OrTimeout(cancellationToken, Timeout, GeneratingTokenTaskDescription); diff --git a/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiationOptions.cs b/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiationOptions.cs index 3ead293b3..8794d439c 100644 --- a/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiationOptions.cs +++ b/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiationOptions.cs @@ -7,40 +7,44 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; -namespace Microsoft.Azure.SignalR.Management +namespace Microsoft.Azure.SignalR.Management; + +public class NegotiationOptions { - public class NegotiationOptions - { - internal static readonly NegotiationOptions Default = new NegotiationOptions(); - - /// - /// Gets or sets the HTTP context object that might provide information for routing and generating access token. - /// - public HttpContext HttpContext { get; set; } - - /// - /// Gets or sets the user ID. If null, the identity name in of the property will be used. - /// - public string UserId { get; set; } - - /// - /// Gets or sets the claim list to be put into access token. If null, the claims in of the property will be used. - /// - public IList Claims { get; set; } - - /// - /// Gets or sets the lifetime of . Default value is one hour. - /// - public TimeSpan TokenLifetime { get; set; } = TimeSpan.FromHours(1); - - /// - /// Gets or sets the flag indicates whether the client is a diagnostic client. - /// - public bool IsDiagnosticClient { get; set; } = false; - - /// - /// Gets or sets the flag indicates whether detailed errors are logged in the client side. - /// - public bool EnableDetailedErrors { get; set; } = false; - } + internal static readonly NegotiationOptions Default = new NegotiationOptions(); + + /// + /// Gets or sets the HTTP context object that might provide information for routing and generating access token. + /// + public HttpContext HttpContext { get; set; } + + /// + /// Gets or sets the user ID. If null, the identity name in of the property will be used. + /// + public string UserId { get; set; } + + /// + /// Gets or sets the claim list to be put into access token. If null, the claims in of the property will be used. + /// + public IList Claims { get; set; } + + /// + /// Gets or sets the lifetime of . Default value is one hour. + /// + public TimeSpan TokenLifetime { get; set; } = TimeSpan.FromHours(1); + + /// + /// Gets or sets the flag indicates whether the client is a diagnostic client. + /// + public bool IsDiagnosticClient { get; set; } = false; + + /// + /// Gets or sets the flag indicates whether detailed errors are logged in the client side. + /// + public bool EnableDetailedErrors { get; set; } = false; + + /// + /// Gets or sets the flag indicates that whether the connection should be closed when the authentication token expires. The lifetime of the token is determined by . + /// + public bool CloseOnAuthenticationExpiration { get; set; } = false; } \ No newline at end of file diff --git a/test/Microsoft.Azure.SignalR.Management.Tests/NegotiateProcessorFacts.cs b/test/Microsoft.Azure.SignalR.Management.Tests/NegotiateProcessorFacts.cs index cf98ad00d..f19129f5d 100644 --- a/test/Microsoft.Azure.SignalR.Management.Tests/NegotiateProcessorFacts.cs +++ b/test/Microsoft.Azure.SignalR.Management.Tests/NegotiateProcessorFacts.cs @@ -31,6 +31,23 @@ from claims in _claimLists from appName in _appNames select new object[] { userId, claims, appName }; + [Fact] + public async Task GenerateTokenWithCloseOnAuthExpiration() + { + var hubContext = await new ServiceManagerBuilder() + .WithOptions(o => o.ConnectionString = "Endpoint=https://zityang-signalr-standard-dev.service.signalr.net;AccessKey=K/JHYahkm7MAZHc9G0R5rvCM5gCI/Fh9oIgVF9xdWFA=;Version=1.0;") + .BuildServiceManager() + .CreateHubContextAsync("hub", default); + var now = DateTimeOffset.UtcNow; + var negotiateResponse = await hubContext.NegotiateAsync(new NegotiationOptions { CloseOnAuthenticationExpiration = true, TokenLifetime = TimeSpan.FromSeconds(30) }); + var token = JwtTokenHelper.JwtHandler.ReadJwtToken(negotiateResponse.AccessToken); + var closeOnAuthExpiration = Assert.Single(token.Claims.Where(c => c.Type == Constants.ClaimType.CloseOnAuthExpiration)); + Assert.Equal("true", closeOnAuthExpiration.Value); + var ttl = Assert.Single(token.Claims.Where(c => c.Type == Constants.ClaimType.AuthExpiresOn)); + Assert.True(long.TryParse(ttl.Value, out var expiresOn)); + Assert.InRange(DateTimeOffset.FromUnixTimeSeconds(expiresOn), now.AddSeconds(29), now.AddSeconds(32)); + } + [Theory] [MemberData(nameof(TestGenerateAccessTokenData))] public async Task GenerateClientEndpoint(string userId, Claim[] claims, string appName)