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
, orhttps%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 useservice::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 beaccess_denied
in the query parameters. - Otherwise, there might be an error somewhere.
- If the user authorized your app, the
- Now, you will need to exchange the authorization code for the Microsoft tokens. Make a
GET
request tohttps://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.
- The response will be similar to:
- You will now need to authenticate the user through Xbox Live.
- Send a
POST
request tohttps://user.auth.xboxlive.com/user/authenticate
. Make sure you haveapplication/json
in yourAccept
andContent-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, addd=
before{YOUR MICROSOFT ACCESS TOKEN}
inRpsTicket
and retry. It should work again. It is uncertain whend=
is required, and the results seem to differ on different devices. Therefore, you should probably do the request withoutd=
first, and do it withd=
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}" } ] } }
- Send a
- You will now need to authenticate the user through XSTS.
- Send a
POST
request tohttps://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 containXErr
. There are a few knownXErr
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.
- Send a
- 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 tohttps://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
withhttp://xboxlive.com
. Note that this is nothttps://xboxlive.com
. - In the response, you can get the user’s XUID in
DisplayClaims.xui.0.xid
and the user’s Xbox gamertag inDisplayClaims.xui.0.gtg
.
- To do so, send the same
- You will now need to authenticate with Microsoft Services.
- Send a
POST
request tohttps://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 }
- Send a
- 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 tohttps://api.minecraftservices.com/minecraft/profile
with the headerAuthorization: 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 tominecraft.net
or the official Minecraft launcher at least once to create a profile first.
- Send a
- 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 theurl
, and verify it againstsha1
andsize
. ThetotalSize
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 theurl
, and verify it againstsha1
andsize
. 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 theurl
, and verify it againstsha1
andsize
. 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
, thename
is eitherlinux
,osx
orwindows
. The arch could be potentiallyx86
,x86_64 (or amd64)
orarm(...)
. Theversion
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). Thenatives
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 inclassifiers
. Caveat: In incredibly rare cases, the value in thenatives
contains${arch}
(for example,"windows": "natives-windows-${arch}"
). In this case, replace${arch}
with either32
or64
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 theurl
for the key you just mapped from the previous step. Verify the file againstsha1
andsize
, 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 genericartifact
for all libraries (including native bindings), check if the name of the library containsnative
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 inextract.exclude
, but that’s way too much hassle.) - Store them to a temporary directory or anywhere you like. Note the path (“natives path”).
- For libraries with
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 istrue
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 withonline-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)