mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-24 21:46:52 +01:00
Ensure that SignalR works with the webpanel
- Fix issue with CORS preventing static file browsing. - AllowCredentials in CORS. Switch from `AllowAnyOrigin` to a wildcard matching `Func` to bypass the CORS specification that says you can't do that. - Add workaround for legacy SignalR `access_token` query string. - Get token `nbf` from claims rather than through the composition root.
This commit is contained in:
@@ -473,33 +473,6 @@ namespace Tgstation.Server.Host.Core
|
||||
logger.LogTrace("Swagger API generation enabled");
|
||||
}
|
||||
|
||||
// Enable endpoint routing
|
||||
applicationBuilder.UseRouting();
|
||||
|
||||
// Set up CORS based on configuration if necessary
|
||||
Action<CorsPolicyBuilder> corsBuilder = null;
|
||||
if (controlPanelConfiguration.AllowAnyOrigin)
|
||||
{
|
||||
logger.LogTrace("Access-Control-Allow-Origin: *");
|
||||
corsBuilder = builder => builder.AllowAnyOrigin();
|
||||
}
|
||||
else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
|
||||
{
|
||||
logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
|
||||
corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
|
||||
}
|
||||
|
||||
var originalBuilder = corsBuilder;
|
||||
corsBuilder = builder =>
|
||||
{
|
||||
builder
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.SetPreflightMaxAge(TimeSpan.FromDays(1));
|
||||
originalBuilder?.Invoke(builder);
|
||||
};
|
||||
applicationBuilder.UseCors(corsBuilder);
|
||||
|
||||
// spa loading if necessary
|
||||
if (controlPanelConfiguration.Enable)
|
||||
{
|
||||
@@ -518,6 +491,34 @@ namespace Tgstation.Server.Host.Core
|
||||
logger.LogTrace("Web control panel disabled!");
|
||||
#endif
|
||||
|
||||
// Enable endpoint routing
|
||||
applicationBuilder.UseRouting();
|
||||
|
||||
// Set up CORS based on configuration if necessary
|
||||
Action<CorsPolicyBuilder> corsBuilder = null;
|
||||
if (controlPanelConfiguration.AllowAnyOrigin)
|
||||
{
|
||||
logger.LogTrace("Access-Control-Allow-Origin: *");
|
||||
corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
|
||||
}
|
||||
else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
|
||||
{
|
||||
logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
|
||||
corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
|
||||
}
|
||||
|
||||
var originalBuilder = corsBuilder;
|
||||
corsBuilder = builder =>
|
||||
{
|
||||
builder
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials()
|
||||
.SetPreflightMaxAge(TimeSpan.FromDays(1));
|
||||
originalBuilder?.Invoke(builder);
|
||||
};
|
||||
applicationBuilder.UseCors(corsBuilder);
|
||||
|
||||
// validate the API version
|
||||
applicationBuilder.UseApiCompatibility();
|
||||
|
||||
@@ -596,10 +597,20 @@ namespace Tgstation.Server.Host.Core
|
||||
jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!");
|
||||
jwtBearerOptions.Events = new JwtBearerEvents
|
||||
{
|
||||
OnTokenValidated = tokenValidatedContext =>
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var acf = tokenValidatedContext.HttpContext.RequestServices.GetRequiredService<AuthenticationContextFactory>();
|
||||
acf.SetTokenNbf(tokenValidatedContext.SecurityToken.ValidFrom);
|
||||
if (String.IsNullOrWhiteSpace(context.Token))
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(accessToken) &&
|
||||
path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
@@ -48,7 +49,7 @@ namespace Tgstation.Server.Host.Security
|
||||
|
||||
var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub);
|
||||
if (userIdClaim == default)
|
||||
throw new InvalidOperationException("Missing required claim!");
|
||||
throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
|
||||
|
||||
long userId;
|
||||
try
|
||||
@@ -60,9 +61,26 @@ namespace Tgstation.Server.Host.Security
|
||||
throw new InvalidOperationException("Failed to parse user ID!", e);
|
||||
}
|
||||
|
||||
var nbfClaim = principal.FindFirst(JwtRegisteredClaimNames.Nbf);
|
||||
if (nbfClaim == default)
|
||||
throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Nbf}' claim!");
|
||||
|
||||
DateTimeOffset nbf;
|
||||
try
|
||||
{
|
||||
nbf = new DateTimeOffset(
|
||||
EpochTime.DateTime(
|
||||
Int64.Parse(nbfClaim.Value, CultureInfo.InvariantCulture)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse nbf!", ex);
|
||||
}
|
||||
|
||||
var authenticationContext = await authenticationContextFactory.CreateAuthenticationContext(
|
||||
userId,
|
||||
apiHeaders?.InstanceId,
|
||||
nbf,
|
||||
CancellationToken.None); // DCT: None available
|
||||
|
||||
if (authenticationContext.Valid)
|
||||
|
||||
@@ -46,11 +46,6 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
readonly AuthenticationContext currentAuthenticationContext;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DateTimeOffset"/> the request's token must be valid after.
|
||||
/// </summary>
|
||||
DateTimeOffset? validAfter;
|
||||
|
||||
/// <summary>
|
||||
/// 1 if <see cref="currentAuthenticationContext"/> was initialized, 0 otherwise.
|
||||
/// </summary>
|
||||
@@ -80,27 +75,12 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => currentAuthenticationContext.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Populate <see cref="validAfter"/> with a given <paramref name="tokenNbf"/>.
|
||||
/// </summary>
|
||||
/// <param name="tokenNbf">The <see cref="DateTimeOffset"/> an issued token is not valid before.</param>
|
||||
public void SetTokenNbf(DateTimeOffset tokenNbf)
|
||||
{
|
||||
if (validAfter.HasValue)
|
||||
throw new InvalidOperationException("SetTokenNbf called multiple times!");
|
||||
|
||||
validAfter = tokenNbf;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IAuthenticationContext> CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken)
|
||||
public async ValueTask<IAuthenticationContext> CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset notBefore, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.Exchange(ref initialized, 1) != 0)
|
||||
throw new InvalidOperationException("Authentication context has already been loaded");
|
||||
|
||||
if (!validAfter.HasValue)
|
||||
throw new InvalidOperationException("SetTokenNbf has not been called!");
|
||||
|
||||
var user = await databaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
@@ -122,7 +102,7 @@ namespace Tgstation.Server.Host.Security
|
||||
systemIdentity = identityCache.LoadCachedIdentity(user);
|
||||
else
|
||||
{
|
||||
if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter.Value)
|
||||
if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > notBefore)
|
||||
{
|
||||
logger.LogDebug("Rejecting token for user {userId} created before last password update: {lastPasswordUpdate}", userId, user.LastPasswordUpdate.Value);
|
||||
return currentAuthenticationContext;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
@@ -13,8 +14,9 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
/// <param name="userId">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="Models.User"/>.</param>
|
||||
/// <param name="instanceId">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="Models.Instance"/> for the operation.</param>
|
||||
/// <param name="notBefore">The <see cref="DateTimeOffset"/> the login must not be from before.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the created <see cref="IAuthenticationContext"/>.</returns>
|
||||
ValueTask<IAuthenticationContext> CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken);
|
||||
ValueTask<IAuthenticationContext> CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset notBefore, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user