Skip to content

Instantly share code, notes, and snippets.

@an0ndev
Created June 13, 2021 00:46
Show Gist options
  • Select an option

  • Save an0ndev/331c5052504ccf4919500632a9dd6237 to your computer and use it in GitHub Desktop.

Select an option

Save an0ndev/331c5052504ccf4919500632a9dd6237 to your computer and use it in GitHub Desktop.
pyCraft Microsoft account authentication
# yoink_token.py: pyCraft-compatible Microsoft account authentication
# use the following function:
# def get_mc_auth_token (*, force_use_new_msft_account: bool = False, force_regenerate_mc_client_token: bool = False) -> minecraft.authentication.AuthenticationToken:
# and DO NOT forget to fill in the constants below
# based on https://wiki.vg/Microsoft_Authentication_Scheme and https://wiki.vg/Authentication (for client token desc)
# YOU HAVE TO FILL THESE IN WITH AN AZURE APP
# follow https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
client_id = "FILL_THIS_IN" # uuid
client_secret = "FILL_THIS_IN" # url-safe string
redirect_uri = "http://localhost:65529" # this should be some nonexistent url so the user can copy it into python
# these are files used to cache the microsoft account login and the minecraft client token
client_token_file = "mc_client_token.txt"
save_file = "msft_refresh_token.txt"
import json
import urllib.parse
import uuid
from typing import Optional
import os.path
import requests # pip install requests
import minecraft.authentication # part of pyCraft
url_base = "https://login.live.com/oauth20_{}.srf"
def authenticate_with_msft () -> (str, str):
auth_url_query = {
"client_id": client_id,
"response_type": "code",
"redirect_uri": redirect_uri, # just needs to be an inaccessible url so the user can yoink the token
}
auth_url = f"{url_base.format ('authorize')}?{urllib.parse.urlencode (auth_url_query)}&scope=XboxLive.signin%20offline_access"
print (auth_url)
auth_code = input ("Enter the code param from the url: ")
print (auth_code)
return _make_msft_token_resp (code = auth_code, grant_type = "authorization_code")
def reauthenticate_with_msft (*, refresh_token: str) -> (str, str):
return _make_msft_token_resp (refresh_token = refresh_token, grant_type = "refresh_token")
def _get_from_json (resp: requests.Response, *items: str): return map (resp.json ().__getitem__, items)
def _check_resp (resp: requests.Response):
try:
resp.raise_for_status ()
except:
print (resp.text)
raise
def _json_req (url: str, data: Optional [dict] = None, *, auth_token: Optional [str] = None) -> dict:
req_headers = {"Accept": "application/json"}
if auth_token is not None: req_headers ["Authorization"] = f"Bearer {auth_token}"
function_call_kwargs = {}
meth = "post" if data is not None else "get"
if data is not None:
req_headers ["Content-Type"] = "application/json"
function_call_kwargs ["data"] = json.dumps (data).encode ()
function_call_kwargs ["headers"] = req_headers
resp = getattr (requests, meth) (url, **function_call_kwargs)
_check_resp (resp)
return resp.json ()
def _make_msft_token_resp (*, code: Optional [str] = None, refresh_token: Optional [str] = None, grant_type: str) -> (str, str):
pass
token_url_query = {
"client_id": client_id,
"client_secret": client_secret,
"grant_type": grant_type,
"redirect_uri": redirect_uri
}
if code is not None:
token_url_query ["code"] = code
elif refresh_token is not None:
token_url_query ["refresh_token"] = refresh_token
else:
raise Exception ("need either code or refresh_token")
token_resp = requests.post (f"{url_base.format ('token')}", headers = {"Content-Type": "application/x-www-form-urlencoded"}, data = urllib.parse.urlencode (token_url_query).encode ())
_check_resp (token_resp)
return _get_from_json (token_resp, "access_token", "refresh_token")
def get_mc_auth_token (*, force_use_new_msft_account: bool = False, force_regenerate_mc_client_token: bool = False) -> minecraft.authentication.AuthenticationToken:
if (not os.path.exists (save_file)) or force_use_new_msft_account:
msft_access_token, msft_refresh_token = authenticate_with_msft ()
open (save_file, "w+").write (msft_refresh_token)
else:
msft_refresh_token = open (save_file, "r").read ()
msft_access_token, msft_refresh_token = reauthenticate_with_msft (refresh_token = msft_refresh_token)
open (save_file, "w+").write (msft_refresh_token)
xbl_req_json = {
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": f"d={msft_access_token}"
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
xbl_resp = _json_req ("https://user.auth.xboxlive.com/user/authenticate", xbl_req_json)
xbl_token: str = xbl_resp ["Token"]
xbl_userhash: str = xbl_resp ["DisplayClaims"] ["xui"] [0] ["uhs"]
xsts_req_json = {
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
xbl_token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
xsts_resp = _json_req ("https://xsts.auth.xboxlive.com/xsts/authorize", xsts_req_json)
xsts_token: str = xsts_resp ["Token"]
xsts_userhash: str = xsts_resp ["DisplayClaims"] ["xui"] [0] ["uhs"]
assert xbl_userhash == xsts_userhash
mc_auth_req_json = {"identityToken": f"XBL3.0 x={xbl_userhash};{xsts_token}"}
mc_auth_resp = _json_req ("https://api.minecraftservices.com/authentication/login_with_xbox", mc_auth_req_json)
mc_access_token: str = mc_auth_resp ["access_token"]
mc_ownership_check_resp = _json_req ("https://api.minecraftservices.com/entitlements/mcstore", auth_token = mc_access_token)
if not any (map (lambda item_name: item_name.endswith ("minecraft"), map (lambda item: item ["name"], mc_ownership_check_resp ["items"]))): raise Exception ("account does not own minecraft!")
mc_profile = _json_req ("https://api.minecraftservices.com/minecraft/profile", auth_token = mc_access_token)
mc_uuid = mc_profile ["id"]
mc_username = mc_profile ["name"]
if (not os.path.exists (client_token_file)) or force_regenerate_mc_client_token:
client_token = uuid.uuid4 ().hex
open (client_token_file, "w+").write (client_token)
else:
client_token = open (client_token_file, "r").read ()
auth_token = minecraft.authentication.AuthenticationToken (username = mc_username, access_token = mc_access_token, client_token = client_token)
auth_token.profile = minecraft.authentication.Profile (id_ = mc_uuid, name = mc_username)
return auth_token
if __name__ == "__main__":
get_mc_auth_token ()
@BattleB0t
Copy link

How exactly do you use that with pycraft?

@an0ndev
Copy link
Author

an0ndev commented Nov 24, 2021

How exactly do you use that with pycraft?

Read the directions at the top of the file. It would probably work better as a class, this is more of a POC than anything. But basically once you set the constants, you can use it like so:

import yoink_token

auth_token = yoink_token.get_mc_auth_token ()
connection = minecraft.networking.connection.Connection ("hostname", 25565, auth_token = auth_token)
# etc...

@justinrest
Copy link

justinrest commented Jul 7, 2022

This script has been very useful to me. I've added automatic retrieving of the code from the redirect if anyone wants it.

# yoink_token.py: pyCraft-compatible Microsoft account authentication
# use the following function:
# def get_mc_auth_token (*, force_use_new_msft_account: bool = False, force_regenerate_mc_client_token: bool = False) -> minecraft.authentication.AuthenticationToken:
# and DO NOT forget to fill in the constants below

# When you import this file make sure to create an instance of the class to access the get_mc_auth_token function
# something like this
# from yoink_token import MinecraftToken
# token = MinecraftToken()
# access_token = token.get_mc_auth_token()

# based on https://wiki.vg/Microsoft_Authentication_Scheme and https://wiki.vg/Authentication (for client token desc)

# YOU HAVE TO FILL THESE IN WITH AN AZURE APP
# follow https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app

client_id = "FILL-THIS-IN" # uuid
client_secret = "FILL-THIS-IN" # url-safe string
port = 65529 # web server port
host = "localhost" # web server host, (used to retrieve the code from the url)
redirect_uri = f"http://{host}:{port}/"


client_token_file = "mc_client_token.txt"
save_file = "msft_refresh_token.txt"

from fastapi import FastAPI # pip install fastapi
import json
import urllib.parse
import uuid
from typing import Optional
import os.path
import uvicorn # pip install uvicorn 
import time
import threading
import requests # pip install requests
import minecraft.authentication # part of pyCraft

url_base = "https://login.live.com/oauth20_{}.srf"

class MinecraftToken:
    def __init__(self) -> None:
        self.bearer_token = None

    def authenticate_with_msft (self) -> (str, str):
        auth_url_query = {
            "client_id": client_id,
            "response_type": "code",
            "redirect_uri": redirect_uri, # just needs to be an inaccessible url so the user can yoink the token
        }
        auth_url = f"{url_base.format ('authorize')}?{urllib.parse.urlencode (auth_url_query)}&scope=XboxLive.signin%20offline_access"
        print(f"Login to Microsoft account at:\n{auth_url}")


        threading.Thread(target = self.createServer).start()
        while self.bearer_token is None:
            continue

        return self._make_msft_token_resp (code = self.bearer_token, grant_type = "authorization_code")
    def createServer (self):
        app = FastAPI()
        
        @app.get("/")
        def root(code: str):
            self.bearer_token = code
            uvicorn.stop()
            return "Success"
            
        
        uvicorn.run(app, host=host, port=port,log_level="error")

    def reauthenticate_with_msft (self, *, refresh_token: str) -> (str, str):
        return self._make_msft_token_resp (refresh_token = refresh_token, grant_type = "refresh_token")

    def _get_from_json (self, resp: requests.Response, *items: str): return map (resp.json ().__getitem__, items)
    def _check_resp (self, resp: requests.Response):
        try:
            resp.raise_for_status ()
        except:
            print (resp.text)
            raise
    def _json_req (self, url: str, data: Optional [dict] = None, *, auth_token: Optional [str] = None) -> dict:
        req_headers = {"Accept": "application/json"}
        if auth_token is not None: req_headers ["Authorization"] = f"Bearer {auth_token}"
        function_call_kwargs = {}
        meth = "post" if data is not None else "get"
        if data is not None:
            req_headers ["Content-Type"] = "application/json"
            function_call_kwargs ["data"] = json.dumps (data).encode ()
        function_call_kwargs ["headers"] = req_headers
        resp = getattr (requests, meth) (url, **function_call_kwargs)
        self._check_resp (resp)
        return resp.json ()

    def _make_msft_token_resp (self, *, code: Optional [str] = None, refresh_token: Optional [str] = None, grant_type: str) -> (str, str):
        pass
        token_url_query = {
            "client_id": client_id,
            "client_secret": client_secret,
            "grant_type": grant_type,
            "redirect_uri": redirect_uri
        }
        if code is not None:
            token_url_query ["code"] = code
        elif refresh_token is not None:
            token_url_query ["refresh_token"] = refresh_token
        else:
            raise Exception ("need either code or refresh_token")
        token_resp = requests.post (f"{url_base.format ('token')}", headers = {"Content-Type": "application/x-www-form-urlencoded"}, data = urllib.parse.urlencode (token_url_query).encode ())
        self._check_resp (token_resp)
        return self._get_from_json (token_resp, "access_token", "refresh_token")

    def get_mc_auth_token (self, *, force_use_new_msft_account: bool = False, force_regenerate_mc_client_token: bool = False) -> minecraft.authentication.AuthenticationToken:
        if (not os.path.exists (save_file)) or force_use_new_msft_account:
            msft_access_token, msft_refresh_token = self.authenticate_with_msft ()
            open (save_file, "w+").write (msft_refresh_token)
        else:
            msft_refresh_token = open (save_file, "r").read ()
            msft_access_token, msft_refresh_token = self.reauthenticate_with_msft (refresh_token = msft_refresh_token)
            open (save_file, "w+").write (msft_refresh_token)

        xbl_req_json = {
            "Properties": {
                "AuthMethod": "RPS",
                "SiteName": "user.auth.xboxlive.com",
                "RpsTicket": f"d={msft_access_token}"
            },
            "RelyingParty": "http://auth.xboxlive.com",
            "TokenType": "JWT"
        }
        xbl_resp = self._json_req ("https://user.auth.xboxlive.com/user/authenticate", xbl_req_json)
        xbl_token: str = xbl_resp ["Token"]
        xbl_userhash: str = xbl_resp ["DisplayClaims"] ["xui"] [0] ["uhs"]

        xsts_req_json = {
            "Properties": {
                "SandboxId": "RETAIL",
                "UserTokens": [
                    xbl_token
                ]
            },
            "RelyingParty": "rp://api.minecraftservices.com/",
            "TokenType": "JWT"
        }
        xsts_resp = self._json_req ("https://xsts.auth.xboxlive.com/xsts/authorize", xsts_req_json)
        xsts_token: str = xsts_resp ["Token"]
        xsts_userhash: str = xsts_resp ["DisplayClaims"] ["xui"] [0] ["uhs"]
        assert xbl_userhash == xsts_userhash

        mc_auth_req_json = {"identityToken": f"XBL3.0 x={xbl_userhash};{xsts_token}"}
        mc_auth_resp = self._json_req ("https://api.minecraftservices.com/authentication/login_with_xbox", mc_auth_req_json)
        mc_access_token: str = mc_auth_resp ["access_token"]

        mc_ownership_check_resp = self._json_req ("https://api.minecraftservices.com/entitlements/mcstore", auth_token = mc_access_token)
        if not any (map (lambda item_name: item_name.endswith ("minecraft"), map (lambda item: item ["name"], mc_ownership_check_resp ["items"]))): raise Exception ("account does not own minecraft!")

        mc_profile = self._json_req ("https://api.minecraftservices.com/minecraft/profile", auth_token = mc_access_token)
        mc_uuid = mc_profile ["id"]
        mc_username = mc_profile ["name"]

        if (not os.path.exists (client_token_file)) or force_regenerate_mc_client_token:
            client_token = uuid.uuid4 ().hex
            open (client_token_file, "w+").write (client_token)
        else:
            client_token = open (client_token_file, "r").read ()

        auth_token = minecraft.authentication.AuthenticationToken (username = mc_username, access_token = mc_access_token, client_token = client_token)
        auth_token.profile = minecraft.authentication.Profile (id_ = mc_uuid, name = mc_username)

        return auth_token

if __name__ == "__main__":
    TOKEN = MinecraftToken()
    # print's access token
    print(TOKEN.get_mc_auth_token().access_token)```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment