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 { // 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 options, ILoggerFactory logger, UrlEncoder encoder, SteamSessionService sessionService, IViewerRepository viewerRepository) : base(options, logger, encoder) { _sessionService = sessionService; _viewerRepository = viewerRepository; } protected async override Task 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(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); } }