Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access token protection proof of concept #24

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions authproxy/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
plugins {
id 'java-library'
}

repositories {
mavenCentral()
maven {
url = "https://libraries.minecraft.net/"
}
}

dependencies {
compileOnly 'org.slf4j:slf4j-api:2.0.16'

testImplementation "com.mojang:authlib:6.0.57"
testImplementation 'io.javalin:javalin:6.3.0'
testImplementation 'org.slf4j:slf4j-simple:2.0.16'
testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.3"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.3"
}

test {
useJUnitPlatform()
}

tasks.withType(JavaCompile).configureEach {
it.options.release = 17
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.fabricmc.sandbox.authproxy;

public record AccessToken(String realAccessToken, String sandboxToken) {
public RequestProcessor.Header rewriteHeader(RequestProcessor.Header header) {
if (header.key().equals("Authorization") && header.value().startsWith("Bearer ")) {
String value = header.value();

if (sandboxToken.equals(value.substring(7))) {
return new RequestProcessor.Header("Authorization", "Bearer " + realAccessToken);
}

return header;
}

return header;
}
}
118 changes: 118 additions & 0 deletions authproxy/src/main/java/net/fabricmc/sandbox/authproxy/AuthProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package net.fabricmc.sandbox.authproxy;

import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class AuthProxy implements RequestProcessor, AutoCloseable {
private static final String SESSION_HOST = "https://sessionserver.mojang.com";
private static final String SESSION_PATH = "/session";
private static final String SESSION_SYSTEM_PROPERTY = "minecraft.api.session.host";

private static final String SERVICES_HOST = "https://api.minecraftservices.com";
private static final String SERVICES_PATH = "/api";
private static final String SERVICES_SYSTEM_PROPERTY = "minecraft.api.services.host";

private static final List<String> IGNORED_HEADERS = List.of("Connection", "Host", "Content-length");

private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

private final int port;
private final AccessToken accessToken;
private final AuthProxyServer server;

private AuthProxy(int port, AccessToken accessToken, String sessionHost, String servicesHost) throws IOException {
this.port = port;
this.accessToken = accessToken;
this.server = AuthProxyServer.create(port, Map.of(
SESSION_PATH, new ProxyHttpHandler(this, SESSION_PATH, sessionHost, HTTP_CLIENT),
SERVICES_PATH, new ProxyHttpHandler(this, SERVICES_PATH, servicesHost, HTTP_CLIENT)
));
this.server.start();
}

public static AuthProxy create(int port, String realAccessToken, String sandboxToken) throws IOException {
return create(port, new AccessToken(realAccessToken, sandboxToken));
}

public static AuthProxy create(int port, AccessToken accessToken) throws IOException {
return create(port, accessToken, SESSION_HOST, SERVICES_HOST);
}

public static AuthProxy create(int port, AccessToken accessToken, String sessionHost, String servicesHost) throws IOException {
return new AuthProxy(port, accessToken, sessionHost, servicesHost);
}

private String getProxyHost() {
return "http://localhost:" + port;
}

public String getSessionProxyAddress() {
return getProxyHost() + SESSION_PATH;
}

public String getApiProxyAddress() {
return getProxyHost() + SERVICES_PATH;
}

public Map<String, String> getSystemProperties() {
return Map.of(
SESSION_SYSTEM_PROPERTY, getSessionProxyAddress(),
SERVICES_SYSTEM_PROPERTY, getApiProxyAddress()
);
}

// The list of arguments to pass to the Minecraft game to configure it to use the proxy
public String[] getArguments() {
return new String[] {
"-D" + SESSION_SYSTEM_PROPERTY + "=" + getSessionProxyAddress(),
"-D" + SERVICES_SYSTEM_PROPERTY + "=" + getApiProxyAddress()
};
}

@Override
public void close() throws Exception {
server.close();
}

@Override
public Request process(HttpExchange exchange, byte[] body) {
return new Request() {
@Override
public String path() {
return exchange.getRequestURI().getPath();
}

@Override
public String method() {
return exchange.getRequestMethod();
}

@Override
public Header[] headers() {
Header[] headers = Header.of(exchange.getRequestHeaders());
return Arrays.stream(headers)
.filter(header -> !IGNORED_HEADERS.contains(header.key()))
.map(accessToken::rewriteHeader)
.toArray(Header[]::new);
}

@Override
public byte[] body() {
if ("/session/session/minecraft/join".equals(path())) {
String str = new String(body, StandardCharsets.UTF_8);
str = str.replace(accessToken.sandboxToken(), accessToken.realAccessToken());
return str.getBytes(StandardCharsets.UTF_8);
}

return body;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.fabricmc.sandbox.authproxy;

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Map;

public class AuthProxyServer implements AutoCloseable {
private static final int BACKLOG = 8;

private final HttpServer server;

private AuthProxyServer(HttpServer server) {
this.server = server;
}

public static AuthProxyServer create(int port, Map<String, ProxyHttpHandler> handlers) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(port), BACKLOG);
AuthProxyServer authProxyServer = new AuthProxyServer(server);
handlers.forEach(server::createContext);
server.setExecutor(null); // TODO might want to use a thread pool?
return authProxyServer;
}

public void start() {
server.start();
}

@Override
public void close() throws Exception {
server.stop(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package net.fabricmc.sandbox.authproxy;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Arrays;

public class ProxyHttpHandler implements HttpHandler {
private static final boolean ENABLE_SENSITIVE_LOGGING = false;
private static final Logger LOGGER = LoggerFactory.getLogger(ProxyHttpHandler.class);

private final RequestProcessor requestProcessor;
private final String pathPrefix;
private final String proxyHost;
private final HttpClient httpClient;

public ProxyHttpHandler(RequestProcessor requestProcessor, String pathPrefix, String proxyHost, HttpClient httpClient) {
this.requestProcessor = requestProcessor;
this.pathPrefix = pathPrefix;

if (!proxyHost.startsWith("http://") && !proxyHost.startsWith("https://")) {
throw new IllegalArgumentException("Invalid proxy host: " + proxyHost);
}

this.proxyHost = proxyHost;
this.httpClient = httpClient;

LOGGER.info("Proxying requests with path prefix '{}' to '{}'", pathPrefix, proxyHost);
}

@Override
public void handle(HttpExchange exchange) throws IOException {
try {
handleInternal(exchange);
} catch (Exception e) {
e.printStackTrace();
exchange.sendResponseHeaders(500, 0);
} finally {
exchange.close();
}
}

private void handleInternal(HttpExchange exchange) throws IOException {
byte[] body;

try (InputStream is = exchange.getRequestBody()) {
body = is.readAllBytes();
}

if (body.length == 0) {
body = null;
}


if (ENABLE_SENSITIVE_LOGGING) {
LOGGER.warn("Incoming Request:");
LOGGER.warn(" Path: {}", exchange.getRequestURI().toString());
LOGGER.warn(" Method: {}", exchange.getRequestMethod());
LOGGER.warn(" Headers: {}", exchange.getRequestHeaders());
LOGGER.warn(" Body: {}", body != null ? new String(body) : "null");
}

RequestProcessor.Request request = requestProcessor.process(exchange, body);

String path = request.path();
if (!path.startsWith(pathPrefix)) {
exchange.sendResponseHeaders(400, 0);
return;
}

String pathWithoutPrefix = path.substring(pathPrefix.length());
URI proxyUri = URI.create(proxyHost + pathWithoutPrefix);

if (ENABLE_SENSITIVE_LOGGING) {
LOGGER.warn("Proxy Outgoing Request:");
LOGGER.warn(" Path: {}", proxyUri);
LOGGER.warn(" Method: {}", request.method());
LOGGER.warn(" Headers: {}", Arrays.toString(request.headers()));
LOGGER.warn(" Body: {}", request.body() != null ? new String(request.body()) : "null");
}

body = request.body();

HttpRequest.Builder proxyRequest = HttpRequest.newBuilder()
.uri(proxyUri)
.method(request.method(), body != null ? HttpRequest.BodyPublishers.ofByteArray(body) : HttpRequest.BodyPublishers.noBody())
.headers(RequestProcessor.Header.toPairs(request.headers()));

HttpResponse<byte[]> response;

try {
response = httpClient.send(proxyRequest.build(), HttpResponse.BodyHandlers.ofByteArray());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

if (ENABLE_SENSITIVE_LOGGING) {
LOGGER.warn("Proxy Response:");
LOGGER.warn(" Status: {}", response.statusCode());
LOGGER.warn(" Headers: {}", response.headers());
LOGGER.warn(" Body: {}", new String(response.body()));
}

exchange.sendResponseHeaders(response.statusCode(), response.body().length);

if (response.body().length > 0) {
exchange.getResponseBody().write(response.body());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.fabricmc.sandbox.authproxy;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.util.stream.Stream;

public interface RequestProcessor {
Request process(HttpExchange exchange, byte[] body);

interface Request {
String path();

String method();

Header[] headers();

byte[] body();
}

record Header(String key, String value) {
public static Header[] of(Headers headers) {
return headers.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().map(value -> new Header(entry.getKey(), value)))
.toArray(Header[]::new);
}

// Header key value pairs
public static String[] toPairs(Header[] headers) {
return java.util.Arrays.stream(headers)
.flatMap(header -> Stream.of(header.key(), header.value()))
.toArray(String[]::new);
}
}
}
Loading