using System.Text.RegularExpressions; namespace AuthApi.Services; public class SteamOpenIdService { public const string Endpoint = "https://steamcommunity.com/openid/login"; private static readonly Regex ClaimedIdPattern = new( "^https?://steamcommunity\\.com/openid/id/(?[0-9]{17})$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private readonly IHttpClientFactory _httpClients; public SteamOpenIdService(IHttpClientFactory httpClients) => _httpClients = httpClients; public static string BuildAuthenticationUrl(string realm, string returnUrl) { var parameters = new Dictionary { ["openid.ns"] = "http://specs.openid.net/auth/2.0", ["openid.mode"] = "checkid_setup", ["openid.return_to"] = returnUrl, ["openid.realm"] = realm, ["openid.identity"] = "http://specs.openid.net/auth/2.0/identifier_select", ["openid.claimed_id"] = "http://specs.openid.net/auth/2.0/identifier_select" }; return Endpoint + "?" + string.Join("&", parameters.Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")); } public async Task VerifyCallbackAsync(IQueryCollection query, string expectedReturnUrl, CancellationToken cancellationToken) { if (!string.Equals(query["openid.mode"], "id_res", StringComparison.Ordinal) || !string.Equals(query["openid.op_endpoint"].ToString().TrimEnd('/'), Endpoint.TrimEnd('/'), StringComparison.OrdinalIgnoreCase) || !string.Equals(query["openid.return_to"], expectedReturnUrl, StringComparison.Ordinal)) return null; var claimedId = query["openid.claimed_id"].ToString(); if (!string.Equals(claimedId, query["openid.identity"], StringComparison.Ordinal)) return null; var match = ClaimedIdPattern.Match(claimedId); if (!match.Success) return null; var fields = query .Where(pair => pair.Key.StartsWith("openid.", StringComparison.Ordinal)) .ToDictionary(pair => pair.Key, pair => pair.Value.ToString()); fields["openid.mode"] = "check_authentication"; var client = _httpClients.CreateClient(nameof(SteamOpenIdService)); client.Timeout = TimeSpan.FromSeconds(15); using var response = await client.PostAsync(Endpoint, new FormUrlEncodedContent(fields), cancellationToken); if (!response.IsSuccessStatusCode) return null; var validation = await response.Content.ReadAsStringAsync(cancellationToken); return IsValidAuthenticationResponse(validation) ? match.Groups["id"].Value : null; } internal static bool IsValidAuthenticationResponse(string response) => response.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Any(line => string.Equals(line, "is_valid:true", StringComparison.Ordinal)); internal static string? ExtractSteamId(string claimedId) { var match = ClaimedIdPattern.Match(claimedId); return match.Success ? match.Groups["id"].Value : null; } }