How Minecraft launchers work

I’ve been recently investigating how Minecraft launchers work, and I found that there was surprisingly little information about Minecraft launchers except for this wiki article on the Microsoft Authentication Scheme and this great blog post named Inside a Minecraft Launcher, both of which were very helpful though containing some inaccuracies.

I learned a lot from my experience building a Minecraft launcher, and I believe that as of Aug 15, 2023, my knowledge of Minecraft launchers is up to date. I thought it’d be interesting to share them here.

Authentication

Currently, the only authentication method you need to be aware of is the Microsoft Authentication Scheme. It is detailed in Microsoft Authentication Scheme (archived), which basically requires you to perform the following steps:

  • First, create an Azure application and obtain an OAuth2 client ID. Note that by default, you can not use this application to authenticate to Minecraft Services unless you have received approval from Mojang. See this support article for details. When in development, you can also use the official Minecraft launcher’s client ID to test your code, which is 00000000402B5328.
  • When the user requests to log in through Microsoft, create a web view and send them to the following URL: https://login.live.com/oauth20_authorize.srf?prompt=select_account&client_id={YOUR CLIENT ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri={YOUR REDIRECT URI}
    • When you are experimenting with the official Minecraft launcher’s client ID, you will need the following information:
    • The redirect URI is https://login.live.com/oauth20_desktop.srf, or https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf if url-encoded.
    • Pass the following additional query arguments to the oauth20_authorize.srf endpoint: lw=1, fl=dob,easi2, xsup=1, nopa=2.
    • Instead of XboxLive.signin offline_access for the scope, you should use service::user.auth.xboxlive.com::MBI_SSL (url-encoded, service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL).
    • Using the above information will not prompt an “authorize this app” page, and you will receive an authorization code immediately after the user signs in.
  • After the user authorizes or cancels, they will be redirected to your redirect URI with certain query arguments.
    • If the user authorized your app, the code will be in the query parameters. Take note of it.
    • If the user cancels, the error will be access_denied in the query parameters.
    • Otherwise, there might be an error somewhere.
  • Now, you will need to exchange the authorization code for the Microsoft tokens. Make a GET request to https://login.live.com/oauth20_token.srf?client_id={YOUR CLIENT ID}&code={THE CODE YOU RECEIVED}&redirect_uri={YOUR REDIRECT URI, SEE ABOVE}&grant_type=authorization_code&scope={YOUR SCOPES, SEE ABOVE}.
    • The response will be similar to: { "access_token": "...", "refresh_token": "...", "expires_in": "...", ... }
    • Take note of the access token, refresh token, and expires in values (this is not strictly needed). Store them on the user’s device.
  • You will now need to authenticate the user through Xbox Live.
    • Send a POST request to https://user.auth.xboxlive.com/user/authenticate. Make sure you have application/json in your Accept and Content-Type headers. The body should be the following: { "Properties": { "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": "{YOUR MICROSOFT ACCESS TOKEN}" }, "RelyingParty": "<http://auth.xboxlive.com>", "TokenType": "JWT" }
    • Caveat: Sometimes (unknown when), this will result in a 400 status code, indicating bad format for the request. In this case, add d= before {YOUR MICROSOFT ACCESS TOKEN} in RpsTicket and retry. It should work again. It is uncertain when d= is required, and the results seem to differ on different devices. Therefore, you should probably do the request without d= first, and do it with d= if the first request fails.
    • The response will be similar to: { "IssueInstant": "2020-12-07T19:52:08.4463796Z", "NotAfter": "2020-12-21T19:52:08.4463796Z", "Token": "{TAKE NOTE OF THIS, YOUR XBOX LIVE TOKEN}", "DisplayClaims": { "xui": [ { "uhs": "{TAKE NOTE OF THIS, YOUR USERHASH}" } ] } }
  • You will now need to authenticate the user through XSTS.
    • Send a POST request to https://xsts.auth.xboxlive.com/xsts/authorize. Again, make sure your headers are correct. The body should be: { "Properties": { "SandboxId": "RETAIL", "UserTokens": ["{YOUR XBOX LIVE TOKEN}"] }, "RelyingParty": "rp://api.minecraftservices.com/", "TokenType": "JWT" }
    • The response will be similar to { "IssueInstant": "2020-12-07T19:52:08.4463796Z", "NotAfter": "2020-12-21T19:52:08.4463796Z", "Token": "{TAKE NOTE OF THIS, YOUR XSTS TOKEN}", "DisplayClaims": { "xui": [ { "uhs": "{SAME AS PREVIOUS REQUEST}" } ] } }
    • In some cases, the endpoint can return a 401 error, and the response body will contain XErr. There are a few known XErr codes:
      • 2148916233: The account doesn’t have an Xbox account. Once they sign up for one (or login through minecraft.net to create one), they can proceed with the login. This shouldn’t happen with accounts that have purchased Minecraft with a Microsoft account, as they would’ve already gone through the Xbox signup process.
      • 2148916235: The account is from a country where Xbox Live is not available/banned
      • 2148916236/2148916237: The account needs adult verification on the Xbox page. (South Korea)
      • 2148916238: The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult.
  • For newer versions with Microsoft integration, you will need the user’s XUID to launch the game with support for Microsoft integration, such as Xbox Live family settings. This step is optional, and the game works fine without the XUID.
    • To do so, send the same POST request to https://xsts.auth.xboxlive.com/xsts/authorize as in the previous step, but with the following amendments in the body:
    • In Properties, add the following: "OptionalDisplayClaims": ["mgt", "mgs", "umg"].
    • Replace RelyingParty with http://xboxlive.com. Note that this is not https://xboxlive.com.
    • In the response, you can get the user’s XUID in DisplayClaims.xui.0.xid and the user’s Xbox gamertag in DisplayClaims.xui.0.gtg.
  • You will now need to authenticate with Microsoft Services.
    • Send a POST request to https://api.minecraftservices.com/authentication/login_with_xbox with the body: { "identityToken": "XBL3.0 x={USERHASH};{XSTS TOKEN}" }
    • The response will be similar to: { "username": "some uuid, not the user's uuid", "roles": [], "access_token": "{TAKE NOTE OF THIS, MINECRAFT ACCESS TOKEN}", "token_type": "Bearer", "expires_in": 86400 }
  • You will now need to fetch the user’s Minecraft profile, which includes the user’s actual UUID and Minecraft profile name.
    • Send a GET request to https://api.minecraftservices.com/minecraft/profile with the header Authorization: Bearer {MINECRAFT ACCESS TOKEN}.
    • The response will be similar to: { "id": "6cc9ba8e88034534a3d2ade79263cb1e", // The user's actual UUID. Take note of this. "name": "Dreta", // The user's profile name. Take note of this. "skins": [ { "id": "...", "state": "ACTIVE", "url": "...", "variant": "...", "alias": "..." } ], "capes": [ ... ] }
    • If the response contains "error": "NOT_FOUND", then the user does not have a Minecraft profile. It is possible that the user does not own Minecraft, but it’s also possible that the user hasn’t created a Minecraft profile yet. In this case, you need to instruct the user to log in to minecraft.net or the official Minecraft launcher at least once to create a profile first.
  • The Microsoft authentication process is now complete.

Download

The version manifest

Before downloading the game, you should download the list of all available vanilla versions (the “version manifest”) at https://piston-meta.mojang.com/mc/game/version_manifest.json and store it somewhere. The version manifest contains information about the latest releases and snapshots, along with a list of all releases.

Each release in versions of the version manifest has the same structure, that being:

{
    "id": "1.20.1",
    "type": "release", // one of release, snapshot, old_beta and old_alpha,
    "url": "<https://piston-meta.mojang.com/v1/packages/31fc23d93c5baed9cab1a8855fc2086f15584d0e/1.20.1.json>",
    "time": "2023-08-09T11:43:41+00:00",
    "releaseTime": "2023-06-12T13:25:51+00:00"
}

The version metadata

After choosing which version to download, you should download the url of the version present in the version manifest, which is the version metadata. The version metadata is quite complicated and will be used throughout the downloading and launching of the game. For now, we will focus on the download part only.

The asset index, the client, and the logging configurations

These are the easier parts of the download.

  • First, download the asset index. Find the assetIndex entry in the version metadata. It should be similar to: "assetIndex": { "id": "7", "sha1": "0d51506baf97687eafd8e7991d7698816f4e6769", "size": 411581, "totalSize": 618189521, "url": "<https://piston-meta.mojang.com/v1/packages/0d51506baf97687eafd8e7991d7698816f4e6769/7.json>" } Download the url, and verify it against sha1 and size. The totalSize is the total size of all assets in this asset index, useful for calculating the progress of asset downloads. Store this in .minecraft/assets/indexes/{id}.json.
  • Second, download the logging config. Find the logging entry in the version metadata. It should be similar to: "logging": { "client": { "argument": "-Dlog4j.configurationFile=${path}", "file": { "id": "client-1.12.xml", "sha1": "bd65e7d2e3c237be76cfbef4c2405033d7f91521", "size": 888, "url": "<https://piston-data.mojang.com/v1/objects/bd65e7d2e3c237be76cfbef4c2405033d7f91521/client-1.12.xml>", }, "type": "log4j2-xml" } } Again, download the url, and verify it against sha1 and size. Store this in .minecraft/assets/log_configs/{id (already contains .xml)}.
  • At last, download the client. Find the downloads entry in the version metadata. It should be similar to: "downloads": { "client": { "sha1": "08314fae8fbff190e056a8ae4b9fc9cd603436f6", "size": "23110000", "url": "<https://piston-data.mojang.com/v1/objects/08314fae8fbff190e056a8ae4b9fc9cd603436f6/client.jar>", }, ... } Again, download the url, and verify it against sha1 and size. Store this in .minecraft/versions/{version}/{version}.jar.

Assets

In the assets index file you just downloaded, there is a single entry called objects. The entries within objects are similar to the following:

"icons/icon_128x128.png": {
    "hash": "b62ca8ec10d07e6bf5ac8dae0c8c1d2e6a1e3356",
    "size": 9101
}

The name (or the key) of the asset isn’t really important, it’s only used for showing which asset you are downloading to the user. The hash, however, is important. To download the asset, you need to download https://resources.download.minecraft.net/{first two characters of hash}/{full hash}. Afterwards, verify the download against the hash and the size. Store it to .minecraft/assets/objects/{first two characters of hash}/{full hash}. Repeat for all assets.

Libraries

Arguably the most complicated part of the download, downloading libraries requires extensive work. You can find a list of libraries to download in libraries in the version metadata. Each of which is similar to the following:

{
    "downloads": {
        "artifact": ...,
        "classifiers": ...,
    },
    "name": "...",
    "extract": {
        "exclude": ["META-INF/"]
    },
    "rules": [...],
    "natives": {...}
}

Not all of these keys are always present in the library. In newer versions, downloads.classifiers, natives and extract does not exist. rules are present occasionally. name is always present. downloads.artifact is present most of the times. extract is typically present when downloads.classifiers is present.

Rule parsing (for libraries)

Before downloading the library, you need to check if you need to download it. If rules isn’t present in the library, you will always need to download it. If they are present, however, you can parse them as follows:

  • Rules look similar to: { "action": "allow", // or "disallow" "features": {...}, "os": { "name": "...", "version": "...", "arch": "..." } }
  • For rule parsing in libraries, features is generally not present.
  • For os, the name is either linux, osx or windows. The arch could be potentially x86, x86_64 (or amd64) or arm(...). The version is a regular expression that you match with the operating system’s version.
  • When parsing rules, start with not allowing the download of this library (the download status). Go from top to bottom, when a rule matches the status of the operating system, set the download status to action == "allow".
  • Only download the library if the final download status is allow.

Downloading the primary artifact

For newer versions, this is the only download you will need, and native bindings are included as primary artifacts. The primary artifact is located in downloads.artifact and is not always present. It looks similar to:

"artifact": {
    "path": "com/github/oshi/oshi-core/6.2.2/oshi-core-6.2.2.jar",
    "sha1": "54f5efc19bca95d709d9a37d19ffcbba3d21c1a6",
    "size": 947865,
    "url": "<https://libraries.minecraft.net/com/github/oshi/oshi-core/6.2.2/oshi-core-6.2.2.jar>"
}

Download the url and check it against sha1 and size. Store it to .minecraft/libraries/{path}. For example, in this case, you should store it to .minecraft/libraries/com/github/oshi/oshi-core/6.2.2/oshi-core-6.2.2.jar.

Downloading the legacy native bindings

For older versions, classifiers is included for native bindings in the library’s downloads. In this case, you should:

  • First, determine which “classifier” to download through the natives (natives mapping). The natives generally look like: "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" } As you can see, natives maps the operating system to the keys in classifiers. Caveat: In incredibly rare cases, the value in the natives contains ${arch} (for example, "windows": "natives-windows-${arch}"). In this case, replace ${arch} with either 32 or 64 for 32-bit or 64-bit, respectively.
  • classifiers looks like this: "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", "size": 578680, "url": "<https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar>" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", "size": 426822, "url": "<https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar>" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", "size": 613748, "url": "<https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar>" } } Download the url for the key you just mapped from the previous step. Verify the file against sha1 and size, and store it to .minecraft/libraries/{path}.

