q13x-eaglerproxy/server/plugins/EagProxyAAS/CustomAuthflow.js
2024-09-04 12:02:00 +00:00

187 lines
8.1 KiB
JavaScript

import fs from "fs";
import crypto from "crypto";
import Constants from "prismarine-auth/src/common/Constants.js";
const { Endpoints, msalConfig } = Constants;
import LiveTokenManager from "prismarine-auth/src/TokenManagers/LiveTokenManager.js";
import JavaTokenManager from "prismarine-auth/src/TokenManagers/MinecraftJavaTokenManager.js";
import XboxTokenManager from "prismarine-auth/src/TokenManagers/XboxTokenManager.js";
import MsaTokenManager from "prismarine-auth/src/TokenManagers/MsaTokenManager.js";
import BedrockTokenManager from "prismarine-auth/src/TokenManagers/MinecraftBedrockTokenManager.js";
async function retry(methodFn, beforeRetry, times) {
while (times--) {
if (times !== 0) {
try {
return await methodFn();
}
catch (e) {
if (e instanceof URIError) {
throw e;
}
else {
// debug(e);
}
}
await new Promise((resolve) => setTimeout(resolve, 2000));
await beforeRetry();
}
else {
return await methodFn();
}
}
}
export class CustomAuthflow {
username;
options;
codeCallback;
msa;
doTitleAuth;
xbl;
mba;
mca;
constructor(username = "", cache, options, codeCallback) {
this.username = username;
if (options && !options.flow) {
throw new Error("Missing 'flow' argument in options. See docs for more information.");
}
this.options = options || { flow: "msal" };
this.initTokenManagers(username, cache);
this.codeCallback = codeCallback;
}
initTokenManagers(username, cache) {
if (this.options.flow === "live" || this.options.flow === "sisu") {
if (!this.options.authTitle)
throw new Error(`Please specify an "authTitle" in Authflow constructor when using ${this.options.flow} flow`);
this.msa = new LiveTokenManager(this.options.authTitle, ["service::user.auth.xboxlive.com::MBI_SSL"], cache({ cacheName: this.options.flow, username }));
this.doTitleAuth = true;
}
else if (this.options.flow === "msal") {
const config = Object.assign({ ...msalConfig }, this.options.authTitle ? { auth: { ...msalConfig.auth, clientId: this.options.authTitle } } : {});
this.msa = new MsaTokenManager(config, ["XboxLive.signin", "offline_access"], cache({ cacheName: "msal", username }));
}
else {
throw new Error(`Unknown flow: ${this.options.flow} (expected "live", "sisu", or "msal")`);
}
const keyPair = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
this.xbl = new XboxTokenManager(keyPair, cache({ cacheName: "xbl", username }));
this.mba = new BedrockTokenManager(cache({ cacheName: "bed", username }));
this.mca = new JavaTokenManager(cache({ cacheName: "mca", username }));
}
static resetTokenCaches(cache) {
if (!cache)
throw new Error("You must provide a cache directory to reset.");
try {
if (fs.existsSync(cache)) {
fs.rmSync(cache, { recursive: true });
return true;
}
}
catch (e) {
console.log("Failed to clear cache dir", e);
return false;
}
}
async getMsaToken() {
if (await this.msa.verifyTokens()) {
const { token } = await this.msa.getAccessToken();
return token;
}
else {
const ret = await this.msa.authDeviceCode((response) => {
if (this.codeCallback)
return this.codeCallback(response);
console.info("[msa] First time signing in. Please authenticate now:");
console.info(response.message);
});
if (ret.account) {
console.info(`[msa] Signed in as ${ret.account.username}`);
}
else {
// We don't get extra account data here per scope
console.info("[msa] Signed in with Microsoft");
}
return ret.accessToken;
}
}
async getXboxToken(relyingParty = this.options.relyingParty || Endpoints.XboxRelyingParty, forceRefresh = false) {
const options = { ...this.options, relyingParty };
const { xstsToken, userToken, deviceToken, titleToken } = await this.xbl.getCachedTokens(relyingParty);
if (xstsToken.valid && !forceRefresh) {
return xstsToken.data;
}
if (options.password) {
const xsts = await this.xbl.doReplayAuth(this.username, options.password, options);
return xsts;
}
return await retry(async () => {
const msaToken = await this.getMsaToken();
// sisu flow generates user and title tokens differently to other flows and should also be used to refresh them if they are invalid
if (options.flow === "sisu" && (!userToken.valid || !deviceToken.valid || !titleToken.valid)) {
const dt = await this.xbl.getDeviceToken(options);
const sisu = await this.xbl.doSisuAuth(msaToken, dt, options);
return sisu;
}
const ut = userToken.token ?? (await this.xbl.getUserToken(msaToken, options.flow === "msal"));
const dt = deviceToken.token ?? (await this.xbl.getDeviceToken(options));
const tt = titleToken.token ?? (this.doTitleAuth ? await this.xbl.getTitleToken(msaToken, dt) : undefined);
const xsts = await this.xbl.getXSTSToken({ userToken: ut, deviceToken: dt, titleToken: tt }, options);
return xsts;
}, () => {
this.msa.forceRefresh = true;
}, 2);
}
async getMinecraftJavaToken(options = {}, quit) {
const response = { token: "", entitlements: {}, profile: {} };
if (await this.mca.verifyTokens()) {
const { token } = await this.mca.getCachedAccessToken();
response.token = token;
}
else {
await retry(async () => {
const xsts = await this.getXboxToken(Endpoints.PCXSTSRelyingParty);
response.token = await this.mca.getAccessToken(xsts);
if (quit.quit)
return;
}, () => {
this.xbl.forceRefresh = true;
}, 2);
}
if (quit.quit)
return;
if (options.fetchEntitlements) {
response.entitlements = await this.mca.fetchEntitlements(response.token).catch((e) => { });
}
if (options.fetchProfile) {
response.profile = await this.mca.fetchProfile(response.token).catch((e) => { });
}
if (options.fetchCertificates) {
response.certificates = await this.mca.fetchCertificates(response.token).catch((e) => []);
}
return response;
}
async getMinecraftBedrockToken(publicKey) {
// TODO: Fix cache, in order to do cache we also need to cache the ECDH keys so disable it
// is this even a good idea to cache?
if ((await this.mba.verifyTokens()) && false) {
// eslint-disable-line
const { chain } = this.mba.getCachedAccessToken();
return chain;
}
else {
if (!publicKey)
throw new Error("Need to specifiy a ECDH x509 URL encoded public key");
return await retry(async () => {
const xsts = await this.getXboxToken(Endpoints.BedrockXSTSRelyingParty);
const token = await this.mba.getAccessToken(publicKey, xsts);
// If we want to auth with a title ID, make sure there's a TitleID in the response
const body = JSON.parse(Buffer.from(token.chain[1].split(".")[1], "base64").toString());
if (!body.extraData.titleId && this.doTitleAuth) {
throw Error("missing titleId in response");
}
return token.chain;
}, () => {
this.xbl.forceRefresh = true;
}, 2);
}
}
}