Merge authentication into Questions API
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
using MongoDB.Driver;
|
||||
using QuestionsApi.Models;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace QuestionsApi.Services;
|
||||
|
||||
public sealed class SteamAuthTransactionStore(MongoDatabaseProvider mongo)
|
||||
{
|
||||
private readonly IMongoCollection<SteamAuthTransaction> _transactions =
|
||||
mongo.Database.GetCollection<SteamAuthTransaction>("SteamAuthTransactions");
|
||||
|
||||
public Task CreateAsync(SteamAuthTransaction transaction) => _transactions.InsertOneAsync(transaction);
|
||||
public async Task<SteamAuthTransaction?> GetAsync(string id) =>
|
||||
await _transactions.Find(transaction => transaction.Id == id).FirstOrDefaultAsync();
|
||||
|
||||
public async Task<bool> MarkVerifiedAsync(string id, string steamId, string status, string? userId)
|
||||
{
|
||||
var filter = Builders<SteamAuthTransaction>.Filter.Eq(value => value.Id, id) &
|
||||
Builders<SteamAuthTransaction>.Filter.Eq(value => value.Status, "pending") &
|
||||
Builders<SteamAuthTransaction>.Filter.Gt(value => value.ExpiresUtc, DateTime.UtcNow);
|
||||
var update = Builders<SteamAuthTransaction>.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<bool> MarkCompleteAsync(string id, string steamId, string userId)
|
||||
{
|
||||
var filter = Builders<SteamAuthTransaction>.Filter.Eq(value => value.Id, id) &
|
||||
Builders<SteamAuthTransaction>.Filter.Eq(value => value.Status, "requires_account") &
|
||||
Builders<SteamAuthTransaction>.Filter.Eq(value => value.SteamId, steamId) &
|
||||
Builders<SteamAuthTransaction>.Filter.Gt(value => value.ExpiresUtc, DateTime.UtcNow);
|
||||
var update = Builders<SteamAuthTransaction>.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<SteamAuthTransaction> Collection => _transactions;
|
||||
}
|
||||
|
||||
public sealed partial class SteamOpenIdService(IHttpClientFactory httpClients)
|
||||
{
|
||||
public const string Endpoint = "https://steamcommunity.com/openid/login";
|
||||
|
||||
[GeneratedRegex("^https?://steamcommunity\\.com/openid/id/(?<id>[0-9]{17})$",
|
||||
RegexOptions.CultureInvariant)]
|
||||
private static partial Regex ClaimedIdPattern();
|
||||
|
||||
public static string BuildAuthenticationUrl(string realm, string returnUrl)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
["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<string?> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user