You have now finished downloading everything, and you’re ready to launch!

Launch

It’s actually rather simple to launch the game.

Setting up the libraries

  • First, determine which libraries are required for the game. You can do so by following the instructions in the download libraries part. Take note of their paths (“library paths”).
  • Second, extract the native bindings to a temporary directory.
    • For libraries with classifiers, these are always native bindings. For newer versions of the game that use generic artifact for all libraries (including native bindings), check if the name of the library contains native to determine whether it’s a native binding. This might not be the best method, but there doesn’t seem to be a better one.
    • Extract the ZIP file (JARs are ZIPs under the hood). These probably contain all sorts of directory structures. Only extract the files ending in .so, .dll and .dylib. (The proper way to do this seems to be to ignore the files and directories stated in extract.exclude, but that’s way too much hassle.)
    • Store them to a temporary directory or anywhere you like. Note the path (“natives path”).

Setting up the arguments

Newer versions

Newer versions of the game have an arguments key in their version metadata. These look like:

"arguments": {
    "game": [
        "--username",
        "${auth_player_name}",
        ...,
        {
            "rules": [
                {
                    "action": "allow",
                    "features": {
                        "is_demo_user": true
                    }
                }
            ],
            "value": "--demo"
        }
    ],
    "jvm": [ ... ]
}

