From 50d8cd34b8939a35590251d7ee4f671e9b3a98f1 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Wed, 14 Aug 2024 01:36:45 -0400 Subject: [PATCH 01/11] Add twitch login support --- .../Content/frontend/main/index.js | 1 + CelesteNet.Server.FrontendModule/Frontend.cs | 2 +- .../FrontendSettings.cs | 7 + .../RCEPs/RCEPPublic.cs | 169 ++++++++++++++++++ 4 files changed, 178 insertions(+), 1 deletion(-) diff --git a/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js b/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js index 49dd21c5..fdb904ea 100644 --- a/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js +++ b/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js @@ -97,6 +97,7 @@ function renderUser() { Create a CelesteNet account to show your profile picture in-game and to let the server remember your last channel and command settings.

Link your Discord account
+ Link your Twitch account
Linking your account is fully optional and requires telling your browser to store a "cookie." This cookie is only used to keep you logged in. diff --git a/CelesteNet.Server.FrontendModule/Frontend.cs b/CelesteNet.Server.FrontendModule/Frontend.cs index f016a131..264389b6 100644 --- a/CelesteNet.Server.FrontendModule/Frontend.cs +++ b/CelesteNet.Server.FrontendModule/Frontend.cs @@ -506,7 +506,7 @@ public NameValueCollection ParseQueryString(string url) { NameValueCollection nvc = new(); int indexOfSplit = url.IndexOf('?'); - if (indexOfSplit == -1) + if (indexOfSplit == -1) return nvc; url = url.Substring(indexOfSplit + 1); diff --git a/CelesteNet.Server.FrontendModule/FrontendSettings.cs b/CelesteNet.Server.FrontendModule/FrontendSettings.cs index a9ba2c55..4c7f3bd4 100644 --- a/CelesteNet.Server.FrontendModule/FrontendSettings.cs +++ b/CelesteNet.Server.FrontendModule/FrontendSettings.cs @@ -30,6 +30,13 @@ public class FrontendSettings : CelesteNetServerModuleSettings { public string DiscordOAuthClientID { get; set; } = ""; public string DiscordOAuthClientSecret { get; set; } = ""; + [YamlIgnore] + public string TwitchOAuthURL => $"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id={TwitchOAuthClientID}&redirect_uri={Uri.EscapeDataString(TwitchOAuthRedirectURL)}&response_type=code"; + [YamlIgnore] + public string TwitchOAuthRedirectURL => $"{CanonicalAPIRoot}/twitchauth"; + public string TwitchOAuthClientID { get; set; } = ""; + public string TwitchOAuthClientSecret { get; set; } = ""; + public HashSet ExecOnlySettings { get; set; } = new(); } diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index 7fc8d1d7..9af97fce 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -17,9 +17,177 @@ namespace Celeste.Mod.CelesteNet.Server.Control { public static partial class RCEndpoints { + + [RCEndpoint(false, "/twitchauth", "", "", "Twitch OAuth2", "User auth using Twitch.")] + public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) + { + NameValueCollection args = f.ParseQueryString(c.Request.RawUrl); + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"{c.Request.RawUrl}"); + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); + if (args.Count == 0) + { + // c.Response.Redirect(f.Settings.OAuthURL); + c.Response.StatusCode = (int)HttpStatusCode.Redirect; + c.Response.Headers.Set("Location", f.Settings.TwitchOAuthURL); + f.RespondJSON(c, new + { + Info = $"Redirecting to {f.Settings.TwitchOAuthURL}" + }); + + return; + } + + if (args["error"] == "access_denied") + { + c.Response.StatusCode = (int)HttpStatusCode.Redirect; + c.Response.Headers.Set("Location", $"http://{c.Request.UserHostName}/"); + f.UnsetKeyCookie(c); + f.UnsetDiscordAuthCookie(c); + f.RespondJSON(c, new + { + Info = "Denied - redirecting to /" + }); + return; + } + + string? code = args["code"]; + if (code.IsNullOrEmpty()) + { + c.Response.StatusCode = (int)HttpStatusCode.BadRequest; + f.RespondJSON(c, new + { + Error = "No code specified." + }); + return; + } + + dynamic? tokenData; + dynamic? userData; + + using (HttpClient client = new()) + { +#pragma warning disable CS8714 // new FormUrlEncodedContent expects nullable. + using (Stream s = client.PostAsync("https://id.twitch.tv/oauth2/token", new FormUrlEncodedContent(new Dictionary() { +#pragma warning restore CS8714 + { "client_id", f.Settings.TwitchOAuthClientID }, + { "client_secret", f.Settings.TwitchOAuthClientSecret }, + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", f.Settings.TwitchOAuthRedirectURL }, + })).Await().Content.ReadAsStreamAsync().Await()) + using (StreamReader sr = new(s)) + using (JsonTextReader jtr = new(sr)) + tokenData = f.Serializer.Deserialize(jtr); + + if (tokenData?.access_token?.ToString() is not string token || + tokenData?.token_type?.ToString() is not string tokenType || + token.IsNullOrEmpty() || + tokenType.IsNullOrEmpty()) + { + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Failed to obtain token: {tokenData}"); + c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + f.RespondJSON(c, new + { + Error = "Couldn't obtain access token from Twitch." + }); + return; + } + + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Bwaa: {tokenData}"); + + if (tokenType == "bearer") + tokenType = "Bearer"; + + using (Stream s = client.SendAsync(new HttpRequestMessage + { + RequestUri = new("https://api.twitch.tv/helix/users"), + Method = HttpMethod.Get, + Headers = { + { "Authorization", $"{tokenType} {token}" }, + { "Client-Id", f.Settings.TwitchOAuthClientID } + } + }).Await().Content.ReadAsStreamAsync().Await()) + using (StreamReader sr = new(s)) + using (JsonTextReader jtr = new(sr)) + userData = f.Serializer.Deserialize(jtr); + } + + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"{userData?.data[0].id}"); + + if (!($"{userData?.data[0].id}" is string uid) || + uid.IsNullOrEmpty()) + { + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Failed to obtain ID: {userData?.data[0].id}"); + c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + f.RespondJSON(c, new + { + Error = "Couldn't obtain user ID from Twitch." + }); + return; + } + + string key = f.Server.UserData.Create(uid, false); + BasicUserInfo info = f.Server.UserData.Load(uid); + + if ($"{userData?.data[0].display_name}" is string global_name && !global_name.IsNullOrEmpty()) + { + info.Name = global_name; + } + if (info.Name.Length > 32) + { + info.Name = info.Name.Substring(0, 32); + } + info.Discrim = "TWITCH"; + f.Server.UserData.Save(uid, info); + + Image avatarOrig; + using (HttpClient client = new()) + { + try + { + using Stream s = client.GetAsync( + $"{userData?.data[0].profile_image_url}" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } + catch + { + using Stream s = client.GetAsync( + $"https://static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-300x300.png" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } + } + + using (avatarOrig) + using (Image avatarScale = avatarOrig.Clone(x => x.Resize(64, 64, sampler: KnownResamplers.Lanczos3))) + using (Image avatarFinal = avatarScale.Clone(x => x.ApplyRoundedCorners().ApplyTagOverlays(f, info))) + { + + using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.orig.png")) + avatarScale.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); + + using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.png")) + avatarFinal.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); + } + + c.Response.StatusCode = (int)HttpStatusCode.Redirect; + c.Response.Headers.Set("Location", $"http://{c.Request.UserHostName}/"); + f.SetKeyCookie(c, key); + f.SetDiscordAuthCookie(c, code); + f.RespondJSON(c, new + { + Info = "Success - redirecting to /" + }); + } + + + [RCEndpoint(false, "/discordauth", "", "", "Discord OAuth2", "User auth using Discord.")] public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { NameValueCollection args = f.ParseQueryString(c.Request.RawUrl); + Logger.Log(LogLevel.CRI, "frontend-discordauth", $"{c.Request.RawUrl}"); + Logger.Log(LogLevel.CRI, "frontend-discordauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); if (args.Count == 0) { // c.Response.Redirect(f.Settings.OAuthURL); @@ -69,6 +237,7 @@ public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { using (JsonTextReader jtr = new(sr)) tokenData = f.Serializer.Deserialize(jtr); + Logger.Log(LogLevel.CRI, "frontend-discordauth", $"Bwaa: {tokenData}"); if (tokenData?.access_token?.ToString() is not string token || tokenData?.token_type?.ToString() is not string tokenType || token.IsNullOrEmpty() || From ebdc5d6afc5b6a5d9d38834db20a0a5a27a9808c Mon Sep 17 00:00:00 2001 From: Stigstille Date: Wed, 14 Aug 2024 01:41:47 -0400 Subject: [PATCH 02/11] Remove debug logger.log functions --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index 9af97fce..5f5fff35 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -93,8 +93,6 @@ public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) return; } - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Bwaa: {tokenData}"); - if (tokenType == "bearer") tokenType = "Bearer"; @@ -112,8 +110,6 @@ public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) userData = f.Serializer.Deserialize(jtr); } - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"{userData?.data[0].id}"); - if (!($"{userData?.data[0].id}" is string uid) || uid.IsNullOrEmpty()) { @@ -237,7 +233,6 @@ public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { using (JsonTextReader jtr = new(sr)) tokenData = f.Serializer.Deserialize(jtr); - Logger.Log(LogLevel.CRI, "frontend-discordauth", $"Bwaa: {tokenData}"); if (tokenData?.access_token?.ToString() is not string token || tokenData?.token_type?.ToString() is not string tokenType || token.IsNullOrEmpty() || From 80ed34de75ee83385ef9af061a7f564ae3280a01 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sat, 17 Aug 2024 22:24:31 -0400 Subject: [PATCH 03/11] Update LogLevel at start of OAuth --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index 5f5fff35..7c61dbf9 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -22,8 +22,8 @@ public static partial class RCEndpoints { public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) { NameValueCollection args = f.ParseQueryString(c.Request.RawUrl); - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"{c.Request.RawUrl}"); - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); + Logger.Log(LogLevel.DBG, "frontend-twitchauth", $"{c.Request.RawUrl}"); + Logger.Log(LogLevel.DBG, "frontend-twitchauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); if (args.Count == 0) { // c.Response.Redirect(f.Settings.OAuthURL); @@ -182,8 +182,8 @@ public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) [RCEndpoint(false, "/discordauth", "", "", "Discord OAuth2", "User auth using Discord.")] public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { NameValueCollection args = f.ParseQueryString(c.Request.RawUrl); - Logger.Log(LogLevel.CRI, "frontend-discordauth", $"{c.Request.RawUrl}"); - Logger.Log(LogLevel.CRI, "frontend-discordauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); + Logger.Log(LogLevel.DBG, "frontend-discordauth", $"{c.Request.RawUrl}"); + Logger.Log(LogLevel.DBG, "frontend-discordauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); if (args.Count == 0) { // c.Response.Redirect(f.Settings.OAuthURL); From ae4320a05c60e3a22a8366bf1d152380d2947fe6 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sat, 17 Aug 2024 22:39:06 -0400 Subject: [PATCH 04/11] Add more error catching --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index 7c61dbf9..9a213d41 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -110,6 +110,17 @@ public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) userData = f.Serializer.Deserialize(jtr); } + if (!($"{userData?.data[0].error}" is string error) || !error.IsNullOrEmpty()) + { + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Status: {userData?.data[0].status}. Error: {userData?.data[0].error}"); + c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + f.RespondJSON(c, new + { + Error = $"Twitch returned an error with status code {userData?.data[0].status}. This means that it came back as {userData?.data[0].error}" + }); + return; + } + if (!($"{userData?.data[0].id}" is string uid) || uid.IsNullOrEmpty()) { From 304c7c4ff8072bbdab4529a53dded1bfbc30d9a7 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sat, 17 Aug 2024 22:46:33 -0400 Subject: [PATCH 05/11] Remove data[0] from data catching When there is an error, there is no `data` array --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index 9a213d41..b4758f55 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -110,13 +110,13 @@ public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) userData = f.Serializer.Deserialize(jtr); } - if (!($"{userData?.data[0].error}" is string error) || !error.IsNullOrEmpty()) + if (!($"{userData?.error}" is string error) || !error.IsNullOrEmpty()) { - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Status: {userData?.data[0].status}. Error: {userData?.data[0].error}"); + Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Status: {userData?.status}. Error: {userData?.error}"); c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; f.RespondJSON(c, new { - Error = $"Twitch returned an error with status code {userData?.data[0].status}. This means that it came back as {userData?.data[0].error}" + Error = $"Twitch returned an error with status code {userData?.status}. This means that it came back as {userData?.error}" }); return; } From 6d5cce41bdc9d804bb34dd932785993c9fc5e545 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sun, 18 Aug 2024 13:39:47 -0400 Subject: [PATCH 06/11] Change Discrim to "DISCORD" Discord changed EVERYONEs discrim to `#0`, so this is useless otherwise --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index b4758f55..d649233b 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -290,7 +290,8 @@ public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { if (info.Name.Length > 32) { info.Name = info.Name.Substring(0, 32); } - info.Discrim = userData.discriminator.ToString(); + // Since ALL discord accounts do not have discriminators, we can seperate DISCORD and TWITCH accounts with the discrim value + info.Discrim = "DISCORD"; f.Server.UserData.Save(uid, info); Image avatarOrig; From 59f60cb3a360ebf38a76af928d5f76ff04f09b26 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sun, 3 Nov 2024 20:14:07 -0500 Subject: [PATCH 07/11] Set up single endpoint for multiple OAuths, hopefully making it easier to add new OAuth methods in the future --- .../Content/frontend/main/index.js | 4 +- .../FrontendSettings.cs | 8 +- .../RCEPs/RCEPPublic.cs | 454 ++++++++---------- 3 files changed, 206 insertions(+), 260 deletions(-) diff --git a/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js b/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js index fdb904ea..c70d827f 100644 --- a/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js +++ b/CelesteNet.Server.FrontendModule/Content/frontend/main/index.js @@ -96,8 +96,8 @@ function renderUser() {

Create a CelesteNet account to show your profile picture in-game and to let the server remember your last channel and command settings.

- Link your Discord account
- Link your Twitch account
+ Link your Discord account
+ Link your Twitch account
Linking your account is fully optional and requires telling your browser to store a "cookie." This cookie is only used to keep you logged in. diff --git a/CelesteNet.Server.FrontendModule/FrontendSettings.cs b/CelesteNet.Server.FrontendModule/FrontendSettings.cs index 4c7f3bd4..4898cfa0 100644 --- a/CelesteNet.Server.FrontendModule/FrontendSettings.cs +++ b/CelesteNet.Server.FrontendModule/FrontendSettings.cs @@ -24,16 +24,16 @@ public class FrontendSettings : CelesteNetServerModuleSettings { // TODO: Separate Discord auth module! [YamlIgnore] - public string DiscordOAuthURL => $"https://discord.com/oauth2/authorize?client_id={DiscordOAuthClientID}&redirect_uri={Uri.EscapeDataString(DiscordOAuthRedirectURL)}&response_type=code&scope=identify"; + public string DiscordOAuthURL => $"https://discord.com/oauth2/authorize?client_id={DiscordOAuthClientID}&redirect_uri={Uri.EscapeDataString(DiscordOAuthRedirectURL)}&response_type=code&scope=identify&state=discord"; [YamlIgnore] - public string DiscordOAuthRedirectURL => $"{CanonicalAPIRoot}/discordauth"; + public string DiscordOAuthRedirectURL => $"{CanonicalAPIRoot}/standardauth"; public string DiscordOAuthClientID { get; set; } = ""; public string DiscordOAuthClientSecret { get; set; } = ""; [YamlIgnore] - public string TwitchOAuthURL => $"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id={TwitchOAuthClientID}&redirect_uri={Uri.EscapeDataString(TwitchOAuthRedirectURL)}&response_type=code"; + public string TwitchOAuthURL => $"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id={TwitchOAuthClientID}&redirect_uri={Uri.EscapeDataString(TwitchOAuthRedirectURL)}&response_type=code&state=twitch"; [YamlIgnore] - public string TwitchOAuthRedirectURL => $"{CanonicalAPIRoot}/twitchauth"; + public string TwitchOAuthRedirectURL => $"{CanonicalAPIRoot}/standardauth"; public string TwitchOAuthClientID { get; set; } = ""; public string TwitchOAuthClientSecret { get; set; } = ""; diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index d649233b..d91d185b 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; @@ -17,191 +18,53 @@ namespace Celeste.Mod.CelesteNet.Server.Control { public static partial class RCEndpoints { - - [RCEndpoint(false, "/twitchauth", "", "", "Twitch OAuth2", "User auth using Twitch.")] - public static void TwitchOAuth(Frontend f, HttpRequestEventArgs c) - { + [RCEndpoint(false, "/standardauth", "", "", "OAuth2", "User auth for all platforms.")] + public static void StandardOAuth(Frontend f, HttpRequestEventArgs c) { + string[] providers = { "discord", "twitch" }; NameValueCollection args = f.ParseQueryString(c.Request.RawUrl); - Logger.Log(LogLevel.DBG, "frontend-twitchauth", $"{c.Request.RawUrl}"); - Logger.Log(LogLevel.DBG, "frontend-twitchauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); - if (args.Count == 0) - { - // c.Response.Redirect(f.Settings.OAuthURL); - c.Response.StatusCode = (int)HttpStatusCode.Redirect; - c.Response.Headers.Set("Location", f.Settings.TwitchOAuthURL); - f.RespondJSON(c, new - { - Info = $"Redirecting to {f.Settings.TwitchOAuthURL}" - }); - - return; - } - - if (args["error"] == "access_denied") - { - c.Response.StatusCode = (int)HttpStatusCode.Redirect; - c.Response.Headers.Set("Location", $"http://{c.Request.UserHostName}/"); - f.UnsetKeyCookie(c); - f.UnsetDiscordAuthCookie(c); - f.RespondJSON(c, new - { - Info = "Denied - redirecting to /" - }); - return; - } + Logger.Log(LogLevel.DBG, "frontend-standardauth", $"{c.Request.RawUrl}"); + Logger.Log(LogLevel.DBG, "frontend-standardauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); - string? code = args["code"]; - if (code.IsNullOrEmpty()) - { - c.Response.StatusCode = (int)HttpStatusCode.BadRequest; - f.RespondJSON(c, new - { - Error = "No code specified." + if (args.Count == 0) { + c.Response.StatusCode = (int) HttpStatusCode.Unauthorized; + f.RespondJSON(c, new { + Error = "Unauthorized - no OAuth provider stated." }); return; } - - dynamic? tokenData; - dynamic? userData; - - using (HttpClient client = new()) - { -#pragma warning disable CS8714 // new FormUrlEncodedContent expects nullable. - using (Stream s = client.PostAsync("https://id.twitch.tv/oauth2/token", new FormUrlEncodedContent(new Dictionary() { -#pragma warning restore CS8714 - { "client_id", f.Settings.TwitchOAuthClientID }, - { "client_secret", f.Settings.TwitchOAuthClientSecret }, - { "grant_type", "authorization_code" }, - { "code", code }, - { "redirect_uri", f.Settings.TwitchOAuthRedirectURL }, - })).Await().Content.ReadAsStreamAsync().Await()) - using (StreamReader sr = new(s)) - using (JsonTextReader jtr = new(sr)) - tokenData = f.Serializer.Deserialize(jtr); - - if (tokenData?.access_token?.ToString() is not string token || - tokenData?.token_type?.ToString() is not string tokenType || - token.IsNullOrEmpty() || - tokenType.IsNullOrEmpty()) - { - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Failed to obtain token: {tokenData}"); - c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + + if (args.Count == 1) { + if (args["state"] == "discord") { + // c.Response.Redirect(f.Settings.OAuthURL); + c.Response.StatusCode = (int)HttpStatusCode.Redirect; + c.Response.Headers.Set("Location", f.Settings.DiscordOAuthURL); f.RespondJSON(c, new { - Error = "Couldn't obtain access token from Twitch." + Info = $"Redirecting to {f.Settings.DiscordOAuthURL}" }); return; } - if (tokenType == "bearer") - tokenType = "Bearer"; - - using (Stream s = client.SendAsync(new HttpRequestMessage + if (args["state"] == "twitch") { - RequestUri = new("https://api.twitch.tv/helix/users"), - Method = HttpMethod.Get, - Headers = { - { "Authorization", $"{tokenType} {token}" }, - { "Client-Id", f.Settings.TwitchOAuthClientID } - } - }).Await().Content.ReadAsStreamAsync().Await()) - using (StreamReader sr = new(s)) - using (JsonTextReader jtr = new(sr)) - userData = f.Serializer.Deserialize(jtr); - } + // c.Response.Redirect(f.Settings.OAuthURL); + c.Response.StatusCode = (int)HttpStatusCode.Redirect; + c.Response.Headers.Set("Location", f.Settings.TwitchOAuthURL); + f.RespondJSON(c, new + { + Info = $"Redirecting to {f.Settings.TwitchOAuthURL}" + }); - if (!($"{userData?.error}" is string error) || !error.IsNullOrEmpty()) - { - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Status: {userData?.status}. Error: {userData?.error}"); - c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - f.RespondJSON(c, new - { - Error = $"Twitch returned an error with status code {userData?.status}. This means that it came back as {userData?.error}" - }); - return; + return; + } } - - if (!($"{userData?.data[0].id}" is string uid) || - uid.IsNullOrEmpty()) + + if (args["state"].IsNullOrEmpty() || (args["state"] != "discord" && args["state"] != "twitch")) { - Logger.Log(LogLevel.CRI, "frontend-twitchauth", $"Failed to obtain ID: {userData?.data[0].id}"); - c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + c.Response.StatusCode = (int)HttpStatusCode.Unauthorized; f.RespondJSON(c, new { - Error = "Couldn't obtain user ID from Twitch." - }); - return; - } - - string key = f.Server.UserData.Create(uid, false); - BasicUserInfo info = f.Server.UserData.Load(uid); - - if ($"{userData?.data[0].display_name}" is string global_name && !global_name.IsNullOrEmpty()) - { - info.Name = global_name; - } - if (info.Name.Length > 32) - { - info.Name = info.Name.Substring(0, 32); - } - info.Discrim = "TWITCH"; - f.Server.UserData.Save(uid, info); - - Image avatarOrig; - using (HttpClient client = new()) - { - try - { - using Stream s = client.GetAsync( - $"{userData?.data[0].profile_image_url}" - ).Await().Content.ReadAsStreamAsync().Await(); - avatarOrig = Image.Load(s); - } - catch - { - using Stream s = client.GetAsync( - $"https://static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-300x300.png" - ).Await().Content.ReadAsStreamAsync().Await(); - avatarOrig = Image.Load(s); - } - } - - using (avatarOrig) - using (Image avatarScale = avatarOrig.Clone(x => x.Resize(64, 64, sampler: KnownResamplers.Lanczos3))) - using (Image avatarFinal = avatarScale.Clone(x => x.ApplyRoundedCorners().ApplyTagOverlays(f, info))) - { - - using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.orig.png")) - avatarScale.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); - - using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.png")) - avatarFinal.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); - } - - c.Response.StatusCode = (int)HttpStatusCode.Redirect; - c.Response.Headers.Set("Location", $"http://{c.Request.UserHostName}/"); - f.SetKeyCookie(c, key); - f.SetDiscordAuthCookie(c, code); - f.RespondJSON(c, new - { - Info = "Success - redirecting to /" - }); - } - - - - [RCEndpoint(false, "/discordauth", "", "", "Discord OAuth2", "User auth using Discord.")] - public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { - NameValueCollection args = f.ParseQueryString(c.Request.RawUrl); - Logger.Log(LogLevel.DBG, "frontend-discordauth", $"{c.Request.RawUrl}"); - Logger.Log(LogLevel.DBG, "frontend-discordauth", $"{f.ParseQueryString(c.Request.RawUrl)}"); - - if (args.Count == 0) { - // c.Response.Redirect(f.Settings.OAuthURL); - c.Response.StatusCode = (int) HttpStatusCode.Redirect; - c.Response.Headers.Set("Location", f.Settings.DiscordOAuthURL); - f.RespondJSON(c, new { - Info = $"Redirecting to {f.Settings.DiscordOAuthURL}" + Error = "Unauthorized - Invalid OAuth provider." }); return; } @@ -228,107 +91,190 @@ public static void DiscordOAuth(Frontend f, HttpRequestEventArgs c) { dynamic? tokenData; dynamic? userData; - - using (HttpClient client = new()) { + + string requestUri = args["state"] == "discord" ? "https://discord.com/api/oauth2/token" : + args["state"] == "twitch" ? "https://id.twitch.tv/oauth2/token" : ""; + string clientId = args["state"] == "discord" ? f.Settings.DiscordOAuthClientID : + args["state"] == "twitch" ? f.Settings.TwitchOAuthClientID : ""; + string clientSecret = args["state"] == "discord" ? f.Settings.DiscordOAuthClientSecret : + args["state"] == "twitch" ? f.Settings.TwitchOAuthClientSecret : ""; + string redirectUri = args["state"] == "discord" ? f.Settings.DiscordOAuthRedirectURL : + args["state"] == "twitch" ? f.Settings.TwitchOAuthRedirectURL : ""; + string scopes = args["state"] == "discord" ? "identity" : args["state"] == "twitch" ? "user:read:chat" : ""; + + using (HttpClient client = new()) + { #pragma warning disable CS8714 // new FormUrlEncodedContent expects nullable. - using (Stream s = client.PostAsync("https://discord.com/api/oauth2/token", new FormUrlEncodedContent(new Dictionary() { + using (Stream s = client.PostAsync(requestUri, + new FormUrlEncodedContent(new Dictionary() + { #pragma warning restore CS8714 - { "client_id", f.Settings.DiscordOAuthClientID }, - { "client_secret", f.Settings.DiscordOAuthClientSecret }, - { "grant_type", "authorization_code" }, - { "code", code }, - { "redirect_uri", f.Settings.DiscordOAuthRedirectURL }, - { "scope", "identity" } - })).Await().Content.ReadAsStreamAsync().Await()) - using (StreamReader sr = new(s)) - using (JsonTextReader jtr = new(sr)) - tokenData = f.Serializer.Deserialize(jtr); - - if (tokenData?.access_token?.ToString() is not string token || - tokenData?.token_type?.ToString() is not string tokenType || - token.IsNullOrEmpty() || - tokenType.IsNullOrEmpty()) { - Logger.Log(LogLevel.CRI, "frontend-discordauth", $"Failed to obtain token: {tokenData}"); - c.Response.StatusCode = (int) HttpStatusCode.InternalServerError; - f.RespondJSON(c, new { - Error = "Couldn't obtain access token from Discord." + { "client_id", clientId }, + { "client_secret", clientSecret }, + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "scope", scopes } + })).Await().Content.ReadAsStreamAsync().Await()) + using (StreamReader sr = new(s)) + using (JsonTextReader jtr = new(sr)) + tokenData = f.Serializer.Deserialize(jtr); + + Logger.Log(LogLevel.DBG, "frontend-standardauth", $"Request URI: {requestUri}"); + + if (tokenData?.access_token?.ToString() is not string token || + tokenData?.token_type?.ToString() is not string tokenType || + token.IsNullOrEmpty() || + tokenType.IsNullOrEmpty()) + { + Logger.Log(LogLevel.CRI, "frontend-standardauth", $"Failed to obtain token: {tokenData}"); + c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + f.RespondJSON(c, new + { + Error = "Couldn't obtain access token." + }); + return; + } + + // Twitch hates it if "bearer" is lowercase + if (tokenType == "bearer" && args["state"] == "twitch") + tokenType = "Bearer"; + + requestUri = args["state"] == "discord" ? "https://discord.com/api/users/@me" : + args["state"] == "twitch" ? "https://api.twitch.tv/helix/users" : ""; + + var request = new HttpRequestMessage + { + RequestUri = new Uri(requestUri), + Method = HttpMethod.Get + }; + + if (args["state"] == "discord") + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(tokenType, token); + } else if (args["state"] == "twitch") + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(tokenType, token); + request.Headers.Add("Client-ID", f.Settings.TwitchOAuthClientID); + } + + using (Stream s = client.SendAsync(request).Await().Content.ReadAsStreamAsync().Await()) + using (StreamReader sr = new(s)) + using (JsonTextReader jtr = new(sr)) + userData = f.Serializer.Deserialize(jtr); + } + + if (!string.IsNullOrEmpty(userData?.error)) + { + Logger.Log(LogLevel.CRI, "frontend-standardauth", $"Status: {userData?.status}. Error: {userData?.error}"); + c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + f.RespondJSON(c, new + { + Error = $"The OAuth provider you have chosen returned an error with status code {userData?.status}. This means that it came back as {userData?.error}" + }); + return; + } + Logger.Log(LogLevel.CRI, "frontend-standardauth", $"UserData-pre: {userData}"); + userData = userData?.data[0] ?? userData; + + if (!(userData?.id?.ToString() is string uid) || + uid.IsNullOrEmpty()) + { + Logger.Log(LogLevel.CRI, "frontend-standardauth", $"Failed to obtain ID: {userData}"); + c.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + f.RespondJSON(c, new + { + Error = "Couldn't obtain user ID." }); return; } + string key = f.Server.UserData.Create(uid, false); + BasicUserInfo info = f.Server.UserData.Load(uid); - using (Stream s = client.SendAsync(new HttpRequestMessage { - RequestUri = new("https://discord.com/api/users/@me"), - Method = HttpMethod.Get, - Headers = { - { "Authorization", $"{tokenType} {token}" } - } - }).Await().Content.ReadAsStreamAsync().Await()) - using (StreamReader sr = new(s)) - using (JsonTextReader jtr = new(sr)) - userData = f.Serializer.Deserialize(jtr); - } + if ((userData.global_name?.ToString() ?? userData.display_name?.ToString()) is string global_name && !global_name.IsNullOrEmpty()) + { + info.Name = global_name; + } + else + { + info.Name = userData.username.ToString(); + } - if (!(userData?.id?.ToString() is string uid) || - uid.IsNullOrEmpty()) { - Logger.Log(LogLevel.CRI, "frontend-discordauth", $"Failed to obtain ID: {userData}"); - c.Response.StatusCode = (int) HttpStatusCode.InternalServerError; - f.RespondJSON(c, new { - Error = "Couldn't obtain user ID from Discord." - }); - return; - } + if (info.Name.Length > 32) + { + info.Name = info.Name.Substring(0, 32); + } - string key = f.Server.UserData.Create(uid, false); - BasicUserInfo info = f.Server.UserData.Load(uid); + // Since ALL discord accounts do not have discriminators, we can seperate DISCORD and TWITCH accounts with the discrim value + info.Discrim = args["state"]?.ToUpper() ?? "FALLBACK"; + f.Server.UserData.Save(uid, info); - if (userData.global_name?.ToString() is string global_name && !global_name.IsNullOrEmpty()) { - info.Name = global_name; - } else { - info.Name = userData.username.ToString(); - } - if (info.Name.Length > 32) { - info.Name = info.Name.Substring(0, 32); - } - // Since ALL discord accounts do not have discriminators, we can seperate DISCORD and TWITCH accounts with the discrim value - info.Discrim = "DISCORD"; - f.Server.UserData.Save(uid, info); - - Image avatarOrig; - using (HttpClient client = new()) { - try { - using Stream s = client.GetAsync( - $"https://cdn.discordapp.com/avatars/{uid}/{userData.avatar.ToString()}.png?size=64" - ).Await().Content.ReadAsStreamAsync().Await(); - avatarOrig = Image.Load(s); - } catch { - using Stream s = client.GetAsync( - $"https://cdn.discordapp.com/embed/avatars/{((int) userData.discriminator) % 6}.png" - ).Await().Content.ReadAsStreamAsync().Await(); - avatarOrig = Image.Load(s); + Image avatarOrig; + using (HttpClient client = new()) + { + if (args["state"] == "discord") { + try + { + using Stream s = client.GetAsync( + $"https://cdn.discordapp.com/avatars/{uid}/{userData.avatar.ToString()}.png?size=64" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } + catch + { + using Stream s = client.GetAsync( + $"https://cdn.discordapp.com/embed/avatars/{((int)userData.discriminator) % 6}.png" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } + } else if (args["state"] == "twitch") + { + try + { + using Stream s = client.GetAsync( + $"{userData?.profile_image_url}" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } + catch + { + using Stream s = client.GetAsync( + $"https://static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-300x300.png" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } + } + else + { + using Stream s = client.GetAsync( + $"https://static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-300x300.png" + ).Await().Content.ReadAsStreamAsync().Await(); + avatarOrig = Image.Load(s); + } } - } - using (avatarOrig) - using (Image avatarScale = avatarOrig.Clone(x => x.Resize(64, 64, sampler: KnownResamplers.Lanczos3))) - using (Image avatarFinal = avatarScale.Clone(x => x.ApplyRoundedCorners().ApplyTagOverlays(f, info))) { + using (avatarOrig) + using (Image avatarScale = avatarOrig.Clone(x => x.Resize(64, 64, sampler: KnownResamplers.Lanczos3))) + using (Image avatarFinal = avatarScale.Clone(x => x.ApplyRoundedCorners().ApplyTagOverlays(f, info))) + { - using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.orig.png")) - avatarScale.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); + using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.orig.png")) + avatarScale.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); - using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.png")) - avatarFinal.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); - } + using (Stream s = f.Server.UserData.WriteFile(uid, "avatar.png")) + avatarFinal.SaveAsPng(s, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); + } - c.Response.StatusCode = (int) HttpStatusCode.Redirect; - c.Response.Headers.Set("Location", $"http://{c.Request.UserHostName}/"); - f.SetKeyCookie(c, key); - f.SetDiscordAuthCookie(c, code); - f.RespondJSON(c, new { - Info = "Success - redirecting to /" - }); + c.Response.StatusCode = (int)HttpStatusCode.Redirect; + c.Response.Headers.Set("Location", $"http://{c.Request.UserHostName}/"); + f.SetKeyCookie(c, key); + f.SetDiscordAuthCookie(c, code); + f.RespondJSON(c, new + { + Info = "Success - redirecting to /" + }); } - private static IImageProcessingContext ApplyTagOverlays(this IImageProcessingContext context, Frontend f, BasicUserInfo info) { foreach (string tagName in info.Tags) { using Stream? asset = f.OpenContent($"frontend/assets/tags/{tagName}.png", out _, out _, out _); From e78d6feb5f209f3f46436f4a9b417d9e0f49c63a Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sun, 3 Nov 2024 21:42:52 -0500 Subject: [PATCH 08/11] Fix issue with logging in with discord, remove debug Logger.Log() funcs --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index d91d185b..53efb7bb 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -174,8 +174,12 @@ public static void StandardOAuth(Frontend f, HttpRequestEventArgs c) { }); return; } - Logger.Log(LogLevel.CRI, "frontend-standardauth", $"UserData-pre: {userData}"); - userData = userData?.data[0] ?? userData; + try + { + userData = userData?.data[0]; + } catch (Exception ex) { + userData = userData; + } if (!(userData?.id?.ToString() is string uid) || uid.IsNullOrEmpty()) From ddac41b9b181ace7106bb4474ddda44df1455b0b Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sun, 3 Nov 2024 21:53:25 -0500 Subject: [PATCH 09/11] Fix chance of possible shared UIDs between OAuths --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index 53efb7bb..c7de160e 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -193,6 +193,8 @@ public static void StandardOAuth(Frontend f, HttpRequestEventArgs c) { return; } + string pfpId = uid; + uid = $"{uid}-{args["state"]}"; string key = f.Server.UserData.Create(uid, false); BasicUserInfo info = f.Server.UserData.Load(uid); @@ -221,7 +223,7 @@ public static void StandardOAuth(Frontend f, HttpRequestEventArgs c) { try { using Stream s = client.GetAsync( - $"https://cdn.discordapp.com/avatars/{uid}/{userData.avatar.ToString()}.png?size=64" + $"https://cdn.discordapp.com/avatars/{pfpId}/{userData.avatar.ToString()}.png?size=64" ).Await().Content.ReadAsStreamAsync().Await(); avatarOrig = Image.Load(s); } From 9d4ee1ac1b34c5edb5913871dd4885a3a72d3ed8 Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sun, 3 Nov 2024 21:55:13 -0500 Subject: [PATCH 10/11] Fix concern about possibly missing "display_name" --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index c7de160e..b75f7120 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -198,7 +198,7 @@ public static void StandardOAuth(Frontend f, HttpRequestEventArgs c) { string key = f.Server.UserData.Create(uid, false); BasicUserInfo info = f.Server.UserData.Load(uid); - if ((userData.global_name?.ToString() ?? userData.display_name?.ToString()) is string global_name && !global_name.IsNullOrEmpty()) + if ((userData.global_name?.ToString() ?? userData.display_name?.ToString() ?? userData.login?.ToString()) is string global_name && !global_name.IsNullOrEmpty()) { info.Name = global_name; } From cd1441bc1c2379114d3294029094a2b6f110a12e Mon Sep 17 00:00:00 2001 From: Stigstille Date: Sun, 3 Nov 2024 21:58:01 -0500 Subject: [PATCH 11/11] Fix inconsequential warnings --- CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs index b75f7120..31fa2dbd 100644 --- a/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs +++ b/CelesteNet.Server.FrontendModule/RCEPs/RCEPPublic.cs @@ -178,7 +178,7 @@ public static void StandardOAuth(Frontend f, HttpRequestEventArgs c) { { userData = userData?.data[0]; } catch (Exception ex) { - userData = userData; + Logger.Log(LogLevel.DEV, "frontend-standardauth", $"No \"data\" array in userData: {ex}"); } if (!(userData?.id?.ToString() is string uid) ||