From 433230b49559a78fb789c6a2f2abd90b4e113fee Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 4 Nov 2025 01:28:51 +0800 Subject: [PATCH] :drunk: AIGC steam connection support (w.i.p) (skip ci) --- .../Auth/OpenId/ConnectionController.cs | 46 ++++--- .../Auth/OpenId/OidcController.cs | 1 + DysonNetwork.Pass/Auth/OpenId/OidcService.cs | 1 + .../Auth/OpenId/SteamOidcService.cs | 124 ++++++++++++++++++ 4 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 DysonNetwork.Pass/Auth/OpenId/SteamOidcService.cs diff --git a/DysonNetwork.Pass/Auth/OpenId/ConnectionController.cs b/DysonNetwork.Pass/Auth/OpenId/ConnectionController.cs index cb72516..93f7157 100644 --- a/DysonNetwork.Pass/Auth/OpenId/ConnectionController.cs +++ b/DysonNetwork.Pass/Auth/OpenId/ConnectionController.cs @@ -353,24 +353,36 @@ public class ConnectionController( private static async Task ExtractCallbackData(HttpRequest request) { var data = new OidcCallbackData(); - switch (request.Method) - { - case "GET": - data.Code = Uri.UnescapeDataString(request.Query["code"].FirstOrDefault() ?? ""); - data.IdToken = Uri.UnescapeDataString(request.Query["id_token"].FirstOrDefault() ?? ""); - data.State = Uri.UnescapeDataString(request.Query["state"].FirstOrDefault() ?? ""); - break; - case "POST" when request.HasFormContentType: - { - var form = await request.ReadFormAsync(); - data.Code = Uri.UnescapeDataString(form["code"].FirstOrDefault() ?? ""); - data.IdToken = Uri.UnescapeDataString(form["id_token"].FirstOrDefault() ?? ""); - data.State = Uri.UnescapeDataString(form["state"].FirstOrDefault() ?? ""); - if (form.ContainsKey("user")) - data.RawData = Uri.UnescapeDataString(form["user"].FirstOrDefault() ?? ""); - break; - } + // Extract data based on request method + if (request.Method == "GET") + { + // Extract from query string + data.Code = Uri.UnescapeDataString(request.Query["code"].FirstOrDefault() ?? ""); + data.IdToken = Uri.UnescapeDataString(request.Query["id_token"].FirstOrDefault() ?? ""); + data.State = Uri.UnescapeDataString(request.Query["state"].FirstOrDefault() ?? ""); + + // Populate all query parameters for providers that need them (like Steam OpenID) + foreach (var param in request.Query) + { + data.QueryParameters[param.Key] = Uri.UnescapeDataString(param.Value.FirstOrDefault() ?? ""); + } + } + else if (request.Method == "POST" && request.HasFormContentType) + { + var form = await request.ReadFormAsync(); + data.Code = Uri.UnescapeDataString(form["code"].FirstOrDefault() ?? ""); + data.IdToken = Uri.UnescapeDataString(form["id_token"].FirstOrDefault() ?? ""); + data.State = Uri.UnescapeDataString(form["state"].FirstOrDefault() ?? ""); + + if (form.ContainsKey("user")) + data.RawData = Uri.UnescapeDataString(form["user"].FirstOrDefault() ?? ""); + + // Populate all form parameters + foreach (var param in form) + { + data.QueryParameters[param.Key] = Uri.UnescapeDataString(param.Value.FirstOrDefault() ?? ""); + } } return data; diff --git a/DysonNetwork.Pass/Auth/OpenId/OidcController.cs b/DysonNetwork.Pass/Auth/OpenId/OidcController.cs index 6ec1843..9aae198 100644 --- a/DysonNetwork.Pass/Auth/OpenId/OidcController.cs +++ b/DysonNetwork.Pass/Auth/OpenId/OidcController.cs @@ -124,6 +124,7 @@ public class OidcController( "discord" => serviceProvider.GetRequiredService(), "github" => serviceProvider.GetRequiredService(), "spotify" => serviceProvider.GetRequiredService(), + "steam" => serviceProvider.GetRequiredService(), "afdian" => serviceProvider.GetRequiredService(), _ => throw new ArgumentException($"Unsupported provider: {provider}") }; diff --git a/DysonNetwork.Pass/Auth/OpenId/OidcService.cs b/DysonNetwork.Pass/Auth/OpenId/OidcService.cs index 63a1a1f..0ebd28c 100644 --- a/DysonNetwork.Pass/Auth/OpenId/OidcService.cs +++ b/DysonNetwork.Pass/Auth/OpenId/OidcService.cs @@ -358,4 +358,5 @@ public class OidcCallbackData public string IdToken { get; set; } = ""; public string? State { get; set; } public string? RawData { get; set; } + public Dictionary QueryParameters { get; set; } = new(); } diff --git a/DysonNetwork.Pass/Auth/OpenId/SteamOidcService.cs b/DysonNetwork.Pass/Auth/OpenId/SteamOidcService.cs new file mode 100644 index 0000000..b4851e1 --- /dev/null +++ b/DysonNetwork.Pass/Auth/OpenId/SteamOidcService.cs @@ -0,0 +1,124 @@ +using System.Text.Json; +using DysonNetwork.Shared.Cache; + +namespace DysonNetwork.Pass.Auth.OpenId; + +public class SteamOidcService( + IConfiguration configuration, + IHttpClientFactory httpClientFactory, + AppDatabase db, + AuthService auth, + ICacheService cache +) + : OidcService(configuration, httpClientFactory, db, auth, cache) +{ + public override string ProviderName => "steam"; + protected override string DiscoveryEndpoint => ""; // Steam uses OpenID 2.0, not OIDC discovery + protected override string ConfigSectionName => "Steam"; + + public override Task GetAuthorizationUrlAsync(string state, string nonce) + { + return Task.FromResult(GetAuthorizationUrl(state, nonce)); + } + + public override string GetAuthorizationUrl(string state, string nonce) + { + var config = GetProviderConfig(); + var returnUrl = config.RedirectUri; + + // Steam OpenID 2.0 authorization URL construction + var queryParams = new Dictionary + { + { "openid.ns", "http://specs.openid.net/auth/2.0" }, + { "openid.mode", "checkid_setup" }, + { "openid.return_to", returnUrl }, + { "openid.realm", new Uri(returnUrl).GetLeftPart(UriPartial.Authority) }, + { "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select" }, + { "openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select" }, + // Store state in the return URL as a query parameter since Steam doesn't support state directly + { "openid.state", state } + }; + + var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); + return $"https://steamcommunity.com/openid/login?{queryString}"; + } + + protected override Task GetDiscoveryDocumentAsync() + { + // Steam doesn't use standard OIDC discovery + return Task.FromResult(null); + } + + public override async Task ProcessCallbackAsync(OidcCallbackData callbackData) + { + // Parse Steam's OpenID 2.0 response + var queryParams = callbackData.QueryParameters; + + // Verify the OpenID response + if (queryParams.GetValueOrDefault("openid.mode") != "id_res") + { + throw new InvalidOperationException("Invalid OpenID response mode"); + } + + // Extract Steam ID from claimed_id + var claimedId = queryParams.GetValueOrDefault("openid.claimed_id"); + if (string.IsNullOrEmpty(claimedId)) + { + throw new InvalidOperationException("No claimed_id in OpenID response"); + } + + // Steam ID is the last part of the claimed_id URL + var steamId = claimedId.Split('/')[^1]; + if (!ulong.TryParse(steamId, out _)) + { + throw new InvalidOperationException("Invalid Steam ID format"); + } + + // Fetch user information from Steam API + var userInfo = await GetUserInfoAsync(steamId); + + return userInfo; + } + + private async Task GetUserInfoAsync(string steamId) + { + var config = GetProviderConfig(); + var apiKey = Configuration[$"Oidc:Steam:ApiKey"] ?? throw new InvalidOperationException("Steam API key not configured"); + + var client = HttpClientFactory.CreateClient(); + var url = $"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key={apiKey}&steamids={steamId}"; + + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var steamResponse = JsonDocument.Parse(json).RootElement; + + var players = steamResponse.GetProperty("response").GetProperty("players"); + if (players.GetArrayLength() == 0) + { + throw new InvalidOperationException("Steam user not found"); + } + + var player = players[0]; + var steamIdStr = player.GetProperty("steamid").GetString() ?? ""; + var personaName = player.GetProperty("personaname").GetString() ?? ""; + var avatarUrl = player.TryGetProperty("avatarfull", out var avatarElement) + ? avatarElement.GetString() + : player.TryGetProperty("avatarmedium", out var mediumAvatarElement) + ? mediumAvatarElement.GetString() + : null; + + return new OidcUserInfo + { + UserId = steamIdStr, + DisplayName = personaName, + PreferredUsername = personaName, + ProfilePictureUrl = avatarUrl, + Provider = ProviderName, + // Steam OpenID doesn't provide email + Email = null, + EmailVerified = false + }; + } +}