diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index b97a21600f..922011a9ac 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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 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 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(); - 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; }, }; diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs index 6cd5f30305..4f02bb0b0d 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs @@ -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) diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 3b5035e69c..f39daaa989 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -46,11 +46,6 @@ namespace Tgstation.Server.Host.Security /// readonly AuthenticationContext currentAuthenticationContext; - /// - /// The the request's token must be valid after. - /// - DateTimeOffset? validAfter; - /// /// 1 if was initialized, 0 otherwise. /// @@ -80,27 +75,12 @@ namespace Tgstation.Server.Host.Security /// public void Dispose() => currentAuthenticationContext.Dispose(); - /// - /// Populate with a given . - /// - /// The an issued token is not valid before. - public void SetTokenNbf(DateTimeOffset tokenNbf) - { - if (validAfter.HasValue) - throw new InvalidOperationException("SetTokenNbf called multiple times!"); - - validAfter = tokenNbf; - } - /// - public async ValueTask CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken) + public async ValueTask 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; diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs index 0233700585..cb70a50b18 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs @@ -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 /// /// The of the . /// The of the for the operation. + /// The the login must not be from before. /// The for the operation. /// A resulting in the created . - ValueTask CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken); + ValueTask CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset notBefore, CancellationToken cancellationToken); } }