This document explains how TokenProtector defends the Minecraft session token, why the real security boundary is not just the JVM, the different types of token reads, why there are so many attack vectors, why obfuscation is easy for stealers, and why OS-level attacks cannot be blocked by a Fabric mod.
TokenProtector uses multiple overlapping layers so that no single bypass undoes the protection.
MainMixin intercepts the new User(...) call in Main.main() via @ModifyArg. Before any other mod's constructor mixin fires, the real JWT is stashed into TokenStash.realAccessToken and a fake token is substituted as the accessToken constructor argument. During the initial session handoff, the real JWT is never visible to the User constructor at all.
UserMixin fires at @At("RETURN") of the User constructor. It reads TokenStash.realAccessToken as the source of truth and stores the real session values in TokenVault. Then it overwrites every visible User field (accessToken, uuid, xuid, clientId, name) with configurable fake replacements.
All public getters (getAccessToken(), getSessionId(), etc.) are also intercepted and return fake values to unauthorized callers.
com.mojang.authlib.minecraft.client.MinecraftClient.accessToken is kept fake at rest. Deep hooks on authlib methods (get, post, postInternal, prepareRequest) see the fake value - normal field reads never expose the real JWT.
The real token is reintroduced only through narrow protected call-site redirects at the exact Mojang auth paths that need it (join server, refresh, etc.). It is never restored into the public authlib field for normal request building.
Because MinecraftClient.accessToken stays fake at rest, polling that field at high speed with Unsafe only returns the fake value. The traditional "read one field 20 million times per second" spin-race does not work - there is no window where the real token briefly appears in that field.
Connection-level spin-races also fail. A race on URLConnection.requests (Unsafe→MessageHeader) that waits for the header map to materialize mid-connection still finds no Authorization header, because the real token is never written to the connection object's header map. The Authorization header is set at a lower JDK socket level that a normal Fabric mod cannot hook easily.
A bypass would require a much harder bytecode-level interception of the exact protected auth redirect path or native socket interception, not a field-read race. Even that is detectable as suspicious behavior.
TokenProtector tracks suspicious behavior patterns:
- rapid authlib token polling,
- repeated unsafe field reads,
- legacy/unmapped token probes,
- launcher/OS token exposure.
Multiplayer login, skins, capes, and Realms all function because the real token is delivered to Mojang's auth endpoints through the narrow protected redirects - not through the normal public getters or fields that mods can read.
The token reader tests every meaningful category of access.
These are the simplest and most common attacks.
User.getAccessToken()User.getSessionId()User.getProfileId()User.getXuid()User.getClientId()
If these are blocked, the first line of defense is working.
Modders can bypass getters by reading fields directly.
Field.get(user, "accessToken")Field.get(user, "uuid")Field.get(user, "xuid")
This proves protection must poison the underlying field itself, not just the getter.
Many stealers are written for old Minecraft versions.
They look for classes like:
client.session.Sessionclient.util.Sessionutil.Sessionclass_320
These are thrown at the JVM because a mod can still load old code paths or use old assumptions. If the protection only covers current names, it is incomplete.
Even if the class exists, field names may differ.
Older mods or leaked code may try fields such as:
field_1983field_1982field_34960field_148258_c
The harness checks those too.
A mod can try to reach the token through the Minecraft instance:
MinecraftClient.getInstance().getUser()MinecraftClient.getInstance().sessionMinecraftClient.getInstance().field_1726
These paths are especially important because they are often the first thing a malicious mod tries.
Token stealers can hide the real names, so the protector must not rely on string matching.
The probe suite tests:
- XOR-encoded class/method names
- Base64-encoded identifiers
- Caesar-shifted strings
- split/reassembled identifiers
- names built from raw int arrays
- dynamic
MethodHandlelookups LambdaMetaFactorygenerated accessors
Why so many? Because real malware does not use plain strings. It reconstructs identifiers at runtime to evade detection.
The true hard target is authlib’s internal token storage.
That chain includes:
- private fields on
Minecraftand authlib objects - the
YggdrasilUserApiServiceimplementation MinecraftClient.accessToken
A protection mod must verify that even these internal objects do not leak the real token to mods.
The token reader hooks key lifecycle moments where a stealer is most likely to read a token:
- when the client starts connecting to a server,
- when the handshake begins,
- when
MinecraftClientis constructed.
Bootstrap-boundary constructor argument capture does not work against the initial session handoff. MainMixin intercepts the new User(...) call via @ModifyArg and substitutes a fake token before any observer fires. The real JWT is already in TokenStash.realAccessToken by then, and UserMixin uses that as the source of truth so TokenVault still receives the real value for protected auth paths. If you whitelist a mod that performs its own User construction (e.g. an alt-account manager), that mod is explicitly trusted and certain obscure interception techniques may succeed against its internal operations - TokenProtector cannot prevent a trusted mod from exposing what it has been allowed to access.
This probe polls MinecraftClient.accessToken at extreme speed during server join, hoping to catch the real JWT in a brief window where authlib temporarily holds it. This does not succeed against TokenProtector because MinecraftClient.accessToken stays fake at rest - the real token is never written into that public field. Polling it at any speed only returns the fake value.
These probes hook authlib's getWithEtag/postWithEtag at RETURN - after the full HTTP cycle completes - and inspect the URLConnection object:
connection.getRequestProperty("Authorization")- checks if the header was set during the connect/read phase- Unsafe access to
URLConnection.requests(the internalMessageHeadermap) - may be materialized post-call URLConnection.connected- confirms the socket was established
These probes test whether the protector injects the real token into the connection object during the connect or read phase, rather than at the prepareRequest point (where headers are still null). This does not succeed because the real token is never placed into the connection's header map - the Authorization header is set at a lower JDK socket layer.
Unlike the authlib field race, this probe targets the URLConnection object directly. It spins on Unsafe.getObject(connection, requestsOffset) in a tight loop for up to 10 seconds, waiting for the internal MessageHeader map to materialize (pop from null to an actual object) mid-connection. Once materialized, it switches to the fast Unsafe→MessageHeader path and races on header inspection.
This does not succeed because the Authorization header is never added to the connection's header map. The real token bypasses URLConnection.getRequestProperty() / requests entirely; it is set at a lower socket-write level.
These are not protections; they are leak detectors.
They search:
ProcessHandle.info().commandLine()RuntimeMXBean.getInputArguments()- system properties containing
token,access,session, orauth - environment variables like
TOKEN,ACCESS,MINECRAFT,MOJANG sun.java.command
These methods show why the launcher or OS can leak tokens even if the JVM is locked down.
Because there are many distinct ways to reach the same secret.
A token can be read at any of these levels:
- clean API calls (
User.getAccessToken()) - private field reads (
Field.get(...)) - unsafe memory access (
Unsafe,VarHandle) - generated call sites (
MethodHandle,LambdaMetaFactory) - obfuscated runtime string assembly
- legacy class names and fields
- authlib internals
- operating system process metadata
Each of those is a separate attack surface.
A stealer can be:
- a naive mod using public getters,
- a complex mod using reflection,
- a legacy mod using old class names,
- an obfuscated mod trying to evade detection,
- a side-channel attacker timing the authlib window,
- a launcher or OS process reading the token before the JVM starts.
A protection app cannot ignore any of those assumptions.
Most mod-based token stealers rely on the same underlying read operation: read a String field from a game object.
What changes is only how the code refers to that field.
That is why obfuscation is cheap:
- build the class name at runtime
- decode the method name from bytes
- use
MethodHandleinstead of a direct call - use
LambdaMetaFactoryto create a function object
The actual secret access still follows the same path.
So the defender must protect the data, not just block specific names.
A stealer does not need to be a full malware product.
For a Fabric mod, the JVM gives you everything:
- runtime reflection
- module access if you can use
MethodHandles.privateLookupIn Unsafewhen a mod can obtain it by reflection- the same classloader as the game
- the same process arguments and environment
In this environment, hiding your intent is cheap:
- mod code is still executed by the game,
- strings can be reconstructed at runtime,
- any access path can be wrapped in a generated lambda,
- the JVM does not prevent you from calling
Field.get()on private data if you already have the object.
So attackers simply move from "name-based detection" to "behavior-based detection." That is why the protection app is designed around data poisoning and token isolation instead of blocking a fixed list of identifiers.
A Fabric mod runs inside the Minecraft JVM. That gives it power over Java objects, but not over the process launcher or the operating system.
Examples:
--accessTokenon the command lineSystem.getenv("ACCESS_TOKEN")ProcessHandle.info().commandLine()launcher_accounts.jsonon disk- any native launcher API that exposes secrets
A Fabric mod cannot alter the OS process listing, the environment, or the launcher’s behavior after the game starts.
A mod can only:
- detect the leak,
- warn the user,
- avoid exposing the same secret again inside the JVM.
It cannot stop another process from reading the process command line or environment variables.
If the launcher puts the token in an environment variable or command line argument, every process on the machine may be able to read it.
That is a fundamentally different class of attack than a malicious mod.
A good protection strategy is therefore:
- protect the JVM-visible token paths,
- and still treat launcher/OS leaks as a separate, higher-privilege threat.
TokenProtector hardens Minecraft against common token-stealing techniques through overlapping defensive layers:
- Bootstrap interception - the real JWT is stashed before any other mod's
Userconstructor hook fires; the constructor argument itself is poisoned. - Field and getter poisoning - every visible
Userfield and getter returns fake values to unauthorized callers. - Authlib protection -
MinecraftClient.accessTokenstays fake at rest; the real token is only reintroduced at the narrow auth endpoints that need it. - Spin-race protection - polling
MinecraftClient.accessTokenorURLConnectionheaders at any speed returns the fake value because the real token never enters those objects. Connection-level materialization-tolerant races also find noAuthorizationheader. - Side-channel detection - suspicious polling, repeated unsafe reads, and legacy probes are tracked and surfaced.
Over 90 probe methods across 11 categories have been tested: public APIs, reflection, legacy names, obfuscation, unsafe memory access, authlib internals, constructor-time reads, post-call connection probes, and connection-level spin-races. All return fake data to unauthorized callers while multiplayer, skins, and Realms continue to work through protected auth redirects.
Some attacks are outside the scope of a Fabric mod:
- command-line leaks,
- environment variable leaks,
- launcher-side token storage,
- and disk-based credential files.
The recommendation is to combine TokenProtector (JVM layer) with a trusted launcher and OS-level hardening for the process and filesystem layers.