For these, you will need to parse the rules to determine whether arguments are needed first. The rule parsing process is similar to Rule parsing (for libraries), but you also need to consider features, which include the following:

  • has_custom_resolution: Whether the user requested to set a custom resolution for the game.
  • is_demo_user: Whether the user requested to play in demo mode. In the official launcher, this is true when the user hasn’t purchased Minecraft yet.
  • has_quick_plays_support: Only available in 23w14a (1.20 snapshot) or later, “Quick Play” is a feature that allows the user to skip the main menu and enter a world (server) directly after launching the game. Whether the user requested to enable “Quick Play”.
  • is_quick_play_singleplayer: Whether the user chose to “Quick Play” in singleplayer.
  • is_quick_play_multiplayer: Whether the user chose to “Quick Play” in a 3rd-party multiplayer server.
  • is_quick_play_realms: Whether the user chose to “Quick Play” in a Miencraft Realms.

Older versions

Older versions of the game don’t have arguments but instead have minecraftArguments. It looks like:

"minecraftArguments": "--username ${auth_player_name} --version ${version_name} (...)"

In this case, you will need to pass the following arguments that are not specified in the version metadata to the JVM:

-cp ${classpath} -Djava.library.path=${natives_directory} -Djna.tmpdir=${natives_directory} -Dorg.lwjgl.system.SharedLibraryExtractPath=${natives_directory} -Dio.netty.native.workdir=${natives_directory} -Dminecraft.launcher.brand=${launcher_name} -Dminecraft.launcher.version=${launcher_version}

