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 ()
@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