Created
June 13, 2021 00:46
-
-
Save an0ndev/331c5052504ccf4919500632a9dd6237 to your computer and use it in GitHub Desktop.
pyCraft Microsoft account authentication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 () |
Author
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
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: