Created
December 14, 2020 20:59
-
-
Save yonderbread/e0dc075213314aad3462c3954ccdcc08 to your computer and use it in GitHub Desktop.
New Minecraft authentication implemented in Python
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
# Microsoft Authentication Scheme | |
# https://wiki.vg/Microsoft_Authentication_Scheme | |
# | |
# DEV NOTE: Might switch to using xbox-webapi-python | |
# instead later on. | |
# https://github.com/OpenXbox/xbox-webapi-python | |
import requests | |
MS_OAUTH2_URL = "https://login.live.com/oauth20_authorize.srf" | |
XBL_AUTHENTICATE = "https://user.auth.xboxlive.com/user/authenticate" | |
XBL_LOGIN = "https://api.minecraftservices.com/authentication/login_with_xbox" | |
XSTS_AUTHENTICATE = "https://xsts.auth.xboxlive.com/xsts/authorize" | |
API_ENTITLEMENTS_URL = "https://api.minecraftservices.com/entitlements/mcstore" | |
API_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile" | |
class MicrosoftOAuth2: | |
def __init__(self, client_id: str, client_secret: str, redirect_uri: str, oauth2_scope: str = None): | |
self.client_id = client_id | |
self.client_secret = client_secret | |
self.redirect_uri = redirect_uri | |
self.oauth2_scope = oauth2_scope | |
self.oauth2_url = None | |
self.access_token = None | |
self.refresh_token = None | |
self.auth_token = None | |
""" | |
Builds URL for user to visit in order to authenticate | |
""" | |
def get_oauth2_url(self, response_type: str = "code"): | |
url = MS_OAUTH2_URL | |
url += "?client_id=" + self.client_id | |
url += "&response_type=" + response_type | |
url += "&redirect_uri=" + self.redirect_uri | |
if self.oauth2_scope is None: | |
url += "&scope=XboxLive.signin" | |
self.oauth2_url = url | |
return url | |
""" | |
Appends extra url parameters to redirect url for returning an auth code at callback | |
""" | |
def get_redirect(self, code: str, state: str = None): | |
url = "https://" + self.redirect_uri | |
url += "?code=" + code | |
if state is not None: | |
url += "&state=" + state | |
return url | |
""" | |
Fetches auth token and refresh token | |
""" | |
def get_authorization_code(self, oauth2_url: str = None): | |
oauth2_url = self.oauth2_url if not oauth2_url else oauth2_url | |
oauth2_url += "&grant_type=authorization_code" | |
with requests.post(oauth2_url, headers={"Content-Type": "w-xxx-form-urlencoded"}) as req: | |
req.raise_for_status() | |
data = req.json() | |
self.auth_token = data["access_token"] | |
self.refresh_token = data["refresh_token"] | |
return req.json() | |
""" | |
Uses refresh token to revalidate access token | |
""" | |
def refresh(self, refresh_token: str = None, oauth2_url: str = None): | |
refresh_token = self.refresh_token if not refresh_token else refresh_token | |
oauth2_url = self.oauth2_url if not oauth2_url else oauth2_url | |
oauth2_url += "&grant_type=refresh_token" | |
oauth2_url += "&refresh_token=" + refresh_token | |
with requests.post(oauth2_url, headers={"Context-Type": "w-xxx-form-urlencoded"}) as req: | |
req.raise_for_status() | |
return req.json() | |
""" | |
Authenticates via Oauth2 to Xbox Live authentication servers | |
Returns an XBL token required later in the authentication process | |
""" | |
def auth_xbl(self): | |
if not self.auth_token: | |
raise Exception("Cannot authenticate Oauth2 user without a valid authentication token/rps ticket.") | |
payload = { | |
"Properties": {"AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": self.auth_token}, | |
"RelyingParty": "http://auth.xboxlive.com", | |
"TokenType": "JWT", | |
} | |
with requests.post( | |
XBL_AUTHENTICATE, headers={"Content-Type": "application/json", "Accept": "application/json"}, data=payload | |
) as req: | |
req.raise_for_status() | |
return req.json() | |
""" | |
Authenticates via Oauth2 to XSTS authentication servers | |
Fetches the users access token | |
""" | |
def auth_xsts(self, xbl_token: str): | |
payload = { | |
"Properties": {"SandboxId": "RETAIL", "UserTokens": [xbl_token]}, | |
"RelyingParty": "rp://api.minecraftservices.com/", | |
"TokenType": "JWT", | |
} | |
with requests.post( | |
XSTS_AUTHENTICATE, headers={"Content-Type": "application/json", "Accept": "application/json"}, data=payload | |
) as req: | |
req.raise_for_status() | |
data = req.json() | |
self.access_token = data["access_token"] | |
return data | |
""" | |
Login to Minecraft | |
Retrieves the user's game access token to launch as an 'online' player | |
""" | |
@staticmethod | |
def login_with_xbox(self, xsts_token: str, uhs: str): | |
payload = {"identityToken": f"XBL3.0 x={uhs};{xsts_token}"} | |
with requests.post( | |
XBL_LOGIN, headers={"Content-Type": "application/json", "Accept": "application/json"}, data=payload | |
) as req: | |
req.raise_for_status() | |
return req.json() | |
""" | |
Checks the user's store entitlements to see if Minecraft has been purchased | |
""" | |
def check_game_ownership(self): | |
if not self.access_token: | |
raise Exception("Cannot authenticate Oauth2 user without an access token.") | |
with requests.get( | |
API_ENTITLEMENTS_URL, headers={"Accept": "application/json", "Authorization": f"Bearer {self.access_token}"} | |
) as req: | |
req.raise_for_status() | |
data = req.json() | |
if "items" not in data: | |
return False | |
return True | |
""" | |
Fetches various information about the player | |
""" | |
def get_profile(self): | |
if not self.access_token: | |
raise Exception("Cannot authenticate Oauth2 user without an access token.") | |
with requests.get( | |
API_ENTITLEMENTS_URL, headers={"Accept": "application/json", "Authorization": f"Bearer {self.access_token}"} | |
) as req: | |
req.raise_for_status() | |
data = req.json() | |
if req.status_code == 401 or "error" in data: | |
raise Exception("Unauthorized, cannot fetch profile information") | |
return data |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment