using MongoDB.Driver; using QuestionsApi.Models; using System.Text.RegularExpressions; namespace QuestionsApi.Services; public sealed class SteamAuthTransactionStore(MongoDatabaseProvider mongo) { private readonly IMongoCollection _transactions = mongo.Database.GetCollection("SteamAuthTransactions"); public Task CreateAsync(SteamAuthTransaction transaction) => _transactions.InsertOneAsync(transaction); public async Task GetAsync(string id) => await _transactions.Find(transaction => transaction.Id == id).FirstOrDefaultAsync(); public async Task MarkVerifiedAsync(string id, string steamId, string status, string? userId) { var filter = Builders.Filter.Eq(value => value.Id, id) & Builders.Filter.Eq(value => value.Status, "pending") & Builders.Filter.Gt(value => value.ExpiresUtc, DateTime.UtcNow); var update = Builders.Update .Set(value => value.SteamId, steamId) .Set(value => value.Status, status) .Set(value => value.UserId, userId); return (await _transactions.UpdateOneAsync(filter, update)).ModifiedCount == 1; } public async Task MarkCompleteAsync(string id, string steamId, string userId) { var filter = Builders.Filter.Eq(value => value.Id, id) & Builders.Filter.Eq(value => value.Status, "requires_account") & Builders.Filter.Eq(value => value.SteamId, steamId) & Builders.Filter.Gt(value => value.ExpiresUtc, DateTime.UtcNow); var update = Builders.Update .Set(value => value.Status, "complete") .Set(value => value.UserId, userId); return (await _transactions.UpdateOneAsync(filter, update)).ModifiedCount == 1; } public Task DeleteForUserAsync(string userId) => _transactions.DeleteManyAsync(transaction => transaction.UserId == userId); internal IMongoCollection Collection => _transactions; } public sealed partial class SteamOpenIdService(IHttpClientFactory httpClients) { public const string Endpoint = "https://steamcommunity.com/openid/login"; [GeneratedRegex("^https?://steamcommunity\\.com/openid/id/(?[0-9]{17})$", RegexOptions.CultureInvariant)] private static partial Regex ClaimedIdPattern(); 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; } }