Skip to content

Commit 46d9c93

Browse files
authored
Implement "Close on auth expiration" for serverless mode (#2036)
Close #1954
1 parent b00287b commit 46d9c93

File tree

3 files changed

+59
-36
lines changed

3 files changed

+59
-36
lines changed

src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiateProcessor.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ public async Task<NegotiationResponse> NegotiateAsync(string hubName, Negotiatio
5050
{
5151
claimProvider = () => claims;
5252
}
53-
var claimsWithUserId = ClaimsUtility.BuildJwtClaims(httpContext?.User, userId: userId, claimProvider, enableDetailedErrors: enableDetailedErrors, isDiagnosticClient: isDiagnosticClient);
53+
var closeOnAuthenticationExpiration = negotiationOptions.CloseOnAuthenticationExpiration;
54+
var authenticationExpiresOn = closeOnAuthenticationExpiration ? DateTimeOffset.UtcNow.Add(negotiationOptions.TokenLifetime) : default(DateTimeOffset?);
55+
var claimsWithUserId = ClaimsUtility.BuildJwtClaims(httpContext?.User, userId: userId, claimProvider, enableDetailedErrors: enableDetailedErrors, isDiagnosticClient: isDiagnosticClient, closeOnAuthenticationExpiration: closeOnAuthenticationExpiration, authenticationExpiresOn: authenticationExpiresOn);
5456

5557
var tokenTask = provider.GenerateClientAccessTokenAsync(hubName, claimsWithUserId, lifetime);
5658
await tokenTask.OrTimeout(cancellationToken, Timeout, GeneratingTokenTaskDescription);

src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiationOptions.cs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,44 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Http.Connections;
99

10-
namespace Microsoft.Azure.SignalR.Management
10+
namespace Microsoft.Azure.SignalR.Management;
11+
12+
public class NegotiationOptions
1113
{
12-
public class NegotiationOptions
13-
{
14-
internal static readonly NegotiationOptions Default = new NegotiationOptions();
15-
16-
/// <summary>
17-
/// Gets or sets the HTTP context object that might provide information for routing and generating access token.
18-
/// </summary>
19-
public HttpContext HttpContext { get; set; }
20-
21-
/// <summary>
22-
/// Gets or sets the user ID. If null, the identity name in <see cref="HttpContext.User" /> of the property <see cref="HttpContext"/> will be used.
23-
/// </summary>
24-
public string UserId { get; set; }
25-
26-
/// <summary>
27-
/// Gets or sets the claim list to be put into access token. If null, the claims in <see cref="HttpContext.User"/> of the property <see cref="HttpContext"/> will be used.
28-
/// </summary>
29-
public IList<Claim> Claims { get; set; }
30-
31-
/// <summary>
32-
/// Gets or sets the lifetime of <see cref="NegotiationResponse.AccessToken"/>. Default value is one hour.
33-
/// </summary>
34-
public TimeSpan TokenLifetime { get; set; } = TimeSpan.FromHours(1);
35-
36-
/// <summary>
37-
/// Gets or sets the flag indicates whether the client is a diagnostic client.
38-
/// </summary>
39-
public bool IsDiagnosticClient { get; set; } = false;
40-
41-
/// <summary>
42-
/// Gets or sets the flag indicates whether detailed errors are logged in the client side.
43-
/// </summary>
44-
public bool EnableDetailedErrors { get; set; } = false;
45-
}
14+
internal static readonly NegotiationOptions Default = new NegotiationOptions();
15+
16+
/// <summary>
17+
/// Gets or sets the HTTP context object that might provide information for routing and generating access token.
18+
/// </summary>
19+
public HttpContext HttpContext { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the user ID. If null, the identity name in <see cref="HttpContext.User" /> of the property <see cref="HttpContext"/> will be used.
23+
/// </summary>
24+
public string UserId { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the claim list to be put into access token. If null, the claims in <see cref="HttpContext.User"/> of the property <see cref="HttpContext"/> will be used.
28+
/// </summary>
29+
public IList<Claim> Claims { get; set; }
30+
31+
/// <summary>
32+
/// Gets or sets the lifetime of <see cref="NegotiationResponse.AccessToken"/>. Default value is one hour.
33+
/// </summary>
34+
public TimeSpan TokenLifetime { get; set; } = TimeSpan.FromHours(1);
35+
36+
/// <summary>
37+
/// Gets or sets the flag indicates whether the client is a diagnostic client.
38+
/// </summary>
39+
public bool IsDiagnosticClient { get; set; } = false;
40+
41+
/// <summary>
42+
/// Gets or sets the flag indicates whether detailed errors are logged in the client side.
43+
/// </summary>
44+
public bool EnableDetailedErrors { get; set; } = false;
45+
46+
/// <summary>
47+
/// 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 <see cref="TokenLifetime"/>.
48+
/// </summary>
49+
public bool CloseOnAuthenticationExpiration { get; set; } = false;
4650
}

test/Microsoft.Azure.SignalR.Management.Tests/NegotiateProcessorFacts.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ from claims in _claimLists
3131
from appName in _appNames
3232
select new object[] { userId, claims, appName };
3333

34+
[Fact]
35+
public async Task GenerateTokenWithCloseOnAuthExpiration()
36+
{
37+
var hubContext = await new ServiceManagerBuilder()
38+
.WithOptions(o => o.ConnectionString = "Endpoint=https://zityang-signalr-standard-dev.service.signalr.net;AccessKey=K/JHYahkm7MAZHc9G0R5rvCM5gCI/Fh9oIgVF9xdWFA=;Version=1.0;")
39+
.BuildServiceManager()
40+
.CreateHubContextAsync("hub", default);
41+
var now = DateTimeOffset.UtcNow;
42+
var negotiateResponse = await hubContext.NegotiateAsync(new NegotiationOptions { CloseOnAuthenticationExpiration = true, TokenLifetime = TimeSpan.FromSeconds(30) });
43+
var token = JwtTokenHelper.JwtHandler.ReadJwtToken(negotiateResponse.AccessToken);
44+
var closeOnAuthExpiration = Assert.Single(token.Claims.Where(c => c.Type == Constants.ClaimType.CloseOnAuthExpiration));
45+
Assert.Equal("true", closeOnAuthExpiration.Value);
46+
var ttl = Assert.Single(token.Claims.Where(c => c.Type == Constants.ClaimType.AuthExpiresOn));
47+
Assert.True(long.TryParse(ttl.Value, out var expiresOn));
48+
Assert.InRange(DateTimeOffset.FromUnixTimeSeconds(expiresOn), now.AddSeconds(29), now.AddSeconds(32));
49+
}
50+
3451
[Theory]
3552
[MemberData(nameof(TestGenerateAccessTokenData))]
3653
public async Task GenerateClientEndpoint(string userId, Claim[] claims, string appName)

0 commit comments

Comments
 (0)