Files
SVSimServer/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs
2026-05-25 14:48:51 -04:00

124 lines
5.6 KiB
C#

using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Viewer;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Extensions;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
using SVSim.EmulatedEntrypoint.Services;
namespace SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication;
public class SteamSessionAuthenticationHandler : AuthenticationHandler<SteamAuthenticationHandlerOptions>
{
// Must mirror the controller-side JSON options — the translation middleware rewrites the
// request body in snake_case, and we have to read it back the same way or every property
// binds to null and we NRE downstream against the Steam ticket.
private static readonly JsonSerializerOptions RequestJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
private readonly SteamSessionService _sessionService;
private readonly IViewerRepository _viewerRepository;
public SteamSessionAuthenticationHandler(IOptionsMonitor<SteamAuthenticationHandlerOptions> options, ILoggerFactory logger, UrlEncoder encoder, SteamSessionService sessionService, IViewerRepository viewerRepository) : base(options, logger, encoder)
{
_sessionService = sessionService;
_viewerRepository = viewerRepository;
}
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string path = Request.Path;
byte[] requestBytes;
try
{
using (var requestBytesStream = new MemoryStream())
{
// Reset request stream
Request.Body.Seek(0, SeekOrigin.Begin);
await Request.Body.CopyToAsync(requestBytesStream);
requestBytes = requestBytesStream.ToArray();
// Reset request stream
Request.Body.Seek(0, SeekOrigin.Begin);
}
}
catch (Exception e)
{
Logger.LogWarning(e, "Auth: failed to read request body on {Path}.", path);
return AuthenticateResult.Fail("Failed to read request body.");
}
// Convert bytes to json
string requestString = Encoding.UTF8.GetString(requestBytes);
BaseRequest? requestJson;
try
{
requestJson = JsonSerializer.Deserialize<BaseRequest>(requestString, RequestJsonOptions);
}
catch (JsonException ex)
{
Logger.LogWarning(ex,
"Auth: failed to JSON-parse request body on {Path} (bodyLen={BodyLen}). " +
"Translation middleware should have rewritten this to JSON — if it didn't, the request bypassed translation (non-Unity UA?).",
path, requestBytes.Length);
return AuthenticateResult.Fail("Invalid request body.");
}
if (requestJson is null || string.IsNullOrEmpty(requestJson.SteamSessionTicket))
{
Logger.LogWarning(
"Auth: request body missing steam_session_ticket on {Path} (bodyLen={BodyLen}, hasViewerId={HasViewerId}, steamId={SteamId}).",
path, requestBytes.Length,
!string.IsNullOrEmpty(requestJson?.ViewerId), requestJson?.SteamId ?? 0);
return AuthenticateResult.Fail("Invalid request body.");
}
// Check steam session validity
bool sessionIsValid = _sessionService.IsTicketValidForUser(requestJson.SteamSessionTicket, requestJson.SteamId);
if (!sessionIsValid)
{
Logger.LogWarning(
"Auth: Steam ticket rejected on {Path} for steamId={SteamId} (ticketLen={TicketLen}). " +
"See SteamSessionService logs above for the underlying Steam reason (BeginAuthSession failure, duplicate, etc.).",
path, requestJson.SteamId, requestJson.SteamSessionTicket.Length);
return AuthenticateResult.Fail("Invalid ticket.");
}
Viewer? viewer =
await _viewerRepository.GetViewerBySocialConnection(SocialAccountType.Steam, requestJson.SteamId);
if (viewer is null)
{
// Most common dev-loop cause: DB was re-bootstrapped and this Steam account hasn't
// been re-linked yet. Log loudly with the steam_id so it's obvious what to add back.
Logger.LogWarning(
"Auth: no viewer linked to steamId={SteamId} on {Path}. " +
"Likely you re-bootstrapped the DB without re-linking this Steam account.",
requestJson.SteamId, path);
return AuthenticateResult.Fail("User not found.");
}
// Add viewer to context
Context.SetViewer(viewer);
// Build identity
ClaimsIdentity identity = new ClaimsIdentity(SteamAuthenticationConstants.SchemeName);
identity.AddClaim(new Claim(ClaimTypes.Name, viewer.DisplayName));
identity.AddClaim(new Claim(ShadowverseClaimTypes.ShortUdidClaim, viewer.ShortUdid.ToString()));
identity.AddClaim(new Claim(ShadowverseClaimTypes.ViewerIdClaim, viewer.Id.ToString()));
identity.AddClaim(new Claim(SteamAuthenticationConstants.SteamIdClaim, requestJson.SteamId.ToString()));
// Build and return final ticket
AuthenticationTicket ticket =
new AuthenticationTicket(new ClaimsPrincipal(identity), SteamAuthenticationConstants.SchemeName);
return AuthenticateResult.Success(ticket);
}
}