In addition, pass the following argument to the JVM if the user is on Windows: -XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump. Pass the following argument to the JVM if the user is on macOS: -XstartOnFirstThread.

Adding the logging config

The logging config has a separate place for its very own argument at logging.client.argument in the version metadata. You need to replace ${path} with the path to the logging config, and add it to the JVM arguments.

Replace the placeholders

You might have noticed that there are several placeholder values in the arguments. You will need to replace them with the proper arguments, depending on your case. The full list of placeholders is as follows:

  • ${natives_directory}: Your natives directory, as above
  • ${launcher_name}: Your launcher name, not displayed in game, used for telemetry
  • ${launcher_version}: Your launcher version, not displayed in game, used for telemetry
  • ${classpath}: See “Build the classpath” below
  • ${clientid}: Unknown, only present in newer versions
  • ${auth_xuid}: The account’s XUID, only present in newer versions with Microsoft integration
  • ${auth_player_name}: The player’s profile name
  • ${version_name}: The name of the version
  • ${game_directory}: The .minecraft directory
  • ${assets_root}: Typically .minecraft/assets
  • ${assets_index_name}: assetIndex.id in the version metadata
  • ${auth_uuid}: The account’s Minecraft UUID
  • ${auth_access_token}: The account’s Minecraft access token, required for accessing servers with online-mode and Minecraft Realms
  • ${user_type}: msa for Microsoft accounts, mojang for Mojang accounts
  • ${version_type}: The version type, displayed at the bottom left of the main menu, does not have to be the version type of the version
  • ${resolution_width}: The user’s specified custom resolution width
  • ${resolution_height}: The user’s specified custom resolution height
  • ${auth_session}: Unknown, doesn’t appear to be used in game
  • ${user_properties}: Only present in older versions, doesn’t appear to be used in game
  • ${game_assets}: Typically .minecraft/assets
  • ${quickPlayPath}: Where to output the logs files when playing in “Quick Play” mode, relative to .minecraft
  • ${quickPlaySingleplayer}: The path to the singleplayer world when playing in “Quick Play” singleplayer
  • ${quickPlayMultiplayer}: The host of the multiplayer server when playing in “Quick Play” multiplayer
  • ${quickPlayRealms}: The ID of the Minecraft Realms when playing in “Quick Play” Minecraft Realms

Build the classpath

One last thing before you launch the game though, you will need to build the classpath. The classpath is a string that includes all the paths to the libraries and the path to the game client itself separated by :.

Launch the game

Finally, after all the work, you can launch the game.

Determine which Java version to use

With snapshot 21w19a (released on May 12 2021), Minecraft switched to Java 16. With snapshot 1.18 pre-2, Minecraft requires Java 17 or newer. With snapshot 24w14a, Minecraft requires Java 21 or newer. For older versions, Minecraft needs Java 8.

You are recommended to download both Java 21 and Java 8, the details of how to download them are not within the scope of this article. For releases after May 12, 2021, you should probably use Java 21. For releases before that, you need to use Java 8.

For Linux and macOS, make sure you check if bin/java is executable and make it executable if it’s not. For Windows, you will use javaw.exe for launching the game.

Launch the game (finally!)

The main class of the game is in mainClass in the version metadata.

{path to java binary} {jvm args} {main class} {game args}

Congratulations, you have successfully launched the game! Now, it’s Minecraft time. (August 15, 2023)

Further Reading