Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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