Skip to content

Commit

Permalink
Implement "Close on auth expiration" for serverless mode (#2036)
Browse files Browse the repository at this point in the history
Close #1954
  • Loading branch information
Y-Sindo authored Sep 2, 2024
1 parent b00287b commit 46d9c93
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ public async Task<NegotiationResponse> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/// <summary>
/// Gets or sets the HTTP context object that might provide information for routing and generating access token.
/// </summary>
public HttpContext HttpContext { get; set; }

/// <summary>
/// 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.
/// </summary>
public string UserId { get; set; }

/// <summary>
/// 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.
/// </summary>
public IList<Claim> Claims { get; set; }

/// <summary>
/// Gets or sets the lifetime of <see cref="NegotiationResponse.AccessToken"/>. Default value is one hour.
/// </summary>
public TimeSpan TokenLifetime { get; set; } = TimeSpan.FromHours(1);

/// <summary>
/// Gets or sets the flag indicates whether the client is a diagnostic client.
/// </summary>
public bool IsDiagnosticClient { get; set; } = false;

/// <summary>
/// Gets or sets the flag indicates whether detailed errors are logged in the client side.
/// </summary>
public bool EnableDetailedErrors { get; set; } = false;
}
internal static readonly NegotiationOptions Default = new NegotiationOptions();

/// <summary>
/// Gets or sets the HTTP context object that might provide information for routing and generating access token.
/// </summary>
public HttpContext HttpContext { get; set; }

/// <summary>
/// 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.
/// </summary>
public string UserId { get; set; }

/// <summary>
/// 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.
/// </summary>
public IList<Claim> Claims { get; set; }

/// <summary>
/// Gets or sets the lifetime of <see cref="NegotiationResponse.AccessToken"/>. Default value is one hour.
/// </summary>
public TimeSpan TokenLifetime { get; set; } = TimeSpan.FromHours(1);

/// <summary>
/// Gets or sets the flag indicates whether the client is a diagnostic client.
/// </summary>
public bool IsDiagnosticClient { get; set; } = false;

/// <summary>
/// Gets or sets the flag indicates whether detailed errors are logged in the client side.
/// </summary>
public bool EnableDetailedErrors { get; set; } = false;

/// <summary>
/// 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"/>.
/// </summary>
public bool CloseOnAuthenticationExpiration { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 46d9c93

Please sign in to comment.