Add Discord OAuth2 login and refactor authentication
Implemented Discord OAuth2 authentication flow with a new Discord.cs handler and integrated it into Program.cs. Refactored authentication logic to use shared JsonSerializerOptions from LauncherConstants. Updated BashUtils with cross-platform URL opening, improved MojangAPI to use shared HttpClient, and enhanced frontend login and log handling in logged.js and login.js.
This commit is contained in:
parent
4ffe3b47c2
commit
d2ae3122c9
@ -1,4 +1,6 @@
|
||||
using Lentia.Utils;
|
||||
using Lentia.Core.Auth.OAuth2;
|
||||
using Lentia.Core.Constants;
|
||||
using Lentia.Utils;
|
||||
using Photino.NET;
|
||||
using System.Text.Json;
|
||||
using LentRules = Lentia.Core.Auth.Yggdrasil;
|
||||
@ -9,7 +11,6 @@ class Program {
|
||||
private static LentRules.AuthenticateResponse? _authenticatedPlayer;
|
||||
private static readonly string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
private static readonly LentRules.Authenticator _ygg = new();
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
|
||||
[STAThread]
|
||||
static void Main(string[] args) {
|
||||
@ -64,13 +65,15 @@ class Program {
|
||||
case "auth::lentia":
|
||||
string user = jsonPayload.GetProperty("username").GetString()!;
|
||||
string pass = jsonPayload.GetProperty("password").GetString()!;
|
||||
var result = await _ygg.Login(user, pass);
|
||||
var lentiaAuthResult = await _ygg.Login(user, pass);
|
||||
|
||||
if (result.Success) {
|
||||
_authenticatedPlayer = result.Player;
|
||||
if (lentiaAuthResult.Success) {
|
||||
_authenticatedPlayer = lentiaAuthResult.Player;
|
||||
WindowHelper.MakeStandardWindow(pw, 1356, 720, "wwwroot/logged.html");
|
||||
payload = new { success = true };
|
||||
} else {
|
||||
payload = new { success = false, error = lentiaAuthResult.Error };
|
||||
}
|
||||
payload = result;
|
||||
break;
|
||||
|
||||
case "auth::logout":
|
||||
@ -83,7 +86,7 @@ class Program {
|
||||
case "dialog::error":
|
||||
string title = jsonPayload.TryGetProperty("title", out var t) ? t.GetString()! : "Lentia";
|
||||
string msg = jsonPayload.GetProperty("message").GetString()!;
|
||||
pw.ShowMessage(title, msg);
|
||||
pw.ShowMessage(title, msg, PhotinoDialogButtons.Ok, PhotinoDialogIcon.Error);
|
||||
payload = new { success = true };
|
||||
break;
|
||||
|
||||
@ -188,10 +191,25 @@ class Program {
|
||||
payload = new { success = false, error = ex.Message };
|
||||
}
|
||||
break;
|
||||
|
||||
case "oauth2::discord":
|
||||
BashUtils.OpenUrl("https://yggdrasil.azures.fr/auth/provider/discord/login");
|
||||
string? code = await Discord.ListenForCode();
|
||||
var discordLoginResult = await Discord.LoginWithDiscordOAuth2(code!);
|
||||
|
||||
if (discordLoginResult.Success) {
|
||||
_authenticatedPlayer = discordLoginResult.Player;
|
||||
WindowHelper.MakeStandardWindow(pw, 1356, 720, "wwwroot/logged.html");
|
||||
payload = new { success = true };
|
||||
} else {
|
||||
Console.WriteLine(discordLoginResult);
|
||||
payload = new { success = false, error = discordLoginResult.Error };
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var response = new { requestId, payload };
|
||||
window.SendWebMessage(JsonSerializer.Serialize(response, _jsonOptions));
|
||||
window.SendWebMessage(JsonSerializer.Serialize(response, LauncherConstants._jsonOptions));
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Bridge error: {ex.Message}");
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Lentia.Core.Constants
|
||||
{
|
||||
public static class LauncherConstants
|
||||
{
|
||||
public static readonly HttpClient Http = new HttpClient();
|
||||
|
||||
public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
public static class Urls
|
||||
{
|
||||
public const string MojangManifest = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json";
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Lentia.Core.Auth.Yggdrasil;
|
||||
using Lentia.Core.Constants;
|
||||
|
||||
namespace Lentia.Core.Auth.OAuth2;
|
||||
|
||||
public class Discord {
|
||||
|
||||
private static HttpListener? _listener;
|
||||
private static readonly string _url = "http://localhost:8877/auth/provider/discord/login/callback/";
|
||||
|
||||
public static async Task<string?> ListenForCode() {
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add(_url);
|
||||
_listener.Start();
|
||||
|
||||
HttpListenerContext context = await _listener.GetContextAsync();
|
||||
HttpListenerRequest request = context.Request;
|
||||
|
||||
string? code = request.QueryString["code"];
|
||||
string redirectUrl = "https://yggdrasil.azures.fr/static/success.html";
|
||||
|
||||
HttpListenerResponse response = context.Response;
|
||||
|
||||
response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
response.RedirectLocation = redirectUrl;
|
||||
response.OutputStream.Close();
|
||||
|
||||
_listener.Stop();
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
public static async Task<AuthResult> LoginWithDiscordOAuth2(string authCode) {
|
||||
var url = $"https://yggdrasil.azures.fr/auth/provider/discord/login/callback?code={authCode}&requestUser=true";
|
||||
try {
|
||||
var response = await LauncherConstants.Http.GetAsync(url);
|
||||
if (response.IsSuccessStatusCode) {
|
||||
var data = await response.Content.ReadFromJsonAsync<AuthenticateResponse>();
|
||||
return data != null ? AuthResult.Ok(data) : AuthResult.Fail("Erreur de lecture des données.");
|
||||
} else {
|
||||
var errorData = await response.Content.ReadFromJsonAsync<YggdrasilError>();
|
||||
Console.WriteLine(await response.Content.ReadAsStringAsync());
|
||||
return AuthResult.Fail(
|
||||
errorData?.errorMessage ?? "Identifiants invalides.",
|
||||
errorData?.error,
|
||||
errorData?.cause
|
||||
);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new Exception($"Error when establishing connection to the API : {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Lentia.Utils;
|
||||
|
||||
@ -16,4 +17,22 @@ public static class BashUtils {
|
||||
proc.Start();
|
||||
return proc.StandardOutput.ReadToEnd();
|
||||
}
|
||||
|
||||
public static void OpenUrl(string url) {
|
||||
try {
|
||||
Process.Start(url);
|
||||
}
|
||||
catch {
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
url = url.Replace("&", "^&");
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
|
||||
Process.Start("xdg-open", url);
|
||||
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||
Process.Start("open", url);
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,14 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Lentia.Core.Constants;
|
||||
|
||||
namespace Lentia.Utils;
|
||||
|
||||
public static class MojangAPI {
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
public static async Task<string> UploadSkinAsync(string filePath, string variant, string token) {
|
||||
using var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
using var content = new MultipartFormDataContent();
|
||||
|
||||
@ -21,7 +20,7 @@ public static class MojangAPI {
|
||||
|
||||
content.Add(fileContent, "file", Path.GetFileName(filePath));
|
||||
|
||||
var response = await client.PostAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/skins", content);
|
||||
var response = await LauncherConstants.Http.PostAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/skins", content);
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
@ -31,66 +30,61 @@ public static class MojangAPI {
|
||||
}
|
||||
|
||||
|
||||
public static async Task<object> GetPlayerProfileAsync(string accessToken) {
|
||||
using var client = new HttpClient();
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
|
||||
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
public static async Task<object> GetPlayerProfileAsync(string accessToken) {
|
||||
LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
LauncherConstants.Http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
var response = await client.GetAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile");
|
||||
var response = await LauncherConstants.Http.GetAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile");
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
string jsonResponse = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, _jsonOptions)!;
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, LauncherConstants._jsonOptions)!;
|
||||
} else {
|
||||
throw new Exception($"API Error: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<object> HideCapeAsync(string accessToken) {
|
||||
using var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
var response = await client.DeleteAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active");
|
||||
var response = await LauncherConstants.Http.DeleteAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active");
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
string jsonResponse = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, _jsonOptions)!;
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, LauncherConstants._jsonOptions)!;
|
||||
} else {
|
||||
throw new Exception($"Échec du retrait de la cape : {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<object> ShowCapeAsync(string accessToken, string capeId) {
|
||||
using var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
var body = new { capeId = capeId };
|
||||
string jsonBody = JsonSerializer.Serialize(body);
|
||||
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.PutAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active", content);
|
||||
var response = await LauncherConstants.Http.PutAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active", content);
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
string jsonResponse = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, _jsonOptions)!;
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, LauncherConstants._jsonOptions)!;
|
||||
} else {
|
||||
throw new Exception($"Erreur lors de l'équipement de la cape : {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<object> ChangeUsernameAsync(string newName, string accessToken) {
|
||||
using var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
var url = $"https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/name/{newName}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Put, url);
|
||||
var response = await client.SendAsync(request);
|
||||
var response = await LauncherConstants.Http.SendAsync(request);
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
string jsonResponse = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, _jsonOptions)!;
|
||||
return JsonSerializer.Deserialize<object>(jsonResponse, LauncherConstants._jsonOptions)!;
|
||||
}
|
||||
else {
|
||||
throw new Exception($"Erreur lors du changement de pseudo (Code: {response.StatusCode})");
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const buttons = document.querySelectorAll("button[frame]")
|
||||
const dynmapFrame = document.querySelector("article.frame.dynmap > iframe")
|
||||
const capesSelector = document.querySelector("article.capes > div.capes")
|
||||
const logsContainer = document.querySelector("div.container.logs")
|
||||
|
||||
let viewerInstance = new skinview3d.SkinViewer({
|
||||
canvas: document.getElementById("skin"),
|
||||
@ -99,7 +100,7 @@ window.initSkin = async function initSkin() {
|
||||
canvas: document.getElementById("skin"),
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight,
|
||||
skin: `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url}`.replace(/\/+$/, "")
|
||||
skin: `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url.replace(/^\//, "")}`
|
||||
})
|
||||
|
||||
const activeCape = window.profile.capes.find(s => s.state === "ACTIVE") || null
|
||||
@ -228,6 +229,18 @@ window.changeUsername = async function changeUsername(newName) {
|
||||
}
|
||||
}
|
||||
|
||||
window.gamelog = {}
|
||||
|
||||
window.gamelog.put = async function put(log) {
|
||||
const logElement = document.createElement("p")
|
||||
logElement.innerText = log
|
||||
logsContainer.appendChild(log)
|
||||
}
|
||||
|
||||
window.gamelog.clear = async function clear() {
|
||||
logsContainer.innerHTML = ""
|
||||
}
|
||||
|
||||
await initSkin()
|
||||
await initSettings()
|
||||
await initCapesSelector()
|
||||
|
||||
@ -17,22 +17,32 @@ async function login() {
|
||||
const username = document.querySelector("#username").value
|
||||
const password = document.querySelector("#password").value
|
||||
const result = await system.call("auth::lentia", { username, password })
|
||||
if (result.success == false) {
|
||||
await system.call("dialog::error", {
|
||||
title: result.error.error,
|
||||
message: result.error.errorMessage
|
||||
})
|
||||
}
|
||||
await processLoginResult(result)
|
||||
}
|
||||
|
||||
function clearPassword() {
|
||||
password.value = ""
|
||||
}
|
||||
|
||||
function requestLoginWithOAuth2() {
|
||||
async function requestLoginWithOAuth2() {
|
||||
showFrame("oauth2")
|
||||
hideInformation()
|
||||
showLoadingBar()
|
||||
const result = await system.call("oauth2::discord")
|
||||
await processLoginResult(result, true)
|
||||
}
|
||||
|
||||
async function processLoginResult(payload, showDefaultPage = false) {
|
||||
if (payload.success == false) {
|
||||
await system.call("dialog::error", {
|
||||
title: payload.error.error,
|
||||
message: payload.error.errorMessage
|
||||
})
|
||||
if (showDefaultPage) {
|
||||
hideLoadingBar()
|
||||
showFrame("provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideLoadingBar()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user