112 lines
5.8 KiB
C#
112 lines
5.8 KiB
C#
using System.Security.Cryptography;
|
|
using GameAuthApi.Models;
|
|
using MongoDB.Driver;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace GameAuthApi.Services;
|
|
|
|
public class SteamAuthService
|
|
{
|
|
private const string Provider = "steam";
|
|
private readonly IMongoCollection<SteamAuthTransaction> _transactions;
|
|
private readonly IMongoCollection<ExternalIdentityLink> _links;
|
|
private readonly IConfiguration _config;
|
|
private readonly HttpClient _http;
|
|
|
|
public SteamAuthService(IConfiguration config, IHttpClientFactory factory)
|
|
{
|
|
_config = config;
|
|
_http = factory.CreateClient();
|
|
var client = new MongoClient(config["MongoDB:ConnectionString"]);
|
|
var database = client.GetDatabase(config["MongoDB:DatabaseName"]);
|
|
_transactions = database.GetCollection<SteamAuthTransaction>("SteamAuthTransactions");
|
|
_links = database.GetCollection<ExternalIdentityLink>("ExternalIdentityLinks");
|
|
}
|
|
|
|
public virtual async Task<(SteamAuthTransaction Transaction, string AuthUrl)> StartAsync()
|
|
{
|
|
var id = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant();
|
|
var transaction = new SteamAuthTransaction { Id = id, CreatedUtc = DateTime.UtcNow, ExpiresUtc = DateTime.UtcNow.AddMinutes(10) };
|
|
await _transactions.InsertOneAsync(transaction);
|
|
var baseUrl = (_config["Auth:PublicBaseUrl"] ?? "https://pauth.ranaze.com").TrimEnd('/');
|
|
var returnTo = $"{baseUrl}/api/Auth/steam/callback?transactionId={Uri.EscapeDataString(id)}";
|
|
var realm = baseUrl + "/";
|
|
var query = new Dictionary<string, string>
|
|
{
|
|
["openid.ns"] = "http://specs.openid.net/auth/2.0",
|
|
["openid.mode"] = "checkid_setup",
|
|
["openid.return_to"] = returnTo,
|
|
["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"
|
|
};
|
|
var authUrl = "https://steamcommunity.com/openid/login?" + string.Join("&", query.Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value)}"));
|
|
return (transaction, authUrl);
|
|
}
|
|
|
|
public virtual async Task<SteamAuthTransaction> GetAsync(string id) =>
|
|
await _transactions.Find(x => x.Id == id && x.ExpiresUtc > DateTime.UtcNow).FirstOrDefaultAsync();
|
|
|
|
public virtual async Task<bool> VerifyCallbackAsync(string transactionId, IQueryCollection query)
|
|
{
|
|
var transaction = await GetAsync(transactionId);
|
|
if (transaction == null) return false;
|
|
var payload = query.Where(x => x.Key.StartsWith("openid.")).ToDictionary(x => x.Key, x => x.Value.ToString());
|
|
payload["openid.mode"] = "check_authentication";
|
|
using var response = await _http.PostAsync("https://steamcommunity.com/openid/login", new FormUrlEncodedContent(payload));
|
|
var result = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode || !result.Contains("is_valid:true", StringComparison.Ordinal)) return false;
|
|
var claimed = query["openid.claimed_id"].ToString();
|
|
const string prefix = "https://steamcommunity.com/openid/id/";
|
|
if (!claimed.StartsWith(prefix, StringComparison.Ordinal) || !claimed[prefix.Length..].All(char.IsDigit)) return false;
|
|
var steamId = claimed[prefix.Length..];
|
|
var link = await _links.Find(x => x.Provider == Provider && x.Subject == steamId).FirstOrDefaultAsync();
|
|
await _transactions.UpdateOneAsync(x => x.Id == transactionId,
|
|
Builders<SteamAuthTransaction>.Update
|
|
.Set(x => x.SteamId, steamId)
|
|
.Set(x => x.UserId, link?.UserId)
|
|
.Set(x => x.Status, link == null ? "requires_account" : "authenticated"));
|
|
return true;
|
|
}
|
|
|
|
public virtual async Task LinkAsync(string transactionId, string userId)
|
|
{
|
|
var transaction = await GetAsync(transactionId) ?? throw new InvalidOperationException("Steam transaction expired.");
|
|
if (transaction.Status != "requires_account" || string.IsNullOrWhiteSpace(transaction.SteamId))
|
|
throw new InvalidOperationException("Steam transaction is not ready.");
|
|
await _links.InsertOneAsync(new ExternalIdentityLink
|
|
{
|
|
Provider = Provider, Subject = transaction.SteamId, UserId = userId, CreatedUtc = DateTime.UtcNow
|
|
});
|
|
await _transactions.UpdateOneAsync(x => x.Id == transactionId,
|
|
Builders<SteamAuthTransaction>.Update.Set(x => x.UserId, userId).Set(x => x.Status, "authenticated"));
|
|
}
|
|
|
|
public virtual async Task CompleteAsync(string transactionId, User user, string accessToken, string refreshToken)
|
|
{
|
|
await _transactions.UpdateOneAsync(x => x.Id == transactionId,
|
|
Builders<SteamAuthTransaction>.Update
|
|
.Set(x => x.Status, "complete")
|
|
.Set(x => x.AccessToken, accessToken)
|
|
.Set(x => x.RefreshToken, refreshToken)
|
|
.Set(x => x.Username, user.Username)
|
|
.Set(x => x.Role, user.Role));
|
|
}
|
|
|
|
public async Task EnsureIndexesAsync()
|
|
{
|
|
await _transactions.Indexes.CreateOneAsync(new CreateIndexModel<SteamAuthTransaction>(
|
|
Builders<SteamAuthTransaction>.IndexKeys.Ascending(x => x.ExpiresUtc),
|
|
new CreateIndexOptions { ExpireAfter = TimeSpan.Zero, Name = "steam_transaction_ttl" }));
|
|
await _links.Indexes.CreateOneAsync(new CreateIndexModel<ExternalIdentityLink>(
|
|
Builders<ExternalIdentityLink>.IndexKeys.Ascending(x => x.Provider).Ascending(x => x.Subject),
|
|
new CreateIndexOptions { Unique = true, Name = "external_identity_unique" }));
|
|
}
|
|
}
|