Support Microsoft authentication from debug mode

main
Charlotte Som 2022-02-13 07:08:39 +00:00
parent b980606088
commit db3c6d2848
7 changed files with 172 additions and 99 deletions

View File

@ -50,6 +50,7 @@ dependencies {
implementation("io.github.spair:imgui-java-natives-windows-ft:$imguiVersion") implementation("io.github.spair:imgui-java-natives-windows-ft:$imguiVersion")
implementation(files("vendor/libs/lwjgl-util.jar")) implementation(files("vendor/libs/lwjgl-util.jar"))
implementation(files("vendor/libs/minecraft_authenticator-2.0.1.jar"))
runtimeOnly("org.joml:joml:1.10.2") runtimeOnly("org.joml:joml:1.10.2")
runtimeOnly("org.anarres:jcpp:1.4.14") runtimeOnly("org.anarres:jcpp:1.4.14")

View File

@ -1,92 +0,0 @@
package site.hackery.unknit;
import com.mojang.authlib.Agent;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
import com.mojang.util.UUIDTypeAdapter;
import net.minecraft.client.main.Main;
import net.minecraft.client.util.Session;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import java.io.IOException;
import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Mixin(Main.class)
public abstract class MixinMinecraftMain {
private static void replace(List<String> arguments, String tag, String value) {
int index = arguments.indexOf(tag);
if (index != -1) {
arguments.set(index + 1, value);
}
}
private static void replaceVersion(List<String> arguments) {
replace(arguments, "--version", "1.18.1");
replace(arguments, "--versionType", "release");
}
private static void replaceSession(List<String> arguments, Session session) {
replace(arguments, "--accessToken", session.getAccessToken());
arguments.add("--username");
arguments.add(session.getUsername());
arguments.add("--uuid");
arguments.add(session.getUuid());
}
@ModifyVariable(method = "main", at = @At("HEAD"), argsOnly = true, remap = false)
private static String[] setArgs(String[] args) {
List<String> arguments = new ArrayList<>(Arrays.asList(args));
replaceVersion(arguments);
try {
String authFile = System.getenv("LOGIN_FILE");
authFile = authFile == null ? "lastlogin.txt" : authFile;
List<String> loginDetails = Files.readAllLines(Paths.get(authFile));
if (loginDetails.size() >= 2) {
Session session = getAuthenticatedSession(loginDetails.get(0), loginDetails.get(1));
replaceSession(arguments, session);
}
} catch (AuthenticationException | IOException ignored) {
}
return arguments.toArray(new String[0]);
}
private static Session getAuthenticatedSession(String username, String password) throws AuthenticationException {
YggdrasilUserAuthentication yggdrasilUserAuthentication = constructUserAuthentication(username, password);
yggdrasilUserAuthentication.logIn();
GameProfile selectedProfile = yggdrasilUserAuthentication.getSelectedProfile();
return new Session(
selectedProfile.getName(),
UUIDTypeAdapter.fromUUID(selectedProfile.getId()),
yggdrasilUserAuthentication.getAuthenticatedToken(),
Optional.empty(),
Optional.empty(),
Session.AccountType.LEGACY // Why legacy?
);
}
private static YggdrasilUserAuthentication constructUserAuthentication(String username, String password) {
YggdrasilUserAuthentication yggdrasilUserAuthentication = (YggdrasilUserAuthentication) new YggdrasilAuthenticationService(Proxy.NO_PROXY, "my epic client token").createUserAuthentication(Agent.MINECRAFT);
yggdrasilUserAuthentication.setUsername(username);
yggdrasilUserAuthentication.setPassword(password);
return yggdrasilUserAuthentication;
}
}

View File

@ -0,0 +1,112 @@
package site.hackery.unknit.auth;
import net.hycrafthd.minecraft_authenticator.login.AuthenticationException;
import net.hycrafthd.minecraft_authenticator.login.Authenticator;
import net.hycrafthd.minecraft_authenticator.login.User;
import net.hycrafthd.minecraft_authenticator.login.file.AuthenticationFile;
import net.minecraft.client.util.Session;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
public class GameAuthenticationFlow {
/**
* Creates an authenticated Minecraft session from given environment variables.
* AUTH_TYPE may be "mojang", "microsoft", or "refresh" (default).
* Under "mojang" authentication, you must supply the "AUTH_USERNAME" and "AUTH_PASSWORD" variables.
* Under "microsoft" authentication, you must supply the "AUTH_TOKEN" variable.
*
* @return a valid Session, or null
*/
public static Session createSessionFromEnvironment() {
try {
Authenticator.Builder readyAuth = createAuthenticator();
if (readyAuth != null) {
Authenticator authenticator = readyAuth.run();
Optional<User> userOpt = authenticator.getUser();
if (userOpt.isEmpty()) {
return null;
}
saveAuthData(authenticator.getResultFile());
User user = userOpt.get();
return new Session(
user.getName(),
user.getUuid(),
user.getAccessToken(),
Optional.empty(),
Optional.empty(),
Session.AccountType.byName(user.getType())
);
}
} catch (AuthenticationException | IOException ignored) {
}
return null;
}
private static Path getAuthDataFile() {
String authFilePath = System.getenv("AUTH_DATA_FILE");
authFilePath = authFilePath == null ? "mc_auth.dat" : authFilePath;
return Paths.get(authFilePath);
}
private static Authenticator.Builder createAuthenticator() throws IOException {
String authType = System.getenv("AUTH_TYPE");
authType = authType == null ? "refresh" : authType;
switch (authType) {
case "mojang":
return createMojangAuthenticator();
case "microsoft":
return createMicrosoftAuthenticator();
case "refresh":
return createRefreshAuthenticator();
}
return null;
}
private static Authenticator.Builder createMojangAuthenticator() {
String username = System.getenv("AUTH_USERNAME");
String password = System.getenv("AUTH_PASSWORD");
if (username == null || password == null) {
throw new RuntimeException("Attempted to perform Mojang authentication without AUTH_USERNAME or AUTH_PASSWORD set");
}
return Authenticator.ofYggdrasil("-", username, password).shouldAuthenticate();
}
private static Authenticator.Builder createMicrosoftAuthenticator() {
String token = System.getenv("AUTH_TOKEN");
if (token == null) {
throw new RuntimeException("Attempted to perform Microsoft authentication without AUTH_TOKEN set");
}
return Authenticator.ofMicrosoft(token).shouldAuthenticate();
}
private static Authenticator.Builder createRefreshAuthenticator() throws IOException {
Path authData = getAuthDataFile();
if (!Files.exists(authData)) {
return null;
}
try (InputStream inputStream = Files.newInputStream(authData)) {
AuthenticationFile file = AuthenticationFile.read(inputStream);
return Authenticator.of(file).shouldAuthenticate();
}
}
private static void saveAuthData(AuthenticationFile file) throws IOException {
try (OutputStream stream = Files.newOutputStream(getAuthDataFile())) {
file.write(stream);
}
}
}

View File

@ -1,9 +1,10 @@
package site.hackery.unknit; package site.hackery.unknit.mixin;
import net.minecraft.client.*; import net.minecraft.client.ClientBrandRetriever;
import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.callback.*; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(value = ClientBrandRetriever.class, remap = false) @Mixin(value = ClientBrandRetriever.class, remap = false)
public abstract class MixinClientBrandRetriever { public abstract class MixinClientBrandRetriever {

View File

@ -1,4 +1,4 @@
package site.hackery.unknit; package site.hackery.unknit.mixin;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.util.ModStatus; import net.minecraft.util.ModStatus;

View File

@ -0,0 +1,51 @@
package site.hackery.unknit.mixin;
import net.minecraft.client.main.Main;
import net.minecraft.client.util.Session;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import site.hackery.unknit.auth.GameAuthenticationFlow;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Mixin(Main.class)
public abstract class MixinMinecraftMain {
private static void replace(List<String> arguments, String tag, String value) {
int index = arguments.indexOf(tag);
if (index != -1) {
arguments.set(index + 1, value);
}
}
private static void replaceVersion(List<String> arguments) {
replace(arguments, "--version", "1.18.1");
replace(arguments, "--versionType", "release");
}
private static void replaceSession(List<String> arguments, Session session) {
replace(arguments, "--accessToken", session.getAccessToken());
arguments.add("--username");
arguments.add(session.getUsername());
arguments.add("--uuid");
arguments.add(session.getUuid());
}
@ModifyVariable(method = "main", at = @At("HEAD"), argsOnly = true, remap = false)
private static String[] setArgs(String[] args) {
List<String> arguments = new ArrayList<>(Arrays.asList(args));
replaceVersion(arguments);
Session session = GameAuthenticationFlow.createSessionFromEnvironment();
if (session != null) {
replaceSession(arguments, session);
}
return arguments.toArray(new String[0]);
}
}

View File

@ -1,6 +1,6 @@
{ {
"required": true, "required": true,
"package": "site.hackery.unknit", "package": "site.hackery.unknit.mixin",
"compatibilityLevel": "JAVA_17", "compatibilityLevel": "JAVA_17",
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1