Files
UnrealEngine/Engine/Source/Programs/Horde/HordeServer/Authentication/JwtAuthHandler.cs
2025-05-18 13:04:45 +08:00

85 lines
3.3 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using HordeServer.Server;
using HordeServer.Utilities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace HordeServer.Authentication
{
/// <summary>
/// JWT handler for server-issued bearer tokens. These tokens are signed using a randomly generated key per DB instance.
/// </summary>
class JwtAuthHandler : JwtBearerHandler, IAsyncDisposable
{
/// <summary>
/// Default name of the authentication scheme
/// </summary>
public const string AuthenticationScheme = "ServerJwt";
readonly AsyncCachedValue<IGlobals> _globals;
public JwtAuthHandler(ILoggerFactory logger, UrlEncoder encoder, IOptionsMonitor<ServerSettings> settings, GlobalsService globalsService, IOptionsMonitorCache<JwtBearerOptions> optionsCache)
: base(GetOptionsMonitor(settings.CurrentValue, optionsCache), logger, encoder)
{
_globals = new AsyncCachedValue<IGlobals>(async ctx => await globalsService.GetAsync(ctx), TimeSpan.FromSeconds(30.0));
}
public async ValueTask DisposeAsync()
{
await _globals.DisposeAsync();
}
private static IOptionsMonitor<JwtBearerOptions> GetOptionsMonitor(ServerSettings settings, IOptionsMonitorCache<JwtBearerOptions> optionsCache)
{
ConfigureNamedOptions<JwtBearerOptions> namedOptions = new (AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = !settings.OidcDebugMode;
});
OptionsFactory<JwtBearerOptions> optionsFactory = new (new[] { namedOptions }, Array.Empty<IPostConfigureOptions<JwtBearerOptions>>());
return new OptionsMonitor<JwtBearerOptions>(optionsFactory, Array.Empty<IOptionsChangeTokenSource<JwtBearerOptions>>(), optionsCache);
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Get the current state
IGlobals globals = await _globals.GetAsync();
Options.TokenValidationParameters.ValidateAudience = false; // Don't need to validate audience because we only issue tokens for our own consumption.
Options.TokenValidationParameters.RequireExpirationTime = false;
Options.TokenValidationParameters.ValidateLifetime = true;
Options.TokenValidationParameters.ValidIssuer = globals.JwtIssuer;
Options.TokenValidationParameters.ValidateIssuer = true;
Options.TokenValidationParameters.ValidateIssuerSigningKey = true;
Options.TokenValidationParameters.IssuerSigningKeys = new[] { globals.JwtSigningKey, globals.RsaSigningKey };
// Silent fail if this JWT is not issued by the server
string? token;
if (!JwtUtils.TryGetBearerToken(Request, "Bearer ", out token))
{
return AuthenticateResult.NoResult();
}
// Validate that it's from the correct issuer, and silent fail if not. Allows multiple handlers for bearer tokens.
JwtSecurityToken? jwtToken;
if (!JwtUtils.TryParseJwt(token, out jwtToken) || !String.Equals(jwtToken.Issuer, globals.JwtIssuer, StringComparison.Ordinal))
{
return AuthenticateResult.NoResult();
}
// Pass it to the base class
AuthenticateResult result = await base.HandleAuthenticateAsync();
return result;
}
}
}