Upload via GUI (19 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 07:03:08 +02:00
parent fb6da6562e
commit ab502f44ef
19 changed files with 10983 additions and 21 deletions
+333
View File
@@ -0,0 +1,333 @@
'use strict';
/*
* auth.js offizieller Microsoft-Login für Minecraft (Phase 3)
* -------------------------------------------------------------
* Ablauf (Device-Code-Flow, wie bei Prism/MultiMC):
* 1. Microsoft OAuth Device-Code -> MS access_token + refresh_token
* 2. Xbox-Live-Authentifizierung -> XBL-Token + userhash
* 3. XSTS-Autorisierung -> XSTS-Token
* 4. Minecraft-Login -> MC access_token
* 5. Minecraft-Profil -> UUID + Name (prüft zugleich den Besitz)
*
* Erfordert eine kostenlose Azure-App-ID (öffentlicher Client, Device-Flow).
* Wir bauen KEINEN Offline-/Crack-Login ohne echtes Konto kein Start.
*/
const fs = require('fs');
const path = require('path');
// Fest eingebaute Azure-App-ID des Launchers (öffentlicher Client kein Geheimnis).
// Jeder Nutzer meldet sich damit über SEIN eigenes Microsoft-Konto an.
// Über die Einstellungen lässt sich bei Bedarf eine eigene ID hinterlegen.
const DEFAULT_CLIENT_ID = '0625f4a8-dfff-4eab-8124-a573b00b0319';
const SCOPE = 'XboxLive.signin offline_access';
// Manche Endpunkte (v. a. api.minecraftservices.com hinter Cloudflare) lehnen
// Anfragen ohne User-Agent mit HTTP 403 ab -> überall mitsenden.
const UA = 'AeroMC-Launcher/0.5';
const DEVICECODE_URL = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode';
const TOKEN_URL = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';
const XBL_URL = 'https://user.auth.xboxlive.com/user/authenticate';
const XSTS_URL = 'https://xsts.auth.xboxlive.com/xsts/authorize';
const MC_LOGIN_URL = 'https://api.minecraftservices.com/authentication/login_with_xbox';
const MC_PROFILE_URL = 'https://api.minecraftservices.com/minecraft/profile';
let USER_DATA = null;
let SECURE = null; // optionaler Verschlüsseler (Electron safeStorage)
function init(userDataPath, secure) {
USER_DATA = userDataPath;
SECURE = secure && secure.available && secure.available() ? secure : null;
migrateLegacy();
if (SECURE) { try { writeStore(readStore()); } catch { /* Migration best effort */ } }
}
// sensible Felder verschlüsselt ablegen (an das Windows-Benutzerkonto gebunden)
const SENSITIVE = ['mcAccessToken', 'msRefreshToken'];
const ENC = 'enc:v1:';
function protect(acc) {
if (!SECURE) return acc;
const out = Object.assign({}, acc);
for (const k of SENSITIVE) {
const v = out[k];
if (typeof v === 'string' && v && !v.startsWith(ENC)) {
try { out[k] = ENC + SECURE.encrypt(v); } catch { /* unverschlüsselt lassen */ }
}
}
return out;
}
function unprotect(acc) {
const out = Object.assign({}, acc);
for (const k of SENSITIVE) {
const v = out[k];
if (typeof v === 'string' && v.startsWith(ENC)) {
try { out[k] = SECURE ? SECURE.decrypt(v.slice(ENC.length)) : null; } catch { out[k] = null; }
}
}
return out;
}
function accountsFile() { return path.join(USER_DATA, 'accounts.json'); }
function legacyFile() { return path.join(USER_DATA, 'account.json'); }
// Speicherform: { activeId, accounts: [ {profile,mcAccessToken,mcExpiresAt,msRefreshToken,updatedAt}, ... ] }
function readStore() {
try {
const s = JSON.parse(fs.readFileSync(accountsFile(), 'utf8'));
if (s && Array.isArray(s.accounts)) {
return { activeId: s.activeId, accounts: s.accounts.map(unprotect) };
}
} catch { /* leer */ }
return { activeId: null, accounts: [] };
}
function writeStore(s) {
const safe = { activeId: s.activeId, accounts: (s.accounts || []).map(protect) };
fs.writeFileSync(accountsFile(), JSON.stringify(safe, null, 2));
}
// altes Einzelkonto (account.json) übernehmen
function migrateLegacy() {
try {
if (fs.existsSync(accountsFile())) return;
const old = JSON.parse(fs.readFileSync(legacyFile(), 'utf8'));
if (old && old.profile) {
writeStore({ activeId: old.profile.id, accounts: [old] });
try { fs.unlinkSync(legacyFile()); } catch { /* egal */ }
}
} catch { /* nichts zu migrieren */ }
}
function upsertAccount(acc) {
const s = readStore();
s.accounts = s.accounts.filter((a) => a.profile.id !== acc.profile.id);
s.accounts.push(acc);
s.activeId = acc.profile.id; // frisch eingeloggtes/erneuertes Konto wird aktiv
writeStore(s);
return acc;
}
function listAccounts() {
const s = readStore();
return s.accounts.map((a) => ({ id: a.profile.id, name: a.profile.name, active: a.profile.id === s.activeId }));
}
function setActive(id) {
const s = readStore();
if (s.accounts.some((a) => a.profile.id === id)) { s.activeId = id; writeStore(s); }
return listAccounts();
}
function removeAccount(id) {
const s = readStore();
s.accounts = s.accounts.filter((a) => a.profile.id !== id);
if (s.activeId === id) s.activeId = s.accounts[0] ? s.accounts[0].profile.id : null;
writeStore(s);
return listAccounts();
}
function getAccountById(id) {
return readStore().accounts.find((a) => a.profile.id === id) || null;
}
function getActiveRaw() {
const s = readStore();
return s.accounts.find((a) => a.profile.id === s.activeId) || null;
}
function clearAccount() { try { fs.unlinkSync(accountsFile()); } catch { /* egal */ } }
async function postForm(url, params) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': UA, Accept: 'application/json' },
body: new URLSearchParams(params).toString(),
});
const data = await res.json().catch(() => ({}));
return { status: res.status, data };
}
async function postJson(url, body, headers = {}) {
const res = await fetch(url, {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': UA }, headers),
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
return { status: res.status, data };
}
// ---------------------------------------------------------------------------
// 1. Device-Code anfordern
// ---------------------------------------------------------------------------
async function requestDeviceCode(clientId) {
if (!clientId) throw new Error('Keine Azure-Client-ID gesetzt (Einstellungen → Microsoft-Login).');
const { status, data } = await postForm(DEVICECODE_URL, { client_id: clientId, scope: SCOPE });
if (status !== 200) {
throw new Error('Device-Code fehlgeschlagen: ' + (data.error_description || data.error || status));
}
return data; // { user_code, device_code, verification_uri, expires_in, interval, message }
}
// ---------------------------------------------------------------------------
// 2. Auf Autorisierung warten (pollt Token-Endpoint)
// ---------------------------------------------------------------------------
async function pollForToken(clientId, deviceCode, intervalSec, onTick) {
let interval = Math.max(intervalSec || 5, 3);
const deadline = Date.now() + 15 * 60 * 1000;
while (Date.now() < deadline) {
await sleep(interval * 1000);
const { data } = await postForm(TOKEN_URL, {
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
client_id: clientId,
device_code: deviceCode,
});
if (data.access_token) return data; // { access_token, refresh_token, expires_in }
if (data.error === 'authorization_pending') { onTick && onTick(); continue; }
if (data.error === 'slow_down') { interval += 5; continue; }
if (data.error === 'authorization_declined') throw new Error('Anmeldung abgelehnt.');
if (data.error === 'expired_token') throw new Error('Code abgelaufen bitte erneut anmelden.');
throw new Error('Login-Fehler: ' + (data.error_description || data.error));
}
throw new Error('Zeitüberschreitung Code nicht rechtzeitig bestätigt.');
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
// ---------------------------------------------------------------------------
// 3.5. Xbox -> XSTS -> Minecraft -> Profil
// ---------------------------------------------------------------------------
async function xboxLogin(msAccessToken) {
const { status, data } = await postJson(XBL_URL, {
Properties: { AuthMethod: 'RPS', SiteName: 'user.auth.xboxlive.com', RpsTicket: 'd=' + msAccessToken },
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT',
});
if (status !== 200 || !data.Token) {
throw new Error('Xbox-Live-Login fehlgeschlagen (' + status + ')' + (data && data.Message ? ': ' + data.Message : '') + '.');
}
const uhs = data.DisplayClaims && data.DisplayClaims.xui && data.DisplayClaims.xui[0] && data.DisplayClaims.xui[0].uhs;
return { token: data.Token, uhs };
}
async function xstsAuth(xblToken) {
const { status, data } = await postJson(XSTS_URL, {
Properties: { SandboxId: 'RETAIL', UserTokens: [xblToken] },
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT',
});
if (status === 401) {
const xerr = data.XErr;
if (xerr === 2148916233) throw new Error('Dieses Microsoft-Konto hat kein Xbox-Profil. Bitte einmal auf xbox.com anmelden.');
if (xerr === 2148916238) throw new Error('Kinderkonto muss einer Familie hinzugefügt werden.');
throw new Error('XSTS-Autorisierung abgelehnt (XErr ' + xerr + ').');
}
if (status !== 200 || !data.Token) throw new Error('XSTS-Autorisierung fehlgeschlagen (' + status + ').');
return { token: data.Token };
}
async function minecraftLogin(uhs, xstsToken) {
const { status, data } = await postJson(MC_LOGIN_URL, {
identityToken: `XBL3.0 x=${uhs};${xstsToken}`,
});
if (status !== 200 || !data.access_token) {
const detail = data && (data.errorMessage || data.error || data.path) ? ': ' + (data.errorMessage || data.error || data.path) : '';
throw new Error('Minecraft-Login fehlgeschlagen (' + status + ')' + detail + '.');
}
return { accessToken: data.access_token, expiresIn: data.expires_in || 86400 };
}
async function minecraftProfile(mcAccessToken) {
const res = await fetch(MC_PROFILE_URL, {
headers: { Authorization: 'Bearer ' + mcAccessToken, 'User-Agent': UA, Accept: 'application/json' },
});
if (res.status === 404) throw new Error('Dieses Konto besitzt kein Minecraft (Java Edition).');
if (!res.ok) throw new Error('Profil laden fehlgeschlagen (' + res.status + ').');
const data = await res.json();
return { id: data.id, name: data.name }; // id = UUID ohne Bindestriche
}
// vollständige Kette ab MS-Token -> gespeichertes Konto
async function completeFromMsToken(msToken) {
const xbl = await xboxLogin(msToken.access_token);
const xsts = await xstsAuth(xbl.token);
const mc = await minecraftLogin(xbl.uhs, xsts.token);
const profile = await minecraftProfile(mc.accessToken);
return {
profile, // { id, name }
mcAccessToken: mc.accessToken,
mcExpiresAt: Date.now() + (mc.expiresIn - 60) * 1000,
msRefreshToken: msToken.refresh_token || null,
updatedAt: new Date().toISOString(),
};
}
// ersetzt ein Konto in der Liste, ohne das aktive Konto zu ändern (für Token-Refresh)
function updateAccount(acc) {
const s = readStore();
s.accounts = s.accounts.filter((a) => a.profile.id !== acc.profile.id);
s.accounts.push(acc);
writeStore(s);
return acc;
}
// ---------------------------------------------------------------------------
// Öffentliche Login-Orchestrierung
// ---------------------------------------------------------------------------
/*
* startLogin: fordert Device-Code an und ruft danach den Callback mit dem Code.
* Läuft dann im Hintergrund bis zur Bestätigung. Gibt am Ende das Konto zurück.
*/
async function login(clientId, onCode, onTick) {
const dc = await requestDeviceCode(clientId);
onCode && onCode({
userCode: dc.user_code,
verificationUri: dc.verification_uri,
message: dc.message,
expiresIn: dc.expires_in,
});
const msToken = await pollForToken(clientId, dc.device_code, dc.interval, onTick);
const account = await completeFromMsToken(msToken);
return upsertAccount(account); // neues Konto speichern + aktiv setzen
}
// ein Konto gültig machen (Token ggf. per Refresh erneuern)
async function refreshAccount(clientId, acc) {
if (acc.mcAccessToken && acc.mcExpiresAt && Date.now() < acc.mcExpiresAt) return acc;
if (acc.msRefreshToken && clientId) {
const { data } = await postForm(TOKEN_URL, {
grant_type: 'refresh_token',
client_id: clientId,
refresh_token: acc.msRefreshToken,
scope: SCOPE,
});
if (data.access_token) {
try {
const fresh = await completeFromMsToken(data);
if (!fresh.msRefreshToken) fresh.msRefreshToken = acc.msRefreshToken;
return updateAccount(fresh);
} catch { return null; }
}
}
return null;
}
// aktives Konto gültig holen (null wenn kein Login)
async function getValidAccount(clientId) {
const acc = getActiveRaw();
return acc ? refreshAccount(clientId, acc) : null;
}
// bestimmtes Konto gültig holen (für „starten als …")
async function getValidAccountById(clientId, id) {
const acc = getAccountById(id);
return acc ? refreshAccount(clientId, acc) : null;
}
function currentAccount() { return getActiveRaw(); }
module.exports = {
init, login, getValidAccount, getValidAccountById, currentAccount, clearAccount,
listAccounts, setActive, removeAccount, DEFAULT_CLIENT_ID,
};